very-happy-cli 0.2.154 → 0.2.156

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/dist/{AcpBackend-B14xNj5L.cjs → AcpBackend-BHe1_F1h.cjs} +3 -3
  2. package/dist/{AcpBackend-Ckyadhf4.mjs → AcpBackend-C3nc1u-s.mjs} +2 -2
  3. package/dist/{AcpSessionManager-CCHmPdrR.cjs → AcpSessionManager-BVxkQnNf.cjs} +1 -1
  4. package/dist/{AcpSessionManager-H8t4P-IW.mjs → AcpSessionManager-dN3k1LbI.mjs} +1 -1
  5. package/dist/agentGuidance-C-CmV03f.cjs +1665 -0
  6. package/dist/agentGuidance-C604n0Zq.mjs +1613 -0
  7. package/dist/{agentHome-BUsLTZnz.cjs → agentHome-CmIJNV7f.cjs} +5 -5
  8. package/dist/{agentHome-B20RWdgE.mjs → agentHome-D2XGsoqs.mjs} +5 -5
  9. package/dist/{auto-BQnncDcs.mjs → auto-B1I6G_Nf.mjs} +21 -21
  10. package/dist/{auto-IeldARBS.cjs → auto-CMDq4oIu.cjs} +24 -24
  11. package/dist/codex/happyMcpStdioBridge.cjs +6 -3
  12. package/dist/codex/happyMcpStdioBridge.mjs +6 -3
  13. package/dist/{config-DM_xI9M6.cjs → config-BYHSRY2E.cjs} +8 -8
  14. package/dist/{config-B-ocQh5V.mjs → config-BkTu25bH.mjs} +8 -8
  15. package/dist/{index-ZKuBjvHi.mjs → index-B5QbzEga.mjs} +21 -13
  16. package/dist/{index-BUxBxSxJ.cjs → index-B_-7NpGZ.cjs} +21 -13
  17. package/dist/{index-eCjkm8Y2.cjs → index-BbtR-Qto.cjs} +606 -982
  18. package/dist/{index-okLyUzeD.cjs → index-CB8QnjvS.cjs} +7 -7
  19. package/dist/{index-BELV05ko.mjs → index-DAAreiRF.mjs} +548 -911
  20. package/dist/{index-CIx3248o.mjs → index-PYO8Mdz4.mjs} +7 -7
  21. package/dist/index.cjs +6 -6
  22. package/dist/index.mjs +6 -6
  23. package/dist/{install-CoLN3whR.mjs → install-CNtXzwT_.mjs} +1 -1
  24. package/dist/{install-BZOdxfPd.cjs → install-CO1ps6Wt.cjs} +1 -1
  25. package/dist/{installTerminalHooks-CA4OcAS7.cjs → installTerminalHooks-BOAnQ1Qe.cjs} +5 -5
  26. package/dist/{installTerminalHooks-Dde_8t36.mjs → installTerminalHooks-CMpOV3RU.mjs} +5 -5
  27. package/dist/lib.cjs +2 -2
  28. package/dist/lib.d.cts +24 -8
  29. package/dist/lib.d.mts +24 -8
  30. package/dist/lib.mjs +2 -2
  31. package/dist/{limits-Dt7_unH4.mjs → limits-DmMdchTz.mjs} +309 -13
  32. package/dist/{limits-CmzISka2.cjs → limits-o29KXBgj.cjs} +329 -12
  33. package/dist/{mcp-9Ui4nQZL.mjs → mcp-C_uejotl.mjs} +17 -17
  34. package/dist/{mcp-7iDHlyCI.cjs → mcp-CwK1nI7Y.cjs} +23 -23
  35. package/dist/{piRuntime-C4E4pgye.mjs → piRuntime-Cu2-8nOT.mjs} +2 -2
  36. package/dist/{piRuntime-BesGGPME.cjs → piRuntime-Dj5WnqzJ.cjs} +2 -2
  37. package/dist/{piTerminal-fUogwxYr.cjs → piTerminal-D-BIbov9.cjs} +6 -6
  38. package/dist/{piTerminal-CCEGCQOY.mjs → piTerminal-DanZeBrO.mjs} +6 -6
  39. package/dist/{runGemini-CNPGSsSK.cjs → runGemini--QqK-Ap5.cjs} +7 -7
  40. package/dist/{runGemini-BW-pEklU.mjs → runGemini-CZ_lhMWY.mjs} +6 -6
  41. package/dist/{runOpenClaw-Bsodkg_s.cjs → runOpenClaw-C0xLhTvk.cjs} +6 -6
  42. package/dist/{runOpenClaw-NRB6-_jU.mjs → runOpenClaw-CxgLpm38.mjs} +5 -5
  43. package/dist/{send-DYeSFg8s.mjs → send-BaOkDgH0.mjs} +5 -39
  44. package/dist/{send-Bd00QcHC.cjs → send-CTcGMVuG.cjs} +8 -42
  45. package/dist/{sessions-DlPOYKLU.mjs → sessions-Bn304xF1.mjs} +88 -45
  46. package/dist/{sessions-RIFUoy_g.cjs → sessions-OmvyJb0n.cjs} +99 -56
  47. package/dist/{spawn-D1bhyHbH.mjs → spawn-BKNabutV.mjs} +22 -22
  48. package/dist/{spawn-DxrfHTF4.cjs → spawn-CC6V2KiA.cjs} +30 -30
  49. package/dist/{teams-pfKHpkgL.mjs → teams-CLbS2_4Q.mjs} +8 -8
  50. package/dist/{teams-Cfy0VMCo.cjs → teams-XtrH3_h3.cjs} +8 -8
  51. package/dist/{todo-B2CjDg_V.mjs → todo-BcPbNKt2.mjs} +8 -6
  52. package/dist/{todo-B4cBFzHB.cjs → todo-CEDvKNXK.cjs} +8 -6
  53. package/dist/{types-D5RWyBE5.mjs → types-BEVzYSoV.mjs} +52 -291
  54. package/dist/{types-Bd_TGr0F.cjs → types-CphWjqHc.cjs} +64 -320
  55. package/package.json +7 -7
  56. package/dist/agentGuidance-DUBXppv-.cjs +0 -554
  57. package/dist/agentGuidance-TLYQxsm2.mjs +0 -530
@@ -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-CphWjqHc.cjs');
7
+ var persistence = require('./limits-o29KXBgj.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-C-CmV03f.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-CmIJNV7f.cjs'); });
5406
+ const { describeAgentHome } = await Promise.resolve().then(function () { return require('./types-CphWjqHc.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,10 @@ function startDaemonControlServer({
6293
6240
  pushClipboard,
6294
6241
  pushFilePreview,
6295
6242
  onTerminalHook,
6296
- setTerminalTitle
6243
+ setTerminalTitle,
6244
+ onSessionEdit,
6245
+ listPeers,
6246
+ isTurnActive
6297
6247
  }) {
6298
6248
  return new Promise((resolve) => {
6299
6249
  const app = fastify({
@@ -6377,7 +6327,8 @@ function startDaemonControlServer({
6377
6327
  children: z.z.array(z.z.object({
6378
6328
  startedBy: z.z.string(),
6379
6329
  happySessionId: z.z.string(),
6380
- pid: z.z.number()
6330
+ pid: z.z.number(),
6331
+ turnActive: z.z.boolean()
6381
6332
  }))
6382
6333
  })
6383
6334
  }
@@ -6389,7 +6340,8 @@ function startDaemonControlServer({
6389
6340
  children: children.filter((child) => child.happySessionId !== void 0).map((child) => ({
6390
6341
  startedBy: child.startedBy,
6391
6342
  happySessionId: child.happySessionId,
6392
- pid: child.pid
6343
+ pid: child.pid,
6344
+ turnActive: isTurnActive ? isTurnActive(child.happySessionId) : false
6393
6345
  }))
6394
6346
  };
6395
6347
  });
@@ -6616,6 +6568,45 @@ function startDaemonControlServer({
6616
6568
  }
6617
6569
  return { status: "ok" };
6618
6570
  });
6571
+ typed.post("/session-edit", {
6572
+ schema: {
6573
+ body: z.z.object({
6574
+ sessionId: z.z.string().regex(/^[a-zA-Z0-9_-]{1,128}$/),
6575
+ path: z.z.string().min(1).max(4096),
6576
+ tool: z.z.string().min(1).max(64),
6577
+ cwd: z.z.string().max(4096).optional(),
6578
+ /** When the runner saw the call (ms). Clamped to now by the daemon;
6579
+ * older than the conflict window = ignored (replayed history). */
6580
+ at: z.z.number().int().nonnegative().optional()
6581
+ }),
6582
+ response: { 200: z.z.object({ status: z.z.literal("ok") }) }
6583
+ }
6584
+ }, async (request) => {
6585
+ try {
6586
+ onSessionEdit?.(request.body);
6587
+ } catch (error) {
6588
+ persistence.logger.debug("[CONTROL SERVER] session-edit handler failed:", error);
6589
+ }
6590
+ return { status: "ok" };
6591
+ });
6592
+ typed.post("/peers", {
6593
+ schema: {
6594
+ response: {
6595
+ 200: z.z.object({
6596
+ sessions: z.z.array(z.z.object({
6597
+ sessionId: z.z.string(),
6598
+ kind: z.z.enum(["managed", "mirror"]),
6599
+ pid: z.z.number().optional(),
6600
+ cwd: z.z.string().optional(),
6601
+ flavor: z.z.string().optional(),
6602
+ title: z.z.string().optional(),
6603
+ variant: z.z.string().optional(),
6604
+ edits: z.z.array(z.z.object({ path: z.z.string(), tool: z.z.string(), at: z.z.number() }))
6605
+ }))
6606
+ })
6607
+ }
6608
+ }
6609
+ }, async () => ({ sessions: listPeers ? listPeers() : [] }));
6619
6610
  typed.post("/stop", {
6620
6611
  schema: {
6621
6612
  response: {
@@ -6651,6 +6642,273 @@ function startDaemonControlServer({
6651
6642
  });
6652
6643
  }
6653
6644
 
6645
+ const EDIT_CONFLICT_WINDOW_MS = 30 * 6e4;
6646
+ function pairKey$1(a, b, path) {
6647
+ return a < b ? `${a}\0${b}\0${path}` : `${b}\0${a}\0${path}`;
6648
+ }
6649
+ class EditConflictTracker {
6650
+ windowMs;
6651
+ maxPaths;
6652
+ /** real path → sessionId → latest edit */
6653
+ byPath = /* @__PURE__ */ new Map();
6654
+ /** (sorted pair, path) → when the notice went out */
6655
+ notified = /* @__PURE__ */ new Map();
6656
+ constructor(options = {}) {
6657
+ this.windowMs = options.windowMs ?? EDIT_CONFLICT_WINDOW_MS;
6658
+ this.maxPaths = options.maxPaths ?? 5e3;
6659
+ }
6660
+ /**
6661
+ * Remember an edit and return the peers to notify about it. `isLive`
6662
+ * decides whether another session still counts (dead ones are dropped
6663
+ * from the table on the spot).
6664
+ */
6665
+ record(edit, isLive) {
6666
+ this.prune(edit.at);
6667
+ let sessions = this.byPath.get(edit.path);
6668
+ if (!sessions) {
6669
+ sessions = /* @__PURE__ */ new Map();
6670
+ this.byPath.set(edit.path, sessions);
6671
+ }
6672
+ const conflicts = [];
6673
+ for (const [sessionId, peer] of sessions) {
6674
+ if (sessionId === edit.sessionId) continue;
6675
+ if (!isLive(sessionId)) {
6676
+ sessions.delete(sessionId);
6677
+ continue;
6678
+ }
6679
+ if (edit.at - peer.at > this.windowMs) continue;
6680
+ const key = pairKey$1(edit.sessionId, sessionId, edit.path);
6681
+ const notifiedAt = this.notified.get(key);
6682
+ if (notifiedAt !== void 0 && edit.at - notifiedAt <= this.windowMs) continue;
6683
+ this.notified.set(key, edit.at);
6684
+ conflicts.push({ editor: edit, peer });
6685
+ }
6686
+ sessions.set(edit.sessionId, edit);
6687
+ return conflicts;
6688
+ }
6689
+ /** Paths this session edited inside the window, newest first. */
6690
+ editsOf(sessionId, now, limit = 20) {
6691
+ const out = [];
6692
+ for (const [path, sessions] of this.byPath) {
6693
+ const edit = sessions.get(sessionId);
6694
+ if (edit && now - edit.at <= this.windowMs) out.push({ path, tool: edit.tool, at: edit.at });
6695
+ }
6696
+ return out.sort((a, b) => b.at - a.at).slice(0, limit);
6697
+ }
6698
+ /** Session ended: nothing it did counts any more. */
6699
+ forget(sessionId) {
6700
+ for (const [path, sessions] of this.byPath) {
6701
+ sessions.delete(sessionId);
6702
+ if (sessions.size === 0) this.byPath.delete(path);
6703
+ }
6704
+ for (const key of this.notified.keys()) {
6705
+ const [a, b] = key.split("\0");
6706
+ if (a === sessionId || b === sessionId) this.notified.delete(key);
6707
+ }
6708
+ }
6709
+ /** Drop everything older than the window (and cap the table size). */
6710
+ prune(now) {
6711
+ for (const [path, sessions] of this.byPath) {
6712
+ for (const [sessionId, edit] of sessions) if (now - edit.at > this.windowMs) sessions.delete(sessionId);
6713
+ if (sessions.size === 0) this.byPath.delete(path);
6714
+ }
6715
+ for (const [key, at] of this.notified) if (now - at > this.windowMs) this.notified.delete(key);
6716
+ if (this.byPath.size > this.maxPaths) {
6717
+ 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);
6718
+ for (const entry of oldest.slice(0, this.byPath.size - this.maxPaths)) this.byPath.delete(entry.path);
6719
+ }
6720
+ }
6721
+ /** Test / diagnostics helper. */
6722
+ size() {
6723
+ return this.byPath.size;
6724
+ }
6725
+ }
6726
+ function normalizeEditPath(path$1, cwd, realpath = node_fs.realpathSync.native ?? node_fs.realpathSync) {
6727
+ const absolute = path.isAbsolute(path$1) ? path.resolve(path$1) : path.resolve(cwd, path$1);
6728
+ let existing = absolute;
6729
+ const tail = [];
6730
+ for (let depth = 0; depth < 64; depth++) {
6731
+ try {
6732
+ const real = realpath(existing);
6733
+ return tail.length ? path.join(real, ...tail) : real;
6734
+ } catch {
6735
+ const parent = path.dirname(existing);
6736
+ if (parent === existing) break;
6737
+ tail.unshift(existing.slice(parent.length).replace(new RegExp(`^\\${path.sep}`), ""));
6738
+ existing = parent;
6739
+ }
6740
+ }
6741
+ return absolute;
6742
+ }
6743
+
6744
+ const CONFLICT_COALESCE_MS = 1e4;
6745
+ const MAX_NOTICES_PER_PAIR = 5;
6746
+ function pairKey(a, b) {
6747
+ return a < b ? `${a}\0${b}` : `${b}\0${a}`;
6748
+ }
6749
+ function senderOf(info, sessionId) {
6750
+ return { sessionId, title: info?.title, flavor: info?.flavor, cwd: info?.cwd };
6751
+ }
6752
+ function createPeerCoordinator(deps) {
6753
+ const now = deps.now ?? (() => Date.now());
6754
+ const windowMs = deps.windowMs ?? EDIT_CONFLICT_WINDOW_MS;
6755
+ const coalesceMs = deps.coalesceMs ?? CONFLICT_COALESCE_MS;
6756
+ const maxNoticesPerPair = deps.maxNoticesPerPair ?? MAX_NOTICES_PER_PAIR;
6757
+ const tracker = new EditConflictTracker({ windowMs });
6758
+ const readPersisted = deps.readPersisted ?? persistence.readPersistedSessions;
6759
+ const send = deps.send ?? agentGuidance.sendUserMessage;
6760
+ const normalize = deps.normalize ?? normalizeEditPath;
6761
+ const liveTitle = deps.liveTitle ?? (async (sessionId, persisted) => (await agentGuidance.readSessionMetadata(sessionId, persisted))?.summary?.text);
6762
+ const schedule = deps.schedule ?? ((fn, ms) => {
6763
+ const t = setTimeout(fn, ms);
6764
+ t.unref?.();
6765
+ return t;
6766
+ });
6767
+ let getMirrors = deps.getMirrors ?? (() => []);
6768
+ const pending = /* @__PURE__ */ new Map();
6769
+ const noticesSent = /* @__PURE__ */ new Map();
6770
+ let inflight = Promise.resolve();
6771
+ const snapshot = () => {
6772
+ const at = now();
6773
+ const out = /* @__PURE__ */ new Map();
6774
+ for (const child of deps.getChildren()) {
6775
+ const id = child.happySessionId;
6776
+ if (!id || out.has(id)) continue;
6777
+ const meta = child.happySessionMetadataFromLocalWebhook;
6778
+ out.set(id, {
6779
+ sessionId: id,
6780
+ kind: "managed",
6781
+ pid: child.pid,
6782
+ ...meta?.path ? { cwd: meta.path } : {},
6783
+ ...meta?.flavor ? { flavor: meta.flavor } : {},
6784
+ ...meta?.summary?.text ? { title: meta.summary.text } : {},
6785
+ ...meta?.variant ?? child.variant ? { variant: meta?.variant ?? child.variant } : {},
6786
+ edits: tracker.editsOf(id, at)
6787
+ });
6788
+ }
6789
+ for (const mirror of getMirrors()) {
6790
+ if (out.has(mirror.sessionId)) continue;
6791
+ out.set(mirror.sessionId, {
6792
+ sessionId: mirror.sessionId,
6793
+ kind: "mirror",
6794
+ flavor: "terminal-mirror",
6795
+ ...mirror.cwd ? { cwd: mirror.cwd } : {},
6796
+ ...mirror.title ? { title: mirror.title } : {},
6797
+ edits: tracker.editsOf(mirror.sessionId, at)
6798
+ });
6799
+ }
6800
+ return out;
6801
+ };
6802
+ const deliver = async (recipient, peer, peerId, paths, peerEditedAgoMs, noticeId) => {
6803
+ if (recipient.kind === "mirror") return;
6804
+ const persisted = readPersisted()[recipient.sessionId];
6805
+ if (!persisted) {
6806
+ persistence.logger.debug(`[PEERS] no key for ${recipient.sessionId}; conflict notice ${noticeId} not delivered`);
6807
+ return;
6808
+ }
6809
+ const peerSender = senderOf(peer, peerId);
6810
+ if (!peerSender.title && peer?.kind === "managed") {
6811
+ const peerKey = readPersisted()[peer.sessionId];
6812
+ if (peerKey) {
6813
+ try {
6814
+ peerSender.title = await liveTitle(peer.sessionId, peerKey);
6815
+ } catch {
6816
+ }
6817
+ }
6818
+ }
6819
+ const text = agentGuidance.formatEditConflictNotice({ id: noticeId, path: paths[0], paths, peer: peerSender, peerEditedAgoMs, windowMs });
6820
+ try {
6821
+ await send(recipient.sessionId, persisted, text, "edit-conflict", {
6822
+ sentFrom: agentGuidance.SESSION_PEER_SENT_FROM,
6823
+ delivery: "steer",
6824
+ localId: `edit-conflict-${noticeId}-${recipient.sessionId}`
6825
+ });
6826
+ persistence.logger.debug(`[PEERS] conflict notice ${noticeId} \u2192 ${recipient.sessionId} (${paths.length} file(s), first ${paths[0]}, peer ${peerId})`);
6827
+ } catch (error) {
6828
+ persistence.logger.debug(`[PEERS] conflict notice ${noticeId} \u2192 ${recipient.sessionId} failed:`, error);
6829
+ }
6830
+ };
6831
+ const flushPair = async (key) => {
6832
+ const pair = pending.get(key);
6833
+ if (!pair) return;
6834
+ pending.delete(key);
6835
+ const at = now();
6836
+ const sent = (noticesSent.get(key) ?? []).filter((t) => at - t <= windowMs);
6837
+ if (sent.length >= maxNoticesPerPair) {
6838
+ noticesSent.set(key, sent);
6839
+ persistence.logger.debug(`[PEERS] pair ${pair.a}/${pair.b}: ${maxNoticesPerPair} notices already sent this window; ${pair.paths.size} conflicting path(s) not announced`);
6840
+ return;
6841
+ }
6842
+ sent.push(at);
6843
+ noticesSent.set(key, sent);
6844
+ const sessions = snapshot();
6845
+ const a = sessions.get(pair.a);
6846
+ const b = sessions.get(pair.b);
6847
+ const paths = [...pair.paths.keys()];
6848
+ const noticeId = node_crypto.randomUUID().slice(0, 8);
6849
+ const latestBy = (sessionId) => Math.max(...[...pair.paths.values()].map((c) => c.editor.sessionId === sessionId ? c.editor.at : c.peer.at));
6850
+ const agoFor = (recipientId, otherId) => Math.max(0, latestBy(recipientId) - latestBy(otherId));
6851
+ if (a) await deliver(a, b, pair.b, paths, agoFor(pair.a, pair.b), noticeId);
6852
+ if (b) await deliver(b, a, pair.a, paths, agoFor(pair.b, pair.a), noticeId);
6853
+ };
6854
+ const enqueue = (conflict) => {
6855
+ const key = pairKey(conflict.editor.sessionId, conflict.peer.sessionId);
6856
+ let pair = pending.get(key);
6857
+ if (!pair) {
6858
+ pair = { a: conflict.editor.sessionId, b: conflict.peer.sessionId, paths: /* @__PURE__ */ new Map(), timer: null };
6859
+ pair.timer = schedule(() => {
6860
+ inflight = inflight.then(() => flushPair(key)).catch(() => {
6861
+ });
6862
+ }, coalesceMs);
6863
+ pending.set(key, pair);
6864
+ }
6865
+ pair.paths.set(conflict.editor.path, conflict);
6866
+ };
6867
+ return {
6868
+ list: () => [...snapshot().values()],
6869
+ onEdit(edit) {
6870
+ const sessions = snapshot();
6871
+ const self = sessions.get(edit.sessionId);
6872
+ if (!self) {
6873
+ persistence.logger.debug(`[PEERS] edit from unknown/dead session ${edit.sessionId} ignored`);
6874
+ return [];
6875
+ }
6876
+ const current = now();
6877
+ const at = typeof edit.at === "number" && Number.isFinite(edit.at) ? Math.min(edit.at, current) : current;
6878
+ if (current - at > windowMs) {
6879
+ persistence.logger.debug(`[PEERS] edit from ${edit.sessionId} on ${edit.path} is ${Math.round((current - at) / 6e4)}m old (replayed history); ignored`);
6880
+ return [];
6881
+ }
6882
+ const cwd = edit.cwd ?? self.cwd ?? process.cwd();
6883
+ const path = normalize(edit.path, cwd);
6884
+ const conflicts = tracker.record({ sessionId: edit.sessionId, path, tool: edit.tool, at }, (id) => sessions.has(id));
6885
+ for (const conflict of conflicts) enqueue(conflict);
6886
+ return conflicts;
6887
+ },
6888
+ async flush() {
6889
+ for (const key of [...pending.keys()]) {
6890
+ const pair = pending.get(key);
6891
+ if (pair?.timer && typeof pair.timer === "object") clearTimeout(pair.timer);
6892
+ inflight = inflight.then(() => flushPair(key)).catch(() => {
6893
+ });
6894
+ }
6895
+ await inflight;
6896
+ },
6897
+ forget(sessionId) {
6898
+ tracker.forget(sessionId);
6899
+ for (const [key, pair] of pending) {
6900
+ if (pair.a === sessionId || pair.b === sessionId) {
6901
+ if (pair.timer && typeof pair.timer === "object") clearTimeout(pair.timer);
6902
+ pending.delete(key);
6903
+ }
6904
+ }
6905
+ },
6906
+ setMirrorSource(source) {
6907
+ getMirrors = source;
6908
+ }
6909
+ };
6910
+ }
6911
+
6654
6912
  const ASSISTANT_CLAUDE_MD = `# very-happy \u8C03\u5EA6\u4E2D\u5FC3\uFF08\u8BED\u97F3\u52A9\u624B\uFF09
6655
6913
 
6656
6914
  \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 +8445,7 @@ function parseJsonObject(text) {
8187
8445
  return null;
8188
8446
  }
8189
8447
  }
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)) {
8448
+ 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-BbtR-Qto.cjs', document.baseURI).href))).resolve(s)) {
8191
8449
  const platformArch = `${platform}-${arch}`;
8192
8450
  const candidates = [
8193
8451
  `@anthropic-ai/claude-agent-sdk-${platformArch}/claude`,
@@ -8570,6 +8828,73 @@ class ClaudeAuthService {
8570
8828
  }
8571
8829
  }
8572
8830
 
8831
+ const CLAUDE_EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
8832
+ function pathOf(input) {
8833
+ if (!input || typeof input !== "object") return null;
8834
+ const record = input;
8835
+ const candidate = record.file_path ?? record.notebook_path;
8836
+ return typeof candidate === "string" && candidate.trim() !== "" ? candidate : null;
8837
+ }
8838
+ function extractClaudeEditPaths(body) {
8839
+ if (!body || typeof body !== "object") return [];
8840
+ const line = body;
8841
+ if (line.type !== "assistant") return [];
8842
+ const content = line.message?.content;
8843
+ if (!Array.isArray(content)) return [];
8844
+ const stamp = line.timestamp;
8845
+ const at = typeof stamp === "string" ? Date.parse(stamp) : typeof stamp === "number" ? stamp : NaN;
8846
+ const edits = [];
8847
+ for (const block of content) {
8848
+ if (!block || typeof block !== "object") continue;
8849
+ const b = block;
8850
+ if (b.type !== "tool_use" || typeof b.name !== "string" || !CLAUDE_EDIT_TOOLS.has(b.name)) continue;
8851
+ const path = pathOf(b.input);
8852
+ if (path) edits.push({ path, tool: b.name, ...Number.isFinite(at) ? { at } : {} });
8853
+ }
8854
+ return edits;
8855
+ }
8856
+ function extractCodexPatchPaths(changes) {
8857
+ if (!changes || typeof changes !== "object" || Array.isArray(changes)) return [];
8858
+ return Object.keys(changes).filter((path) => path.trim() !== "").map((path) => ({ path, tool: "CodexPatch" }));
8859
+ }
8860
+ function extractAcpEditPaths(toolKind, args) {
8861
+ if (!args || typeof args !== "object") return [];
8862
+ const a = args;
8863
+ if (a.piTool === "write" || a.piTool === "edit") {
8864
+ const raw = a.rawInput;
8865
+ const path = raw && typeof raw.path === "string" && raw.path.trim() !== "" ? raw.path : null;
8866
+ return path ? [{ path, tool: a.piTool }] : [];
8867
+ }
8868
+ if (typeof a.piTool === "string") return [];
8869
+ if (toolKind !== "edit" || !Array.isArray(a.locations)) return [];
8870
+ const edits = [];
8871
+ for (const location of a.locations) {
8872
+ const path = location && typeof location === "object" ? location.path : void 0;
8873
+ if (typeof path === "string" && path.trim() !== "" && !edits.some((e) => e.path === path)) edits.push({ path, tool: "edit" });
8874
+ }
8875
+ return edits;
8876
+ }
8877
+ class EditReportThrottle {
8878
+ constructor(intervalMs = 5e3) {
8879
+ this.intervalMs = intervalMs;
8880
+ }
8881
+ lastReportedAt = /* @__PURE__ */ new Map();
8882
+ /** Returns the edits that should be reported now and remembers them. */
8883
+ take(edits, now = Date.now()) {
8884
+ const due = [];
8885
+ for (const edit of edits) {
8886
+ const last = this.lastReportedAt.get(edit.path);
8887
+ if (last !== void 0 && now - last < this.intervalMs) continue;
8888
+ this.lastReportedAt.set(edit.path, now);
8889
+ due.push(edit);
8890
+ }
8891
+ if (this.lastReportedAt.size > 512) {
8892
+ for (const [path, at] of this.lastReportedAt) if (now - at > this.intervalMs * 10) this.lastReportedAt.delete(path);
8893
+ }
8894
+ return due;
8895
+ }
8896
+ }
8897
+
8573
8898
  const INTERNAL_CLAUDE_EVENT_TYPES = /* @__PURE__ */ new Set(["file-history-snapshot", "change", "queue-operation"]);
8574
8899
  function parseLines(lines) {
8575
8900
  const out = [];
@@ -8606,12 +8931,15 @@ function createMirrorScanner(opts) {
8606
8931
  }
8607
8932
  tail.offset = 0;
8608
8933
  }
8934
+ let replaced = false;
8609
8935
  if (tail.offset > size) {
8610
8936
  persistence.logger.debug(`[MIRROR SCANNER] ${tail.path}: offset ${tail.offset} > size ${size} \u2014 file replaced, resetting (localId idempotency absorbs replays)`);
8611
8937
  tail.offset = 0;
8938
+ replaced = true;
8612
8939
  }
8613
8940
  if (tail.offset === size) return;
8614
8941
  const isFirstBackfillRead = tail.mode === "backfill-tail" && tail.offset === 0;
8942
+ const replay = isFirstBackfillRead || replaced;
8615
8943
  const handle = await promises.open(tail.path, "r");
8616
8944
  let buf;
8617
8945
  try {
@@ -8631,7 +8959,7 @@ function createMirrorScanner(opts) {
8631
8959
  if (decision.truncated) opts.events.onBackfillTruncated?.();
8632
8960
  tail.mode = "from-eof";
8633
8961
  }
8634
- if (messages.length > 0) opts.events.onMessages(messages);
8962
+ if (messages.length > 0) opts.events.onMessages(messages, { replay });
8635
8963
  };
8636
8964
  const sync = new index.InvalidateSync(async () => {
8637
8965
  for (const tail of files.values()) {
@@ -8754,8 +9082,17 @@ function createMirrorManager(deps) {
8754
9082
  binding.client.updateMetadata((current) => ({ ...current, ...patch }));
8755
9083
  persistBinding(binding);
8756
9084
  };
8757
- const sendMessages = (binding, messages) => {
9085
+ const sendMessages = (binding, messages, context = { replay: false }) => {
8758
9086
  for (const message of messages) {
9087
+ if (deps.onEdit && binding.status === "active" && !context.replay) {
9088
+ for (const edit of extractClaudeEditPaths(message)) {
9089
+ try {
9090
+ deps.onEdit({ sessionId: binding.happySessionId, path: edit.path, tool: edit.tool, cwd: binding.metadata.path, ...edit.at !== void 0 ? { at: edit.at } : {} });
9091
+ } catch (error) {
9092
+ persistence.logger.debug(`[MIRROR] edit tap failed for terminal ${binding.terminalId}:`, error);
9093
+ }
9094
+ }
9095
+ }
8759
9096
  const key = index.mirrorLineKey(message);
8760
9097
  binding.client.sendClaudeSessionMessage(message, key ? { localIdFor: (envelopeIndex) => index.mirrorLocalId(key, envelopeIndex) } : void 0);
8761
9098
  }
@@ -8764,7 +9101,7 @@ function createMirrorManager(deps) {
8764
9101
  const scanner = createMirrorScanner({
8765
9102
  backfillLines,
8766
9103
  events: {
8767
- onMessages: (messages) => sendMessages(binding, messages),
9104
+ onMessages: (messages, context) => sendMessages(binding, messages, context),
8768
9105
  onBackfillTruncated: opts.withTruncationNotice ? () => binding.client.sendSessionEvent({
8769
9106
  type: "message",
8770
9107
  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 +9124,10 @@ function createMirrorManager(deps) {
8787
9124
  await binding.scanner?.cleanup();
8788
9125
  binding.scanner = null;
8789
9126
  binding.client.closeClaudeSessionTurn("completed");
9127
+ try {
9128
+ deps.onSessionEnded?.(binding.happySessionId);
9129
+ } catch {
9130
+ }
8790
9131
  updateBindingMetadata(binding, {
8791
9132
  lifecycleState: "archived",
8792
9133
  lifecycleStateSince: Date.now(),
@@ -8794,9 +9135,9 @@ function createMirrorManager(deps) {
8794
9135
  archiveReason: reason
8795
9136
  });
8796
9137
  try {
8797
- await deps.api.deactivateSession(binding.happySessionId);
9138
+ await deps.api.archiveSession(binding.happySessionId);
8798
9139
  } catch (error) {
8799
- persistence.logger.debug(`[MIRROR] deactivateSession failed for ${binding.happySessionId}:`, error);
9140
+ persistence.logger.debug(`[MIRROR] archiveSession failed for ${binding.happySessionId}:`, error);
8800
9141
  }
8801
9142
  deps.onBindingsChanged?.();
8802
9143
  };
@@ -8999,6 +9340,13 @@ function createMirrorManager(deps) {
8999
9340
  }
9000
9341
  };
9001
9342
  return {
9343
+ listActiveMirrors() {
9344
+ return [...bindings.values()].filter((binding) => binding.status === "active").map((binding) => ({
9345
+ sessionId: binding.happySessionId,
9346
+ ...binding.metadata.path ? { cwd: binding.metadata.path } : {},
9347
+ ...binding.metadata.summary?.text ? { title: binding.metadata.summary.text } : {}
9348
+ }));
9349
+ },
9002
9350
  handleHookPayload(body) {
9003
9351
  const event = index.parseTerminalHookPayload(body);
9004
9352
  if (!event) {
@@ -9465,683 +9813,6 @@ Delegate from this working directory unless a different repository was explicitl
9465
9813
  ${goal}`;
9466
9814
  }
9467
9815
 
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
9816
  function receiptPath(home, operationId) {
10146
9817
  if (!/^[a-zA-Z0-9_-]{1,128}$/.test(operationId)) throw new Error("Invalid team operation id");
10147
9818
  return path.join(home, "teams", "effects", `${operationId}.json`);
@@ -10295,8 +9966,8 @@ function createTeamWorker(deps) {
10295
9966
  await complete(op, receipt);
10296
9967
  return;
10297
9968
  }
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 } : {} });
9969
+ const key = await agentGuidance.waitForSessionKey(receipt.sessionId, 1e4);
9970
+ 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
9971
  receipt = { ...receipt, phase: "delivered" };
10301
9972
  writeReceipt$1(home, receipt);
10302
9973
  }
@@ -10312,7 +9983,7 @@ function createTeamWorker(deps) {
10312
9983
  if (!bot || bot.generation !== op.generation || bot.sessionId !== op.sessionId) throw new Error("Worker binding changed; preserve resources for reconciliation");
10313
9984
  if (op.sessionId) {
10314
9985
  await deps.stopAndWait(op.sessionId);
10315
- await archiveSession(op.sessionId);
9986
+ await agentGuidance.archiveSession(op.sessionId);
10316
9987
  }
10317
9988
  if (team.archivedAt !== void 0) {
10318
9989
  await complete(op, receipt);
@@ -10339,7 +10010,7 @@ function createTeamWorker(deps) {
10339
10010
  const bot = team.bots.find((b) => b.id === message.recipientBotId);
10340
10011
  if (!bot?.sessionId || !deps.live(bot.sessionId)) continue;
10341
10012
  try {
10342
- const key = await waitForSessionKey(bot.sessionId, 0);
10013
+ const key = await agentGuidance.waitForSessionKey(bot.sessionId, 0);
10343
10014
  if (message.scheduleId) {
10344
10015
  const current = (await http.get(`/v1/teams/${encodeURIComponent(team.id)}`)).data.team;
10345
10016
  const pending = current.messages.find((m) => m.id === message.id);
@@ -10351,7 +10022,7 @@ function createTeamWorker(deps) {
10351
10022
  ${message.body}
10352
10023
 
10353
10024
  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" });
10025
+ await agentGuidance.sendUserMessage(bot.sessionId, key, body, "teams", { localId: `teams-message-${message.id}-${bot.generation}`, sentFrom: "team" });
10355
10026
  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
10027
  } catch {
10357
10028
  deps.log(`Teams message ${message.id} remains pending`);
@@ -10726,16 +10397,16 @@ function automationPromptHeader(name, runId) {
10726
10397
  }
10727
10398
  function automationRunAnchor(runId) {
10728
10399
  const marker = ` run ${runId};`;
10729
- return (body) => userPromptText(body)?.startsWith("[Very Happy automation ") === true && userPromptText(body).includes(marker);
10400
+ return (body) => agentGuidance.userPromptText(body)?.startsWith("[Very Happy automation ") === true && agentGuidance.userPromptText(body).includes(marker);
10730
10401
  }
10731
10402
  async function defaultReadTurn(sessionId, runId) {
10732
10403
  const persisted = persistence.readPersistedSessions()[sessionId];
10733
10404
  if (!persisted) throw new Error(`No local key for session ${sessionId}`);
10734
- return analyzeLatestTurn(await readSessionLog(sessionId, persisted, TURN_WINDOW), { anchor: automationRunAnchor(runId) });
10405
+ return agentGuidance.analyzeLatestTurn(await agentGuidance.readSessionLog(sessionId, persisted, agentGuidance.TURN_WINDOW), { anchor: automationRunAnchor(runId) });
10735
10406
  }
10736
10407
  async function defaultSend(sessionId, text, meta) {
10737
- const persisted = await waitForSessionKey(sessionId, 15e3);
10738
- await sendUserMessage(sessionId, persisted, text, "automation", { ...meta, sentFrom: "automation" });
10408
+ const persisted = await agentGuidance.waitForSessionKey(sessionId, 15e3);
10409
+ await agentGuidance.sendUserMessage(sessionId, persisted, text, "automation", { ...meta, sentFrom: "automation" });
10739
10410
  }
10740
10411
  function expandHome(path$1) {
10741
10412
  if (path$1 === "~") return os.homedir();
@@ -11075,6 +10746,16 @@ ${action.prompt}`, { localId: `automation-${run.id}`, ...meta });
11075
10746
  function daemonExitCode(source) {
11076
10747
  return source === "exception" ? 1 : 0;
11077
10748
  }
10749
+ const HANDOVER_EXIT_CODE = 75;
10750
+
10751
+ function detectDaemonSupervisor(env, platform = process.platform) {
10752
+ const explicit = env.HAPPY_DAEMON_SUPERVISOR?.trim().toLowerCase();
10753
+ if (explicit === "systemd") return "systemd";
10754
+ if (explicit === "none") return "none";
10755
+ if (platform === "linux" && typeof env.INVOCATION_ID === "string" && env.INVOCATION_ID.trim() !== "") return "systemd";
10756
+ return "none";
10757
+ }
10758
+ const SYSTEMD_RESTART_HINT = "systemctl --user restart very-happy-daemon";
11078
10759
 
11079
10760
  const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
11080
10761
  function sanitizeImportTitle(value) {
@@ -11200,7 +10881,7 @@ async function startDaemon() {
11200
10881
  });
11201
10882
  persistence.logger.debug("[DAEMON RUN] Starting daemon process...");
11202
10883
  persistence.logger.debugLargeJson("[DAEMON RUN] Environment", getEnvironmentInfo());
11203
- const runningDaemonVersionMatches = await index.isDaemonRunningCurrentlyInstalledHappyVersion();
10884
+ const runningDaemonVersionMatches = await persistence.isDaemonRunningCurrentlyInstalledHappyVersion();
11204
10885
  const runningDaemonState = await persistence.readDaemonState();
11205
10886
  const runningDaemonEndpointsMatch = Boolean(runningDaemonState) && daemonEndpointsMatch(
11206
10887
  runningDaemonState?.serverUrl,
@@ -11210,7 +10891,7 @@ async function startDaemon() {
11210
10891
  );
11211
10892
  if (!runningDaemonVersionMatches || !runningDaemonEndpointsMatch) {
11212
10893
  persistence.logger.debug("[DAEMON RUN] Daemon version or endpoint mismatch detected, restarting daemon");
11213
- await index.stopDaemon();
10894
+ await persistence.stopDaemon();
11214
10895
  } else {
11215
10896
  persistence.logger.debug("[DAEMON RUN] Daemon version matches, keeping existing daemon");
11216
10897
  console.log("Daemon already running with matching version");
@@ -11230,6 +10911,7 @@ async function startDaemon() {
11230
10911
  persistence.logger.debug("[DAEMON RUN] Auth and machine setup complete");
11231
10912
  const pidToTrackedSession = /* @__PURE__ */ new Map();
11232
10913
  const turnActivity = new index.TurnActivityTracker();
10914
+ const peerCoordinator = createPeerCoordinator({ getChildren: () => Array.from(pidToTrackedSession.values()) });
11233
10915
  const sessionIdToFinishedSession = /* @__PURE__ */ new Map();
11234
10916
  let teamWorker;
11235
10917
  let automationRunner;
@@ -12089,6 +11771,7 @@ async function startDaemon() {
12089
11771
  if (session?.happySessionId && (session.spawnedBy === "teams" || teamWorker?.hasSession(session.happySessionId))) teamWorker?.report(session.happySessionId, "exited");
12090
11772
  if (session?.happySessionId && automationRunner?.hasSession(session.happySessionId)) automationRunner.report(session.happySessionId, "exited");
12091
11773
  if (session?.happySessionId) turnActivity.forget(session.happySessionId);
11774
+ if (session?.happySessionId) peerCoordinator.forget(session.happySessionId);
12092
11775
  if (session?.happySessionId && session.encryption) {
12093
11776
  sessionIdToFinishedSession.set(session.happySessionId, session);
12094
11777
  persistence.logger.debug(`[DAEMON RUN] Process PID ${pid} exited, preserved session ${session.happySessionId} for resume`);
@@ -12147,7 +11830,7 @@ async function startDaemon() {
12147
11830
  sessionId
12148
11831
  );
12149
11832
  assistantReportLastSentAt.set(sessionId, Date.now());
12150
- await sendUserMessage(
11833
+ await agentGuidance.sendUserMessage(
12151
11834
  decision.assistantSessionId,
12152
11835
  assistantEntry,
12153
11836
  formatAssistantReportMessage(title, sessionId, event),
@@ -12174,6 +11857,13 @@ async function startDaemon() {
12174
11857
  onSessionStateEvent,
12175
11858
  onClaudeAuthFailed: (sessionId) => claudeAuthServiceRef?.signalAuthFailed(sessionId),
12176
11859
  onSessionTurnEvent: (sessionId, event) => turnActivity.apply(sessionId, event),
11860
+ onSessionEdit: (edit) => {
11861
+ peerCoordinator.onEdit(edit);
11862
+ },
11863
+ listPeers: () => peerCoordinator.list(),
11864
+ // B-505: `daemon list` shows which sessions have a turn in flight, so an
11865
+ // operator can check the WHOLE machine before a supervised restart.
11866
+ isTurnActive: (sessionId) => turnActivity.activeSessions().includes(sessionId),
12177
11867
  pushClipboard: (text, terminalId) => {
12178
11868
  if (!apiMachineRef) {
12179
11869
  return { delivered: false, truncated: false, totalBytes: 0, error: "daemon is still starting up" };
@@ -12193,6 +11883,8 @@ async function startDaemon() {
12193
11883
  }
12194
11884
  });
12195
11885
  const daemonClaudeCredentials = resolveClaudeCredentialReadiness();
11886
+ const daemonSupervisor = detectDaemonSupervisor(process.env);
11887
+ persistence.logger.debug(`[DAEMON RUN] Supervisor: ${daemonSupervisor}`);
12196
11888
  let fileState = {
12197
11889
  pid: process.pid,
12198
11890
  httpPort: controlPort,
@@ -12202,7 +11894,8 @@ async function startDaemon() {
12202
11894
  serverUrl: persistence.configuration.serverUrl,
12203
11895
  webappUrl: persistence.configuration.webappUrl,
12204
11896
  daemonLogPath: persistence.logger.logFilePath,
12205
- claudeCredentialSource: daemonClaudeCredentials.source
11897
+ claudeCredentialSource: daemonClaudeCredentials.source,
11898
+ supervisor: daemonSupervisor
12206
11899
  };
12207
11900
  persistence.writeDaemonState(fileState);
12208
11901
  persistence.logger.debug("[DAEMON RUN] Daemon state written");
@@ -12239,9 +11932,12 @@ async function startDaemon() {
12239
11932
  const mirrorManager = createMirrorManager({
12240
11933
  api,
12241
11934
  machineId,
12242
- onBindingsChanged: () => apiMachine.requestTerminalListRefresh()
11935
+ onBindingsChanged: () => apiMachine.requestTerminalListRefresh(),
11936
+ onEdit: (edit) => peerCoordinator.onEdit(edit),
11937
+ onSessionEnded: (sessionId) => peerCoordinator.forget(sessionId)
12243
11938
  });
12244
11939
  mirrorManagerRef = mirrorManager;
11940
+ peerCoordinator.setMirrorSource(() => mirrorManager.listActiveMirrors());
12245
11941
  apiMachine.setMirrorIntegration({
12246
11942
  resolveMirrorSessionId: (terminalId) => mirrorManager.resolveMirrorSessionId(terminalId),
12247
11943
  onTerminalClosed: (terminalId) => mirrorManager.onTerminalClosed(terminalId),
@@ -12369,9 +12065,13 @@ async function startDaemon() {
12369
12065
  claudeAuthService.stop();
12370
12066
  apiMachine.shutdown();
12371
12067
  await stopControlServer();
12372
- await index.cleanupDaemonState();
12068
+ await persistence.cleanupDaemonState();
12373
12069
  await persistence.releaseDaemonLock(daemonLockHandle);
12374
12070
  await stopCaffeinate();
12071
+ if (daemonSupervisor === "systemd") {
12072
+ persistence.logger.debug(`[DAEMON RUN] systemd-supervised: exiting ${HANDOVER_EXIT_CODE} for the unit to restart on the new bundle`);
12073
+ process.exit(HANDOVER_EXIT_CODE);
12074
+ }
12375
12075
  try {
12376
12076
  spawnHappyCLI(["daemon", "start"], {
12377
12077
  detached: true,
@@ -12451,7 +12151,7 @@ async function startDaemon() {
12451
12151
  }
12452
12152
  apiMachine.shutdown();
12453
12153
  await stopControlServer();
12454
- await index.cleanupDaemonState();
12154
+ await persistence.cleanupDaemonState();
12455
12155
  await stopCaffeinate();
12456
12156
  await persistence.releaseDaemonLock(daemonLockHandle);
12457
12157
  persistence.logger.debug(`[DAEMON RUN] Cleanup completed, exiting process with code ${daemonExitCode(source)}`);
@@ -12555,8 +12255,8 @@ function createTeamAdoption(sessionId) {
12555
12255
 
12556
12256
  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
12257
  if (!intent.guidanceDelivered) {
12558
- const key = await waitForSessionKey(sessionId, 1e4);
12559
- await sendUserMessage(sessionId, key, instructions, "teams", { localId: `teams-adopt-${input.requestId}`, sentFrom: "team" });
12258
+ const key = await agentGuidance.waitForSessionKey(sessionId, 1e4);
12259
+ await agentGuidance.sendUserMessage(sessionId, key, instructions, "teams", { localId: `teams-adopt-${input.requestId}`, sentFrom: "team" });
12560
12260
  await retainTeam(path, input, connected.team.id, true);
12561
12261
  }
12562
12262
  operation.result = { status: "connected", teamId: connected.team.id, botId: connected.botId, instructions };
@@ -12573,127 +12273,6 @@ This session is now connected as the team lead. Call team_inspect to read the au
12573
12273
  };
12574
12274
  }
12575
12275
 
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
12276
  const PERSONAL_MEMORY_SOFT_LIMIT_CHARS = 2e3;
12698
12277
  function headingTitle(line) {
12699
12278
  const m = /^##(?!#)\s*(.*?)\s*$/.exec(line);
@@ -12800,7 +12379,7 @@ function describeSession(id, persisted, extra) {
12800
12379
  if (meta?.tags?.length) parts.push(`tags=${meta.tags.join(",")}`);
12801
12380
  if (meta?.variant === "assistant") parts.push("(assistant)");
12802
12381
  if (extra?.pid) parts.push(`pid=${extra.pid}`);
12803
- parts.push(sessionWebUrl(id));
12382
+ parts.push(agentGuidance.sessionWebUrl(id));
12804
12383
  return parts.join(" ");
12805
12384
  }
12806
12385
  function describeSummary(summary) {
@@ -12825,7 +12404,7 @@ function registerAssistantSessionTools(mcp) {
12825
12404
  inputSchema: {}
12826
12405
  }, async () => {
12827
12406
  try {
12828
- const sessions = await listSessions();
12407
+ const sessions = await agentGuidance.listSessions();
12829
12408
  if (sessions.length === 0) return ok("No sessions found on this machine.");
12830
12409
  return ok(`${sessions.length} session(s):
12831
12410
  ${sessions.map(describeSummary).join("\n")}`);
@@ -12841,9 +12420,9 @@ ${sessions.map(describeSummary).join("\n")}`);
12841
12420
  limit: z.z.number().optional().describe("How many recent messages to fetch (default 20, max 100)")
12842
12421
  }
12843
12422
  }, async (args) => {
12844
- if (!index.isValidSessionId(args.sessionId)) return fail("Invalid sessionId");
12423
+ if (!persistence.isValidSessionId(args.sessionId)) return fail("Invalid sessionId");
12845
12424
  try {
12846
- const result = await readSessionTranscript(args.sessionId, args.limit ?? 20);
12425
+ const result = await agentGuidance.readSessionTranscript(args.sessionId, args.limit ?? 20);
12847
12426
  const body = result.transcript.length > 0 ? result.transcript : "(no readable conversation content in this range)";
12848
12427
  return ok(`${describeSummary(result.summary)}
12849
12428
  --- last ${result.messageCount} message(s) ---
@@ -12861,11 +12440,11 @@ ${body}`);
12861
12440
  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
12441
  }
12863
12442
  }, async (args) => {
12864
- if (!index.isValidSessionId(args.sessionId)) return fail("Invalid sessionId");
12443
+ if (!persistence.isValidSessionId(args.sessionId)) return fail("Invalid sessionId");
12865
12444
  if (typeof args.text !== "string" || args.text.trim().length === 0) return fail("text must be non-empty");
12866
12445
  try {
12867
- const persisted = await waitForSessionKey(args.sessionId, 0);
12868
- const result = await deliverToSession(args.sessionId, persisted, args.text, MCP_CLIENT_TAG, {
12446
+ const persisted = await agentGuidance.waitForSessionKey(args.sessionId, 0);
12447
+ const result = await agentGuidance.deliverToSession(args.sessionId, persisted, args.text, MCP_CLIENT_TAG, {
12869
12448
  resume: args.resume === true,
12870
12449
  resumeHint: "Pass resume:true to bring it back on this machine, or spawn a new session."
12871
12450
  });
@@ -12890,16 +12469,16 @@ ${body}`);
12890
12469
  if (!normalized.ok) return fail(normalized.error);
12891
12470
  const directory = normalized.directory;
12892
12471
  try {
12893
- const result = await index.spawnDaemonSession(directory, void 0, { spawnedBy: "assistant" });
12472
+ const result = await persistence.spawnDaemonSession(directory, void 0, { spawnedBy: "assistant" });
12894
12473
  if (result?.error || !result?.sessionId) {
12895
12474
  return fail(`Failed to spawn session: ${result?.error ?? "daemon returned no session id"}`);
12896
12475
  }
12897
12476
  const sessionId = result.sessionId;
12898
- const url = sessionWebUrl(sessionId);
12477
+ const url = agentGuidance.sessionWebUrl(sessionId);
12899
12478
  if (args.prompt && args.prompt.trim().length > 0) {
12900
12479
  try {
12901
- const persisted = await waitForSessionKey(sessionId, 15e3);
12902
- await sendUserMessage(sessionId, persisted, args.prompt, MCP_CLIENT_TAG);
12480
+ const persisted = await agentGuidance.waitForSessionKey(sessionId, 15e3);
12481
+ await agentGuidance.sendUserMessage(sessionId, persisted, args.prompt, MCP_CLIENT_TAG);
12903
12482
  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
12483
  } catch (error) {
12905
12484
  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 +12496,10 @@ ${body}`);
12917
12496
  sessionId: z.z.string().describe("The Happy session id to stop")
12918
12497
  }
12919
12498
  }, async (args) => {
12920
- if (!index.isValidSessionId(args.sessionId)) return fail("Invalid sessionId");
12499
+ if (!persistence.isValidSessionId(args.sessionId)) return fail("Invalid sessionId");
12921
12500
  try {
12922
12501
  const persisted = persistence.readPersistedSessions()[args.sessionId];
12923
- const success = await stopSession(args.sessionId);
12502
+ const success = await agentGuidance.stopSession(args.sessionId);
12924
12503
  if (!success) return fail(`Session ${args.sessionId} was not found among the daemon's running sessions.`);
12925
12504
  return ok(`Stopped ${describeSession(args.sessionId, persisted)}`);
12926
12505
  } catch (error) {
@@ -12934,9 +12513,9 @@ ${body}`);
12934
12513
  sessionId: z.z.string().describe("The Happy session id to archive")
12935
12514
  }
12936
12515
  }, async (args) => {
12937
- if (!index.isValidSessionId(args.sessionId)) return fail("Invalid sessionId");
12516
+ if (!persistence.isValidSessionId(args.sessionId)) return fail("Invalid sessionId");
12938
12517
  try {
12939
- await archiveSession(args.sessionId);
12518
+ await agentGuidance.archiveSession(args.sessionId);
12940
12519
  const persisted = persistence.readPersistedSessions()[args.sessionId];
12941
12520
  return ok(`Archived ${describeSession(args.sessionId, persisted)}`);
12942
12521
  } catch (error) {
@@ -12970,7 +12549,7 @@ ${lines.join("\n")}`);
12970
12549
  lines: z.z.number().optional().describe("How many history lines to read (default 80, max 2000)")
12971
12550
  }
12972
12551
  }, async (args) => {
12973
- if (!index.isValidTerminalId(args.terminalId)) return fail("Invalid terminalId");
12552
+ if (!persistence.isValidTerminalId(args.terminalId)) return fail("Invalid terminalId");
12974
12553
  const r = index.readVhTerminal(args.terminalId, args.lines ?? 80);
12975
12554
  if (!r.ok) return fail(`Failed to read terminal ${args.terminalId}: ${r.error}`);
12976
12555
  return ok(`terminal ${args.terminalId} \u2014 last output:
@@ -12985,7 +12564,7 @@ ${r.text}`);
12985
12564
  submit: z.z.boolean().optional().describe("Also press Enter after pasting (default false)")
12986
12565
  }
12987
12566
  }, async (args) => {
12988
- if (!index.isValidTerminalId(args.terminalId)) return fail("Invalid terminalId");
12567
+ if (!persistence.isValidTerminalId(args.terminalId)) return fail("Invalid terminalId");
12989
12568
  if (typeof args.text !== "string" || args.text.length === 0) return fail("text must be non-empty");
12990
12569
  const submit = args.submit === true;
12991
12570
  const r = index.sendToVhTerminal(args.terminalId, args.text, submit);
@@ -13060,13 +12639,14 @@ function normalizeProgress(value) {
13060
12639
  return flat.length > PROGRESS_MAX_CHARS$1 ? flat.slice(0, PROGRESS_MAX_CHARS$1) : flat;
13061
12640
  }
13062
12641
 
13063
- function createMcpServer(handlers, options, sessionId) {
12642
+ function createMcpServer(handlers, options, sessionId, client) {
13064
12643
  const mcp = new mcp_js.McpServer({
13065
12644
  name: "Very Happy Tools",
13066
12645
  version: "1.0.0"
13067
12646
  });
13068
12647
  agentGuidance.registerTeamsTools(mcp, sessionId);
13069
12648
  agentGuidance.registerAutomationTools(mcp);
12649
+ if (client) agentGuidance.registerSessionPeerTools(mcp, agentGuidance.createSessionPeerToolExecutor(client));
13070
12650
  mcp.registerTool("change_title", {
13071
12651
  description: "Change the title of the current chat session",
13072
12652
  title: "Change Chat Title",
@@ -13269,7 +12849,7 @@ async function startHappyServer(client, options) {
13269
12849
  }
13270
12850
  return;
13271
12851
  }
13272
- const mcp = createMcpServer(handlers, options, client.sessionId);
12852
+ const mcp = createMcpServer(handlers, options, client.sessionId, client);
13273
12853
  try {
13274
12854
  const transport = new streamableHttp_js.StreamableHTTPServerTransport({
13275
12855
  sessionIdGenerator: void 0
@@ -13310,6 +12890,7 @@ async function startHappyServer(client, options) {
13310
12890
  agentGuidance.REPORT_PROGRESS_TOOL_NAME,
13311
12891
  ...agentGuidance.TEAM_TOOL_NAMES,
13312
12892
  ...agentGuidance.AUTOMATION_TOOL_NAMES,
12893
+ ...agentGuidance.SESSION_PEER_TOOL_NAMES,
13313
12894
  ...options?.assistant && !process.env.VH_TEAM_SCOPE_FILE ? ASSISTANT_TOOL_NAMES : []
13314
12895
  ],
13315
12896
  stop: () => {
@@ -13470,6 +13051,40 @@ function registerKillSessionHandler(rpcHandlerManager, killThisHappy, archiveEve
13470
13051
  };
13471
13052
  }
13472
13053
 
13054
+ async function settleSessionOnExit(sessionId, intent, deps) {
13055
+ if (intent === "archive") {
13056
+ deps.updateMetadata((currentMetadata) => ({
13057
+ ...currentMetadata,
13058
+ lifecycleState: "archived",
13059
+ lifecycleStateSince: Date.now(),
13060
+ archivedBy: "cli",
13061
+ archiveReason: "User terminated"
13062
+ }));
13063
+ }
13064
+ deps.sendSessionDeath();
13065
+ try {
13066
+ if (intent === "archive") await deps.archiveSession(sessionId);
13067
+ else await deps.deactivateSession(sessionId);
13068
+ } catch (error) {
13069
+ deps.log?.(`[START] ${intent === "archive" ? "archiveSession" : "deactivateSession"} during cleanup failed:`, error);
13070
+ }
13071
+ }
13072
+ class SessionExitGate {
13073
+ decided = null;
13074
+ /** Returns the intent to act on, or null when cleanup already started. */
13075
+ claim(intent) {
13076
+ if (this.decided !== null) return null;
13077
+ this.decided = intent;
13078
+ return intent;
13079
+ }
13080
+ get current() {
13081
+ return this.decided;
13082
+ }
13083
+ }
13084
+ function exitIntentFromArchiveFlag(archive) {
13085
+ return archive ?? true ? "archive" : "offline";
13086
+ }
13087
+
13473
13088
  function isUnsupportedSideQuestionError(error) {
13474
13089
  const message = error instanceof Error ? error.message : String(error);
13475
13090
  return /unknown|unsupported|not supported|unrecognized|invalid.*subtype/i.test(message);
@@ -13646,7 +13261,7 @@ function registerSideQuestionHandler(rpc, deps) {
13646
13261
  const retainMs = deps.retainMs ?? SIDE_QUESTION_RETAIN_MS;
13647
13262
  const maxRunMs = deps.maxRunMs ?? SIDE_QUESTION_MAX_RUN_MS;
13648
13263
  const fork = deps.run ?? (async (input) => {
13649
- const { query } = await Promise.resolve().then(function () { return require('./index-okLyUzeD.cjs'); });
13264
+ const { query } = await Promise.resolve().then(function () { return require('./index-CB8QnjvS.cjs'); });
13650
13265
  return runSideQuestion(query, input);
13651
13266
  });
13652
13267
  const run = async (input, live) => {
@@ -13765,7 +13380,7 @@ const MAX_TITLE_CHARS = 60;
13765
13380
  const GENERATION_TIMEOUT_MS = 3e4;
13766
13381
  function resolveClaudeBinary() {
13767
13382
  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)));
13383
+ 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-BbtR-Qto.cjs', document.baseURI).href)));
13769
13384
  const utilsPath = path.resolve(path.join(terminalTitleSuggest.projectPath(), "scripts", "claude_version_utils.cjs"));
13770
13385
  const { getClaudeCliPath } = require$1(utilsPath);
13771
13386
  const path$1 = getClaudeCliPath();
@@ -14333,7 +13948,7 @@ async function runClaude(credentials, options = {}) {
14333
13948
  }
14334
13949
  try {
14335
13950
  persistence.logger.debug(`[START] Reporting session ${response.id} to daemon`);
14336
- const result = await index.notifyDaemonSessionStarted(response.id, metadata, {
13951
+ const result = await persistence.notifyDaemonSessionStarted(response.id, metadata, {
14337
13952
  encryptionKey: persistence.encodeBase64(response.encryptionKey),
14338
13953
  encryptionVariant: response.encryptionVariant,
14339
13954
  seq: response.seq,
@@ -14433,6 +14048,17 @@ async function runClaude(credentials, options = {}) {
14433
14048
  });
14434
14049
  session.on("claude-session-message", (body) => boardAnalyzer.noteClaudeMessage(body));
14435
14050
  session.on("turn-ended", () => boardAnalyzer.onTurnEnd());
14051
+ const editThrottle = new EditReportThrottle();
14052
+ session.on("claude-session-message", (body) => {
14053
+ try {
14054
+ for (const edit of editThrottle.take(extractClaudeEditPaths(body))) {
14055
+ persistence.reportSessionEditToDaemon({ sessionId: response.id, path: edit.path, tool: edit.tool, cwd: workingDirectory, ...edit.at !== void 0 ? { at: edit.at } : {} });
14056
+ }
14057
+ } catch (error) {
14058
+ persistence.logger.debug("[START] edit report failed:", error);
14059
+ }
14060
+ });
14061
+ options = { ...options, claudeEnvVars: { ...options.claudeEnvVars, VH_PEER_SESSION_ID: response.id } };
14436
14062
  const initialScannerSessionId = forkClaudeSessionId ?? (metadata.claudeSessionId ?? null);
14437
14063
  const remoteScanner = await createSessionScanner({
14438
14064
  sessionId: initialScannerSessionId,
@@ -14772,26 +14398,24 @@ async function runClaude(credentials, options = {}) {
14772
14398
  hasMetadata: Boolean(message.meta)
14773
14399
  });
14774
14400
  });
14401
+ const exitGate = new SessionExitGate();
14775
14402
  const cleanup = async (opts = { archive: true }) => {
14776
- persistence.logger.debug(`[START] Received termination signal, cleaning up (archive=${opts.archive ?? true})...`);
14403
+ const intent = exitGate.claim(exitIntentFromArchiveFlag(opts.archive));
14404
+ if (intent === null) {
14405
+ persistence.logger.debug(`[START] Termination (archive=${opts.archive ?? true}) ignored: cleanup already running as ${exitGate.current}`);
14406
+ return;
14407
+ }
14408
+ persistence.logger.debug(`[START] Received termination signal, cleaning up (archive=${intent === "archive"})...`);
14777
14409
  try {
14778
14410
  if (session) {
14779
- if (opts.archive ?? true) {
14780
- session.updateMetadata((currentMetadata) => ({
14781
- ...currentMetadata,
14782
- lifecycleState: "archived",
14783
- lifecycleStateSince: Date.now(),
14784
- archivedBy: "cli",
14785
- archiveReason: "User terminated"
14786
- }));
14787
- }
14788
14411
  currentSession?.cleanup();
14789
- session.sendSessionDeath();
14790
- try {
14791
- await api.deactivateSession(session.sessionId);
14792
- } catch (err) {
14793
- persistence.logger.debug("[START] deactivateSession during cleanup failed:", err);
14794
- }
14412
+ await settleSessionOnExit(session.sessionId, intent, {
14413
+ updateMetadata: (handler) => session.updateMetadata(handler),
14414
+ sendSessionDeath: () => session.sendSessionDeath(),
14415
+ archiveSession: (id) => api.archiveSession(id),
14416
+ deactivateSession: (id) => api.deactivateSession(id),
14417
+ log: (message, ...args) => persistence.logger.debug(message, ...args)
14418
+ });
14795
14419
  await session.flush();
14796
14420
  await session.close();
14797
14421
  }
@@ -15064,7 +14688,7 @@ async function handleAuthLogin(args) {
15064
14688
  console.log(chalk.gray(" \u2022 Re-authenticate and register machine\n"));
15065
14689
  try {
15066
14690
  persistence.logger.debug("Stopping daemon for force auth...");
15067
- await index.stopDaemon();
14691
+ await persistence.stopDaemon();
15068
14692
  console.log(chalk.gray("\u2713 Stopped daemon"));
15069
14693
  } catch (error) {
15070
14694
  persistence.logger.debug("Daemon was not running or failed to stop:", error);
@@ -15129,7 +14753,7 @@ async function handleAuthLogout() {
15129
14753
  if (answer.toLowerCase() === "y" || answer.toLowerCase() === "yes") {
15130
14754
  try {
15131
14755
  try {
15132
- await index.stopDaemon();
14756
+ await persistence.stopDaemon();
15133
14757
  console.log(chalk.gray("Stopped daemon"));
15134
14758
  } catch {
15135
14759
  }
@@ -15174,7 +14798,7 @@ async function handleAuthStatus() {
15174
14798
  console.log(chalk.gray(`
15175
14799
  Data directory: ${persistence.configuration.happyHomeDir}`));
15176
14800
  try {
15177
- const running = await index.checkIfDaemonRunningAndCleanupStaleState();
14801
+ const running = await persistence.checkIfDaemonRunningAndCleanupStaleState();
15178
14802
  if (running) {
15179
14803
  console.log(chalk.green("\u2713 Daemon running"));
15180
14804
  } else {
@@ -16000,9 +15624,9 @@ function resolveLocalSignupBootstrap(configuredMode, configuredInviteCodes, gene
16000
15624
  };
16001
15625
  }
16002
15626
 
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)));
15627
+ 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-BbtR-Qto.cjs', document.baseURI).href)));
16004
15628
  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)));
15629
+ const require_ = node_module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index-BbtR-Qto.cjs', document.baseURI).href)));
16006
15630
  const PRISMA_QUERY_ENGINE_FILES = {
16007
15631
  "arm64-darwin": "libquery_engine-darwin-arm64.dylib.node",
16008
15632
  "x64-darwin": "libquery_engine-darwin.dylib.node",
@@ -16391,7 +16015,7 @@ const DAEMON_READY_TIMEOUT_MS = 5e3;
16391
16015
  const DAEMON_READY_POLL_INTERVAL_MS = 100;
16392
16016
  async function ensureDaemonRunning() {
16393
16017
  persistence.logger.debug("Ensuring Happy background service is running & matches our version...");
16394
- const versionMatches = await index.isDaemonRunningCurrentlyInstalledHappyVersion();
16018
+ const versionMatches = await persistence.isDaemonRunningCurrentlyInstalledHappyVersion();
16395
16019
  const state = await persistence.readDaemonState();
16396
16020
  const endpointsMatch = Boolean(state) && daemonEndpointsMatch(
16397
16021
  state?.serverUrl,
@@ -16402,9 +16026,9 @@ async function ensureDaemonRunning() {
16402
16026
  if (versionMatches && endpointsMatch) {
16403
16027
  return;
16404
16028
  }
16405
- if (await index.checkIfDaemonRunningAndCleanupStaleState()) {
16029
+ if (await persistence.checkIfDaemonRunningAndCleanupStaleState()) {
16406
16030
  persistence.logger.debug("Stopping daemon whose version or configured relay no longer matches...");
16407
- await index.stopDaemon();
16031
+ await persistence.stopDaemon();
16408
16032
  }
16409
16033
  persistence.logger.debug("Starting Happy background service...");
16410
16034
  const daemonProcess = spawnHappyCLI(["daemon", "start-sync"], {
@@ -16415,7 +16039,7 @@ async function ensureDaemonRunning() {
16415
16039
  daemonProcess.unref();
16416
16040
  const deadline = Date.now() + DAEMON_READY_TIMEOUT_MS;
16417
16041
  while (Date.now() < deadline) {
16418
- if (await index.checkIfDaemonRunningAndCleanupStaleState()) {
16042
+ if (await persistence.checkIfDaemonRunningAndCleanupStaleState()) {
16419
16043
  persistence.logger.debug("Happy background service is ready");
16420
16044
  return;
16421
16045
  }
@@ -18122,7 +17746,7 @@ async function runCodex(opts) {
18122
17746
  if (response) {
18123
17747
  try {
18124
17748
  persistence.logger.debug(`[START] Reporting session ${response.id} to daemon`);
18125
- const result = await index.notifyDaemonSessionStarted(response.id, metadata, {
17749
+ const result = await persistence.notifyDaemonSessionStarted(response.id, metadata, {
18126
17750
  encryptionKey: persistence.encodeBase64(response.encryptionKey),
18127
17751
  encryptionVariant: response.encryptionVariant,
18128
17752
  seq: response.seq,
@@ -18419,6 +18043,7 @@ async function runCodex(opts) {
18419
18043
  return "denied";
18420
18044
  }
18421
18045
  });
18046
+ const editThrottle = new EditReportThrottle();
18422
18047
  client.setEventHandler((msg) => {
18423
18048
  if (msg.type === "token_count") {
18424
18049
  session.sendAgentUsageSnapshot("codex", msg);
@@ -18482,6 +18107,9 @@ async function runCodex(opts) {
18482
18107
  }
18483
18108
  if (msg.type === "patch_apply_begin") {
18484
18109
  const { changes } = msg;
18110
+ for (const edit of editThrottle.take(extractCodexPatchPaths(changes))) {
18111
+ persistence.reportSessionEditToDaemon({ sessionId: session.sessionId, path: edit.path, tool: edit.tool, cwd: process.cwd() });
18112
+ }
18485
18113
  const changeCount = Object.keys(changes).length;
18486
18114
  const filesMsg = changeCount === 1 ? "1 file" : `${changeCount} files`;
18487
18115
  messageBuffer.addMessage(`Modifying ${filesMsg}...`, "tool");
@@ -18527,6 +18155,7 @@ async function runCodex(opts) {
18527
18155
  };
18528
18156
  let first = true;
18529
18157
  let appendSystemPromptInjected = false;
18158
+ process.env.VH_PEER_SESSION_ID = session.sessionId;
18530
18159
  try {
18531
18160
  persistence.logger.debug("[codex]: client.connect begin");
18532
18161
  await client.connect();
@@ -18940,7 +18569,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18940
18569
  return;
18941
18570
  } else if (subcommand === "install-terminal-hooks") {
18942
18571
  try {
18943
- const { installTerminalHooks, parseTerminalHooksArgs, TERMINAL_HOOKS_HELP } = await Promise.resolve().then(function () { return require('./installTerminalHooks-CA4OcAS7.cjs'); });
18572
+ const { installTerminalHooks, parseTerminalHooksArgs, TERMINAL_HOOKS_HELP } = await Promise.resolve().then(function () { return require('./installTerminalHooks-BOAnQ1Qe.cjs'); });
18944
18573
  const command = parseTerminalHooksArgs(args.slice(1));
18945
18574
  if (command.action === "help") {
18946
18575
  console.log(TERMINAL_HOOKS_HELP);
@@ -18957,7 +18586,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18957
18586
  }
18958
18587
  } else if (subcommand === "agent-home") {
18959
18588
  try {
18960
- const { parseAgentHomeArgs, runAgentHomeCommand } = await Promise.resolve().then(function () { return require('./agentHome-BUsLTZnz.cjs'); });
18589
+ const { parseAgentHomeArgs, runAgentHomeCommand } = await Promise.resolve().then(function () { return require('./agentHome-CmIJNV7f.cjs'); });
18961
18590
  await runAgentHomeCommand(parseAgentHomeArgs(args.slice(1)));
18962
18591
  process.exit(0);
18963
18592
  } catch (error) {
@@ -18966,7 +18595,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18966
18595
  }
18967
18596
  } else if (subcommand === "spawn") {
18968
18597
  try {
18969
- const { handleSpawnCommand } = await Promise.resolve().then(function () { return require('./spawn-DxrfHTF4.cjs'); });
18598
+ const { handleSpawnCommand } = await Promise.resolve().then(function () { return require('./spawn-CC6V2KiA.cjs'); });
18970
18599
  await handleSpawnCommand(args.slice(1));
18971
18600
  } catch (error) {
18972
18601
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
@@ -18978,7 +18607,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18978
18607
  return;
18979
18608
  } else if (subcommand === "sessions") {
18980
18609
  try {
18981
- const { handleSessionsCommand } = await Promise.resolve().then(function () { return require('./sessions-RIFUoy_g.cjs'); });
18610
+ const { handleSessionsCommand } = await Promise.resolve().then(function () { return require('./sessions-OmvyJb0n.cjs'); });
18982
18611
  await handleSessionsCommand(args.slice(1));
18983
18612
  } catch (error) {
18984
18613
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
@@ -18990,7 +18619,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18990
18619
  return;
18991
18620
  } else if (subcommand === "send") {
18992
18621
  try {
18993
- const { handleSendCommand } = await Promise.resolve().then(function () { return require('./send-Bd00QcHC.cjs'); });
18622
+ const { handleSendCommand } = await Promise.resolve().then(function () { return require('./send-CTcGMVuG.cjs'); });
18994
18623
  await handleSendCommand(args.slice(1));
18995
18624
  } catch (error) {
18996
18625
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
@@ -19096,9 +18725,9 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19096
18725
  if (geminiSubcommand === "project" && args[2] === "set" && args[3]) {
19097
18726
  const projectId = args[3];
19098
18727
  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; });
18728
+ const { saveGoogleCloudProjectToConfig } = await Promise.resolve().then(function () { return require('./config-BYHSRY2E.cjs'); });
18729
+ const { readCredentialsForConfiguredRelay: readCredentialsForConfiguredRelay2 } = await Promise.resolve().then(function () { return require('./limits-o29KXBgj.cjs'); }).then(function (n) { return n.persistence; });
18730
+ const { ApiClient: ApiClient2 } = await Promise.resolve().then(function () { return require('./types-CphWjqHc.cjs'); }).then(function (n) { return n.api; });
19102
18731
  let userEmail = void 0;
19103
18732
  try {
19104
18733
  const credentials = await readCredentialsForConfiguredRelay2();
@@ -19129,7 +18758,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19129
18758
  }
19130
18759
  if (geminiSubcommand === "project" && args[2] === "get") {
19131
18760
  try {
19132
- const { readGeminiLocalConfig } = await Promise.resolve().then(function () { return require('./config-DM_xI9M6.cjs'); });
18761
+ const { readGeminiLocalConfig } = await Promise.resolve().then(function () { return require('./config-BYHSRY2E.cjs'); });
19133
18762
  const config = readGeminiLocalConfig();
19134
18763
  if (config.googleCloudProject) {
19135
18764
  console.log(`Current Google Cloud Project: ${config.googleCloudProject}`);
@@ -19169,7 +18798,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19169
18798
  process.exit(0);
19170
18799
  }
19171
18800
  try {
19172
- const { runGemini } = await Promise.resolve().then(function () { return require('./runGemini-CNPGSsSK.cjs'); });
18801
+ const { runGemini } = await Promise.resolve().then(function () { return require('./runGemini--QqK-Ap5.cjs'); });
19173
18802
  let startedBy = void 0;
19174
18803
  for (let i = 1; i < args.length; i++) {
19175
18804
  if (args[i] === "--started-by") {
@@ -19192,7 +18821,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19192
18821
  } else if (subcommand === "pi") {
19193
18822
  if (args[1] === "--terminal") {
19194
18823
  try {
19195
- const { runPiTerminal } = await Promise.resolve().then(function () { return require('./piTerminal-fUogwxYr.cjs'); });
18824
+ const { runPiTerminal } = await Promise.resolve().then(function () { return require('./piTerminal-D-BIbov9.cjs'); });
19196
18825
  process.exitCode = await runPiTerminal(args.slice(2));
19197
18826
  } catch (error) {
19198
18827
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
@@ -19209,7 +18838,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19209
18838
  }
19210
18839
  };
19211
18840
  try {
19212
- const { runAcp, resolveAcpAgentConfig } = await Promise.resolve().then(function () { return require('./index-BUxBxSxJ.cjs'); });
18841
+ const { runAcp, resolveAcpAgentConfig } = await Promise.resolve().then(function () { return require('./index-B_-7NpGZ.cjs'); });
19213
18842
  const parsed = parsePiRunnerArgs(args.slice(1));
19214
18843
  const resolved = resolveAcpAgentConfig(["pi", ...parsed.passthrough]);
19215
18844
  const { credentials } = await authAndSetupMachineIfNeeded();
@@ -19237,7 +18866,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19237
18866
  return;
19238
18867
  } else if (subcommand === "acp") {
19239
18868
  try {
19240
- const { runAcp, resolveAcpAgentConfig } = await Promise.resolve().then(function () { return require('./index-BUxBxSxJ.cjs'); });
18869
+ const { runAcp, resolveAcpAgentConfig } = await Promise.resolve().then(function () { return require('./index-B_-7NpGZ.cjs'); });
19241
18870
  let startedBy = void 0;
19242
18871
  let verbose = false;
19243
18872
  const acpArgs = [];
@@ -19277,7 +18906,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19277
18906
  return;
19278
18907
  } else if (subcommand === "openclaw") {
19279
18908
  try {
19280
- const { runOpenClaw } = await Promise.resolve().then(function () { return require('./runOpenClaw-Bsodkg_s.cjs'); });
18909
+ const { runOpenClaw } = await Promise.resolve().then(function () { return require('./runOpenClaw-C0xLhTvk.cjs'); });
19281
18910
  let startedBy = void 0;
19282
18911
  let verbose = false;
19283
18912
  let gatewayUrl;
@@ -19328,7 +18957,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19328
18957
  return;
19329
18958
  } else if (subcommand === "todo") {
19330
18959
  try {
19331
- const { handleTodoCommand } = await Promise.resolve().then(function () { return require('./todo-B4cBFzHB.cjs'); });
18960
+ const { handleTodoCommand } = await Promise.resolve().then(function () { return require('./todo-CEDvKNXK.cjs'); });
19332
18961
  await handleTodoCommand(args.slice(1));
19333
18962
  } catch (error) {
19334
18963
  console.error(JSON.stringify({ error: error instanceof Error ? error.message : "Todo command failed" }));
@@ -19337,7 +18966,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19337
18966
  return;
19338
18967
  } else if (subcommand === "teams") {
19339
18968
  try {
19340
- const { handleTeamsCommand } = await Promise.resolve().then(function () { return require('./teams-Cfy0VMCo.cjs'); });
18969
+ const { handleTeamsCommand } = await Promise.resolve().then(function () { return require('./teams-XtrH3_h3.cjs'); });
19341
18970
  await handleTeamsCommand(args.slice(1));
19342
18971
  } catch (error) {
19343
18972
  console.error(error instanceof Error ? error.message : "Teams command failed");
@@ -19346,7 +18975,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19346
18975
  return;
19347
18976
  } else if (subcommand === "auto") {
19348
18977
  try {
19349
- const { handleAutoCommand } = await Promise.resolve().then(function () { return require('./auto-IeldARBS.cjs'); });
18978
+ const { handleAutoCommand } = await Promise.resolve().then(function () { return require('./auto-CMDq4oIu.cjs'); });
19350
18979
  await handleAutoCommand(args.slice(1));
19351
18980
  } catch (error) {
19352
18981
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Automation command failed");
@@ -19358,7 +18987,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19358
18987
  return;
19359
18988
  } else if (subcommand === "mcp" && (args.length === 1 || args.length === 2 && args[1] === "--terminal-tools")) {
19360
18989
  try {
19361
- const { handleMcpCommand } = await Promise.resolve().then(function () { return require('./mcp-7iDHlyCI.cjs'); });
18990
+ const { handleMcpCommand } = await Promise.resolve().then(function () { return require('./mcp-CwK1nI7Y.cjs'); });
19362
18991
  await handleMcpCommand(args[1] === "--terminal-tools");
19363
18992
  } catch (error) {
19364
18993
  process.stderr.write(`[very-happy mcp] Fatal: ${error instanceof Error ? error.message : String(error)}
@@ -19381,12 +19010,14 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19381
19010
  const daemonSubcommand = args[1];
19382
19011
  if (daemonSubcommand === "list") {
19383
19012
  try {
19384
- const sessions = await index.listDaemonSessions();
19013
+ const sessions = await persistence.listDaemonSessions();
19385
19014
  if (sessions.length === 0) {
19386
19015
  console.log("No active sessions this daemon is aware of (they might have been started by a previous version of the daemon)");
19387
19016
  } else {
19388
19017
  console.log("Active sessions:");
19389
19018
  console.log(JSON.stringify(sessions, null, 2));
19019
+ const busy = sessions.filter((s) => s.turnActive === true);
19020
+ console.log(busy.length === 0 ? "No turn in flight on this machine (wrappers older than 0.2.139 do not report turns)." : `${busy.length} session(s) with a turn in flight: ${busy.map((s) => s.happySessionId).join(", ")}`);
19390
19021
  }
19391
19022
  } catch (error) {
19392
19023
  console.log("No daemon running");
@@ -19399,7 +19030,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19399
19030
  process.exit(1);
19400
19031
  }
19401
19032
  try {
19402
- const success = await index.stopDaemonSession(sessionId);
19033
+ const success = await persistence.stopDaemonSession(sessionId);
19403
19034
  console.log(success ? "Session stopped" : "Failed to stop session");
19404
19035
  } catch (error) {
19405
19036
  console.log("No daemon running");
@@ -19407,7 +19038,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19407
19038
  return;
19408
19039
  } else if (daemonSubcommand === "start") {
19409
19040
  const currentState = await persistence.readDaemonState();
19410
- const running = await index.checkIfDaemonRunningAndCleanupStaleState();
19041
+ const running = await persistence.checkIfDaemonRunningAndCleanupStaleState();
19411
19042
  const currentDaemonMatches = currentState && currentState.startedWithCliVersion === persistence.configuration.currentCliVersion && daemonEndpointsMatch(
19412
19043
  currentState.serverUrl,
19413
19044
  currentState.webappUrl,
@@ -19415,18 +19046,24 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19415
19046
  persistence.configuration.webappUrl
19416
19047
  );
19417
19048
  if (running && !currentDaemonMatches) {
19049
+ if (currentState?.supervisor === "systemd") {
19050
+ console.error(`The running daemon is supervised by systemd; hand over through the unit instead:
19051
+ ${SYSTEMD_RESTART_HINT}`);
19052
+ process.exit(1);
19053
+ }
19418
19054
  console.log("Configured relay or CLI version changed; restarting the existing daemon");
19419
- await index.stopDaemon();
19055
+ await persistence.stopDaemon();
19420
19056
  }
19057
+ const { HAPPY_DAEMON_SUPERVISOR: _supervisor, INVOCATION_ID: _invocation, ...detachedEnv } = process.env;
19421
19058
  const child = spawnHappyCLI(["daemon", "start-sync"], {
19422
19059
  detached: true,
19423
19060
  stdio: "ignore",
19424
- env: process.env
19061
+ env: detachedEnv
19425
19062
  });
19426
19063
  child.unref();
19427
19064
  let started = false;
19428
19065
  for (let i = 0; i < 50; i++) {
19429
- if (await index.checkIfDaemonRunningAndCleanupStaleState()) {
19066
+ if (await persistence.checkIfDaemonRunningAndCleanupStaleState()) {
19430
19067
  started = true;
19431
19068
  break;
19432
19069
  }
@@ -19443,7 +19080,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
19443
19080
  await startDaemon();
19444
19081
  process.exit(0);
19445
19082
  } else if (daemonSubcommand === "stop") {
19446
- await index.stopDaemon();
19083
+ await persistence.stopDaemon();
19447
19084
  process.exit(0);
19448
19085
  } else if (daemonSubcommand === "status") {
19449
19086
  await runDoctorDaemon();
@@ -19734,11 +19371,10 @@ exports.BaseReasoningProcessor = BaseReasoningProcessor;
19734
19371
  exports.CHANGE_TITLE_INSTRUCTION = CHANGE_TITLE_INSTRUCTION;
19735
19372
  exports.CLAUDE_ATTACHMENT_KINDS = CLAUDE_ATTACHMENT_KINDS;
19736
19373
  exports.DEFAULT_GEMINI_MODEL = DEFAULT_GEMINI_MODEL;
19737
- exports.DEFAULT_RECENT_LIMIT = DEFAULT_RECENT_LIMIT;
19374
+ exports.EditReportThrottle = EditReportThrottle;
19738
19375
  exports.GEMINI_API_KEY_ENV = GEMINI_API_KEY_ENV;
19739
19376
  exports.GEMINI_MODEL_ENV = GEMINI_MODEL_ENV;
19740
19377
  exports.GOOGLE_API_KEY_ENV = GOOGLE_API_KEY_ENV;
19741
- exports.MAX_READ_LIMIT = MAX_READ_LIMIT;
19742
19378
  exports.MessageBuffer = MessageBuffer;
19743
19379
  exports.MessageQueue2 = MessageQueue2;
19744
19380
  exports.PI_TEAMS_EXTENSION = PI_TEAMS_EXTENSION;
@@ -19746,41 +19382,29 @@ exports.SPAWN_AGENTS = SPAWN_AGENTS;
19746
19382
  exports.StreamRelay = StreamRelay;
19747
19383
  exports.TEAM_SKILL = TEAM_SKILL;
19748
19384
  exports.TitleGenerator = TitleGenerator;
19749
- exports.TurnWaitTimeoutError = TurnWaitTimeoutError;
19750
19385
  exports.appendStagedAttachmentsToPrompt = appendStagedAttachmentsToPrompt;
19751
- exports.archiveSession = archiveSession;
19752
19386
  exports.checkPreviewPath = checkPreviewPath;
19753
19387
  exports.createSessionMetadata = createSessionMetadata;
19754
- exports.deliverToSession = deliverToSession;
19755
19388
  exports.describeAction = describeAction;
19756
19389
  exports.describeTrigger = describeTrigger;
19390
+ exports.extractAcpEditPaths = extractAcpEditPaths;
19757
19391
  exports.hashObject = hashObject;
19758
19392
  exports.initialMachineMetadata = initialMachineMetadata;
19759
19393
  exports.isValidSpawnOrigin = isValidSpawnOrigin;
19760
- exports.listAccountSessions = listAccountSessions;
19761
- exports.listSessions = listSessions;
19762
19394
  exports.normalizeAcpPermissionMode = normalizeAcpPermissionMode;
19763
19395
  exports.parseDuration = parseDuration;
19764
19396
  exports.query = query;
19765
- exports.readSessionMetadata = readSessionMetadata;
19766
- exports.readSessionTranscript = readSessionTranscript;
19767
19397
  exports.registerAgentAttachmentDownloads = registerAgentAttachmentDownloads;
19768
19398
  exports.registerAssistantSessionTools = registerAssistantSessionTools;
19769
19399
  exports.registerKillSessionHandler = registerKillSessionHandler;
19770
19400
  exports.removeSessionModeFile = removeSessionModeFile;
19771
19401
  exports.resolveFirstMessageMeta = resolveFirstMessageMeta;
19772
- exports.resolvePermissionRequest = resolvePermissionRequest;
19773
19402
  exports.resolveSpawnPermissionMode = resolveSpawnPermissionMode;
19774
19403
  exports.sanitizeSpawnPermissionMode = sanitizeSpawnPermissionMode;
19775
- exports.sendUserMessage = sendUserMessage;
19776
- exports.sessionWebUrl = sessionWebUrl;
19777
19404
  exports.setupOfflineReconnection = setupOfflineReconnection;
19778
19405
  exports.stageClaudeAttachments = stageClaudeAttachments;
19779
19406
  exports.startHappyServer = startHappyServer;
19780
- exports.stopSession = stopSession;
19781
19407
  exports.suggestTitleForPrompt = suggestTitleForPrompt;
19782
19408
  exports.tmuxSupportsSessionEnv = tmuxSupportsSessionEnv;
19783
19409
  exports.triggerFromFlags = triggerFromFlags;
19784
- exports.waitForSessionKey = waitForSessionKey;
19785
- exports.waitForTurnEnd = waitForTurnEnd;
19786
19410
  exports.writeSessionModeFile = writeSessionModeFile;