very-happy-cli 0.2.150 → 0.2.152

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-C-dhGJgd.mjs → AcpBackend-Bs3wwYbt.mjs} +2 -2
  2. package/dist/{AcpBackend-CXawaWWx.cjs → AcpBackend-CogiKCng.cjs} +2 -2
  3. package/dist/{AcpSessionManager-DKOtSqJc.mjs → AcpSessionManager-BT8PJMEm.mjs} +1 -1
  4. package/dist/{AcpSessionManager-TrwEZR7y.cjs → AcpSessionManager-BazkBuFo.cjs} +9 -9
  5. package/dist/agentGuidance-CqBn77AI.cjs +553 -0
  6. package/dist/agentGuidance-Daxn5Nvj.mjs +529 -0
  7. package/dist/{agentHome-Dm-Wjs38.mjs → agentHome-CCYfs8Ec.mjs} +3 -3
  8. package/dist/{agentHome-rylNIkix.cjs → agentHome-CpJjrQZc.cjs} +3 -3
  9. package/dist/auto-DV5emt1-.mjs +466 -0
  10. package/dist/auto-DncmTDvw.cjs +472 -0
  11. package/dist/codex/happyMcpStdioBridge.cjs +4 -2
  12. package/dist/codex/happyMcpStdioBridge.mjs +4 -2
  13. package/dist/{config-Dhu6LU0h.mjs → config-DbKpQpdL.mjs} +5 -5
  14. package/dist/{config-CgPu2wtS.cjs → config-DeJgSMh-.cjs} +5 -5
  15. package/dist/{index-CpNYvMTJ.mjs → index-BauTv2jB.mjs} +900 -72
  16. package/dist/{index-BsYH1sjV.mjs → index-CN7OF2cU.mjs} +5 -5
  17. package/dist/{index-DvfCUuDK.cjs → index-CRaJ9k2p.cjs} +5 -5
  18. package/dist/{index-CzpooENj.mjs → index-DahaLbu4.mjs} +19 -19
  19. package/dist/{index-K6mUS0jF.cjs → index-ZIW1jrYm.cjs} +941 -104
  20. package/dist/{index-DEVkDRas.cjs → index-imt4mC41.cjs} +19 -19
  21. package/dist/index.cjs +4 -4
  22. package/dist/index.mjs +4 -4
  23. package/dist/install-C1K3nrxF.mjs +114 -0
  24. package/dist/install-CDR3qBhs.cjs +118 -0
  25. package/dist/{installTerminalHooks-NIbsQ1wi.cjs → installTerminalHooks-BoDxpaBO.cjs} +5 -5
  26. package/dist/{installTerminalHooks-BF_tmw9V.mjs → installTerminalHooks-VvmlO6Ue.mjs} +5 -5
  27. package/dist/lib.cjs +3 -3
  28. package/dist/lib.mjs +3 -3
  29. package/dist/limits-BNTWyUaZ.cjs +2175 -0
  30. package/dist/limits-CKR_HJnO.mjs +2080 -0
  31. package/dist/{mcp-_TVUSg8W.cjs → mcp-CCKCKL6l.cjs} +5 -5
  32. package/dist/{mcp-DzydgXpf.mjs → mcp-D4dAo5BQ.mjs} +5 -5
  33. package/dist/{piRuntime-CSAiJk0X.cjs → piRuntime-Bn5ypbjH.cjs} +2 -2
  34. package/dist/{piRuntime-ACOhil6t.mjs → piRuntime-CW-XA4rz.mjs} +2 -2
  35. package/dist/{piTerminal-DHxGB3Ic.cjs → piTerminal-B7K75aXB.cjs} +6 -6
  36. package/dist/{piTerminal-BlE4vbvZ.mjs → piTerminal-BLSXK1u2.mjs} +6 -6
  37. package/dist/{runGemini-BMYCU16c.cjs → runGemini-Bgg1BzDK.cjs} +7 -7
  38. package/dist/{runGemini-D5IV1MoF.mjs → runGemini-D5N11LxO.mjs} +7 -7
  39. package/dist/{runOpenClaw-Qs4eFtcS.cjs → runOpenClaw-B9qWHybc.cjs} +6 -6
  40. package/dist/{runOpenClaw-CWPXrKMJ.mjs → runOpenClaw-CCpyKbrn.mjs} +6 -6
  41. package/dist/{send-Dv0km8UI.mjs → send-C44kcXz5.mjs} +21 -7
  42. package/dist/{send-qXWGilm8.cjs → send-wl9ei8yG.cjs} +21 -7
  43. package/dist/{sessions-DoULfKui.mjs → sessions-DXJEcUg6.mjs} +57 -11
  44. package/dist/{sessions-CpIdDhH7.cjs → sessions-Tad8elmy.cjs} +57 -11
  45. package/dist/{spawn-DQuOmUP7.mjs → spawn-BOlb5y1_.mjs} +142 -28
  46. package/dist/{spawn-DqzG1bTH.cjs → spawn-DA8WAFmm.cjs} +142 -28
  47. package/dist/{teams-4mhNh5cj.mjs → teams-Bs8lI887.mjs} +15 -72
  48. package/dist/{teams-ByEgUzsH.cjs → teams-qVLrFcdd.cjs} +13 -70
  49. package/dist/{todo-D7SDXL0c.cjs → todo-mCMoGjJk.cjs} +4 -23
  50. package/dist/{todo-C9IAUf0K.mjs → todo-nzsqGZqp.mjs} +2 -21
  51. package/dist/{types-D2Gkpe0N.cjs → types-CN28qJl7.cjs} +38 -813
  52. package/dist/{types-BD542hb1.mjs → types-xKrxbCRv.mjs} +208 -978
  53. package/package.json +7 -7
  54. package/dist/agentGuidance-Dla-BXO8.cjs +0 -267
  55. package/dist/agentGuidance-DrfhtVP1.mjs +0 -251
  56. package/dist/limits-Dyma1REp.cjs +0 -1236
  57. package/dist/limits-FThmm7WR.mjs +0 -1165
@@ -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-D2Gkpe0N.cjs');
7
- var persistence = require('./limits-Dyma1REp.cjs');
6
+ var index = require('./types-CN28qJl7.cjs');
7
+ var persistence = require('./limits-BNTWyUaZ.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 projectPath = require('./projectPath-ChLhdOyt.cjs');
13
- var agentGuidance = require('./agentGuidance-Dla-BXO8.cjs');
13
+ var agentGuidance = require('./agentGuidance-CqBn77AI.cjs');
14
14
  var promises = require('node:fs/promises');
15
15
  var fs = require('fs/promises');
16
16
  var ink = require('ink');
@@ -478,17 +478,23 @@ function readClaudeSettings() {
478
478
  }
479
479
  function shouldIncludeCoAuthoredBy() {
480
480
  const settings = readClaudeSettings();
481
- if (!settings || settings.includeCoAuthoredBy === void 0) {
481
+ if (!settings) {
482
+ return true;
483
+ }
484
+ if (settings.attribution?.commit === "") {
485
+ return false;
486
+ }
487
+ if (settings.includeCoAuthoredBy === void 0) {
482
488
  return true;
483
489
  }
484
490
  return settings.includeCoAuthoredBy;
485
491
  }
486
492
 
487
493
  const BASE_SYSTEM_PROMPT = (() => agentGuidance.trimIdent(`
488
- ALWAYS when you start a new chat - you must call a tool "mcp__happy__change_title" to set a chat title. When you think chat title is not relevant anymore - call the tool again to change it. When chat name is too generic and you have a change to make it more specific - call the tool again to change it. This title is needed to easily find the chat in the future. Help human.
489
- `))() + "\n\n" + agentGuidance.AGENT_GUIDANCE + "\n\n" + index.BUILTIN_TODO_DISCOVERY;
494
+ This chat is titled automatically from the user's first message. Call "mcp__happy__change_title" only when the topic has clearly moved on or the current title is too generic to find this chat later. Do not call it as a ritual at the start of a chat, and not for a quick question you can just answer.
495
+ `))() + "\n\n" + agentGuidance.AGENT_GUIDANCE + "\n\n" + persistence.BUILTIN_TODO_DISCOVERY;
490
496
  const CO_AUTHORED_CREDITS = (() => agentGuidance.trimIdent(`
491
- When making commit messages, instead of just giving co-credit to Claude, also give credit to Happy like so:
497
+ When you yourself create a git commit, instead of just giving co-credit to Claude, also give credit to Happy like so:
492
498
 
493
499
  <main commit message>
494
500
 
@@ -496,6 +502,8 @@ const CO_AUTHORED_CREDITS = (() => agentGuidance.trimIdent(`
496
502
  via [Very Happy](https://github.com/Mereithhh/very-happy)
497
503
 
498
504
  Co-Authored-By: Claude <noreply@anthropic.com>
505
+
506
+ Do not add these lines to commit message text you only draft for the user.
499
507
  `))();
500
508
  const systemPrompt = (() => {
501
509
  const includeCoAuthored = shouldIncludeCoAuthoredBy();
@@ -892,7 +900,7 @@ async function createSessionScanner(opts) {
892
900
  let processedMessageKeys = /* @__PURE__ */ new Set();
893
901
  let deadSessions = /* @__PURE__ */ new Set();
894
902
  if (opts.sessionId) {
895
- let messages = await readSessionLog(projectDir, opts.sessionId);
903
+ let messages = await readSessionLog$1(projectDir, opts.sessionId);
896
904
  persistence.logger.debug(`[SESSION_SCANNER] Marking ${messages.length} existing messages as processed from session ${opts.sessionId}`);
897
905
  for (let m of messages) {
898
906
  processedMessageKeys.add(messageKey(m));
@@ -915,7 +923,7 @@ async function createSessionScanner(opts) {
915
923
  }
916
924
  }
917
925
  for (let session of sessions) {
918
- const sessionMessages = await readSessionLog(projectDir, session);
926
+ const sessionMessages = await readSessionLog$1(projectDir, session);
919
927
  let skipped = 0;
920
928
  let sent = 0;
921
929
  for (let file of sessionMessages) {
@@ -992,7 +1000,7 @@ async function createSessionScanner(opts) {
992
1000
  return;
993
1001
  }
994
1002
  if (options?.treatExistingAsProcessed) {
995
- const existing = await readSessionLog(projectDir, sessionId);
1003
+ const existing = await readSessionLog$1(projectDir, sessionId);
996
1004
  persistence.logger.debug(`[SESSION_SCANNER] Pre-marking ${existing.length} existing messages as processed for new session ${sessionId}`);
997
1005
  for (const m of existing) {
998
1006
  processedMessageKeys.add(messageKey(m));
@@ -1020,7 +1028,7 @@ function messageKey(message) {
1020
1028
  throw Error();
1021
1029
  }
1022
1030
  }
1023
- async function readSessionLog(projectDir, sessionId) {
1031
+ async function readSessionLog$1(projectDir, sessionId) {
1024
1032
  const expectedSessionFile = path.join(projectDir, `${sessionId}.jsonl`);
1025
1033
  persistence.logger.debug(`[SESSION_SCANNER] Reading session file: ${expectedSessionFile}`);
1026
1034
  let file;
@@ -3499,7 +3507,7 @@ class StreamRelay {
3499
3507
  }
3500
3508
  this.pending.text += text;
3501
3509
  this.pending.bytes += Buffer.byteLength(text, "utf8");
3502
- if (this.pending.bytes >= STREAM_FLUSH_MAX_BYTES || this.pending.text.length >= index.STREAM_DELTA_MAX_CHARS) {
3510
+ if (this.pending.bytes >= STREAM_FLUSH_MAX_BYTES || this.pending.text.length >= persistence.STREAM_DELTA_MAX_CHARS) {
3503
3511
  this.flushPending();
3504
3512
  return;
3505
3513
  }
@@ -5394,8 +5402,8 @@ ${typeLabels[type] || type}:`));
5394
5402
  }
5395
5403
  console.log(chalk.gray(" An external claude command is only needed for native terminal/mirror use."));
5396
5404
  try {
5397
- const { resolveAgentHomesForCli } = await Promise.resolve().then(function () { return require('./agentHome-rylNIkix.cjs'); });
5398
- const { describeAgentHome } = await Promise.resolve().then(function () { return require('./types-D2Gkpe0N.cjs'); }).then(function (n) { return n.index; });
5405
+ const { resolveAgentHomesForCli } = await Promise.resolve().then(function () { return require('./agentHome-CpJjrQZc.cjs'); });
5406
+ const { describeAgentHome } = await Promise.resolve().then(function () { return require('./types-CN28qJl7.cjs'); }).then(function (n) { return n.index; });
5399
5407
  const homes = await resolveAgentHomesForCli();
5400
5408
  console.log(chalk.green(`\u2713 Claude config dir: ${describeAgentHome(homes.claudeConfigDir)}`));
5401
5409
  console.log(chalk.green(`\u2713 Codex home: ${describeAgentHome(homes.codexHome)}`));
@@ -5506,7 +5514,7 @@ async function registryVersion(name) {
5506
5514
  return typeof data.version === "string" ? stableVersion(data.version) : null;
5507
5515
  }
5508
5516
  async function detectAgentVersions(deps = { probe: probeAgentVersion, registryVersion }, now = Date.now()) {
5509
- const agents = await Promise.all(index.AGENT_VERSION_SOURCES.map(async (source) => {
5517
+ const agents = await Promise.all(persistence.AGENT_VERSION_SOURCES.map(async (source) => {
5510
5518
  const local = source.command ? await deps.probe(source.command).catch(() => ({ version: null, missing: false })) : { version: stableVersion(persistence.packageJson.dependencies["@anthropic-ai/claude-agent-sdk"]), missing: false };
5511
5519
  if (local.missing) return { id: source.id, installed: null, latest: null, status: "not-installed" };
5512
5520
  const latest = await deps.registryVersion(source.package).catch(() => null);
@@ -6106,7 +6114,9 @@ async function sendUserMessage(sessionId, persisted, text, client, options = {})
6106
6114
  },
6107
6115
  meta: {
6108
6116
  sentFrom: options.sentFrom ?? "cli",
6109
- ...options.model ? { model: options.model } : {}
6117
+ ...options.model !== void 0 ? { model: options.model } : {},
6118
+ ...options.permissionMode !== void 0 ? { permissionMode: options.permissionMode } : {},
6119
+ ...options.effort !== void 0 ? { effort: options.effort } : {}
6110
6120
  }
6111
6121
  };
6112
6122
  const encrypted = persistence.encodeBase64(persistence.encrypt(
@@ -6220,7 +6230,7 @@ const DENIED_BASENAME_PATTERNS = [
6220
6230
  const DENIED_PATH_SUFFIXES = [
6221
6231
  `${path.sep}.git${path.sep}config`
6222
6232
  ];
6223
- function expandHome(input, home) {
6233
+ function expandHome$1(input, home) {
6224
6234
  if (input === "~") return home;
6225
6235
  if (input.startsWith(`~${path.sep}`) || input.startsWith("~/")) {
6226
6236
  return path.resolve(home, input.slice(2));
@@ -6243,7 +6253,7 @@ function checkPreviewPath(input, opts) {
6243
6253
  if (input.includes("\0")) {
6244
6254
  return { resolved: "", deniedReason: "path contains a NUL byte" };
6245
6255
  }
6246
- const expanded = expandHome(input.trim(), home);
6256
+ const expanded = expandHome$1(input.trim(), home);
6247
6257
  const resolved = path.isAbsolute(expanded) ? path.resolve(expanded) : path.resolve(cwd, expanded);
6248
6258
  for (const dir of DENIED_HOME_DIRS) {
6249
6259
  if (isUnder(resolved, path.resolve(home, dir))) {
@@ -6417,13 +6427,22 @@ function startDaemonControlServer({
6417
6427
  permissionMode: z.z.string().optional(),
6418
6428
  // B-069: spawn-origin tag ('assistant' = session_spawn). Recorded on
6419
6429
  // the TrackedSession; old clients never send it.
6420
- spawnedBy: z.z.string().optional()
6430
+ spawnedBy: z.z.string().optional(),
6431
+ // B-492: `very-happy spawn --fork` — same fields the web's fork flow
6432
+ // sends over the machine RPC. Old CLIs never send them.
6433
+ resumeClaudeSessionId: z.z.string().regex(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i).optional(),
6434
+ resumeCodexThreadId: z.z.string().min(1).max(200).optional(),
6435
+ parentSessionId: z.z.string().min(1).max(200).optional()
6421
6436
  }),
6422
6437
  response: {
6423
6438
  200: z.z.object({
6424
6439
  success: z.z.boolean(),
6425
6440
  sessionId: z.z.string().optional(),
6426
- approvedNewDirectoryCreation: z.z.boolean().optional()
6441
+ approvedNewDirectoryCreation: z.z.boolean().optional(),
6442
+ // B-492: true when resumeClaudeSessionId / resumeCodexThreadId was
6443
+ // honoured. A caller that asked for a fork and does not get this
6444
+ // back is talking to an older daemon that spawned a fresh session.
6445
+ resumed: z.z.boolean().optional()
6427
6446
  }),
6428
6447
  409: z.z.object({
6429
6448
  success: z.z.boolean(),
@@ -6438,13 +6457,14 @@ function startDaemonControlServer({
6438
6457
  }
6439
6458
  }
6440
6459
  }, async (request, reply) => {
6441
- const { directory, sessionId, agent, environmentVariables, variant, forceNew, permissionMode, spawnedBy } = request.body;
6460
+ const { directory, sessionId, agent, environmentVariables, variant, forceNew, permissionMode, spawnedBy, resumeClaudeSessionId, resumeCodexThreadId, parentSessionId } = request.body;
6442
6461
  if (!directory && variant !== "assistant") {
6443
6462
  reply.code(500);
6444
6463
  return { success: false, error: "directory is required" };
6445
6464
  }
6446
6465
  persistence.logger.debug(`[CONTROL SERVER] Spawn session request: dir=${directory}, sessionId=${sessionId || "new"}, agent=${agent || "default"}, variant=${variant || "none"}, forceNew=${forceNew === true}, spawnedBy=${spawnedBy || "unset"}`);
6447
- const result = await spawnSession({ directory, sessionId, agent, environmentVariables, variant, forceNew, permissionMode, spawnedBy });
6466
+ const result = await spawnSession({ directory, sessionId, agent, environmentVariables, variant, forceNew, permissionMode, spawnedBy, resumeClaudeSessionId, resumeCodexThreadId, parentSessionId });
6467
+ const resumed = Boolean(resumeClaudeSessionId || resumeCodexThreadId);
6448
6468
  switch (result.type) {
6449
6469
  case "success":
6450
6470
  if (!result.sessionId) {
@@ -6457,7 +6477,8 @@ function startDaemonControlServer({
6457
6477
  return {
6458
6478
  success: true,
6459
6479
  sessionId: result.sessionId,
6460
- approvedNewDirectoryCreation: true
6480
+ approvedNewDirectoryCreation: true,
6481
+ ...resumed ? { resumed: true } : {}
6461
6482
  };
6462
6483
  case "requestToApproveDirectoryCreation":
6463
6484
  reply.code(409);
@@ -8123,7 +8144,7 @@ function parseJsonObject(text) {
8123
8144
  return null;
8124
8145
  }
8125
8146
  }
8126
- 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-K6mUS0jF.cjs', document.baseURI).href))).resolve(s)) {
8147
+ function resolveSdkClaudeBinary(platform = process.platform, arch = process.arch, resolver = (s) => node_module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index-ZIW1jrYm.cjs', document.baseURI).href))).resolve(s)) {
8127
8148
  const platformArch = `${platform}-${arch}`;
8128
8149
  const candidates = [
8129
8150
  `@anthropic-ai/claude-agent-sdk-${platformArch}/claude`,
@@ -9389,6 +9410,8 @@ ${goal}`;
9389
9410
 
9390
9411
  const MAX_LINE_CHARS = 500;
9391
9412
  const MAX_ARGS_CHARS = 160;
9413
+ const DEFAULT_LIMITS = { line: MAX_LINE_CHARS, args: MAX_ARGS_CHARS };
9414
+ const NO_LIMITS = { line: Number.POSITIVE_INFINITY, args: Number.POSITIVE_INFINITY };
9392
9415
  function truncateText(text, max = MAX_LINE_CHARS) {
9393
9416
  const chars = Array.from(text);
9394
9417
  if (chars.length <= max) return text;
@@ -9404,12 +9427,12 @@ function safeJson(value, max) {
9404
9427
  return "[unserializable]";
9405
9428
  }
9406
9429
  }
9407
- function formatMessageBody(body) {
9430
+ function formatMessageBody(body, limits = DEFAULT_LIMITS) {
9408
9431
  if (!body || typeof body !== "object") return null;
9409
9432
  const b = body;
9410
9433
  if (b.role === "user" && b.content?.type === "text" && typeof b.content.text === "string") {
9411
9434
  const text = squash(b.content.text);
9412
- return text.length > 0 ? `user: ${truncateText(text)}` : null;
9435
+ return text.length > 0 ? `user: ${truncateText(text, limits.line)}` : null;
9413
9436
  }
9414
9437
  if (b.role === "session" && b.content && typeof b.content === "object") {
9415
9438
  const env = b.content;
@@ -9421,18 +9444,18 @@ function formatMessageBody(body) {
9421
9444
  const text = squash(String(ev.text ?? ""));
9422
9445
  if (text.length === 0) return null;
9423
9446
  const role = env.role === "user" ? "user" : "assistant";
9424
- return `${role}: ${truncateText(text)}`;
9447
+ return `${role}: ${truncateText(text, limits.line)}`;
9425
9448
  }
9426
9449
  case "tool-call-start": {
9427
9450
  const name = String(ev.name ?? "tool");
9428
9451
  const title = squash(String(ev.title ?? ""));
9429
- const args = ev.args ? safeJson(ev.args, MAX_ARGS_CHARS) : "";
9452
+ const args = ev.args ? safeJson(ev.args, limits.args) : "";
9430
9453
  const detail = title || args;
9431
- return `[tool] ${name}${detail ? `: ${truncateText(detail, MAX_ARGS_CHARS)}` : ""}`;
9454
+ return `[tool] ${name}${detail ? `: ${truncateText(detail, limits.args)}` : ""}`;
9432
9455
  }
9433
9456
  case "service": {
9434
9457
  const text = squash(String(ev.text ?? ""));
9435
- return text.length > 0 ? `[service] ${truncateText(text, MAX_ARGS_CHARS)}` : null;
9458
+ return text.length > 0 ? `[service] ${truncateText(text, limits.args)}` : null;
9436
9459
  }
9437
9460
  case "turn-end": {
9438
9461
  if (ev.status && ev.status !== "completed") return `[turn ${ev.status}]`;
@@ -9447,12 +9470,12 @@ function formatMessageBody(body) {
9447
9470
  switch (d.type) {
9448
9471
  case "message": {
9449
9472
  const text = squash(String(d.message ?? ""));
9450
- return text.length > 0 ? `assistant: ${truncateText(text)}` : null;
9473
+ return text.length > 0 ? `assistant: ${truncateText(text, limits.line)}` : null;
9451
9474
  }
9452
9475
  case "tool-call":
9453
- return `[tool] ${String(d.name ?? "tool")}: ${safeJson(d.input, MAX_ARGS_CHARS)}`;
9476
+ return `[tool] ${String(d.name ?? "tool")}: ${safeJson(d.input, limits.args)}`;
9454
9477
  case "permission-request":
9455
- return `[permission] ${String(d.toolName ?? "")}: ${truncateText(squash(String(d.description ?? "")), MAX_ARGS_CHARS)}`;
9478
+ return `[permission] ${String(d.toolName ?? "")}: ${truncateText(squash(String(d.description ?? "")), limits.args)}`;
9456
9479
  default:
9457
9480
  return null;
9458
9481
  }
@@ -9463,22 +9486,118 @@ function formatMessageBody(body) {
9463
9486
  const texts = msg.content.filter((c) => c?.type === "text" && typeof c.text === "string").map((c) => squash(c.text)).filter((t) => t.length > 0);
9464
9487
  if (texts.length > 0) {
9465
9488
  const role = msg.role === "user" ? "user" : "assistant";
9466
- return `${role}: ${truncateText(texts.join(" "))}`;
9489
+ return `${role}: ${truncateText(texts.join(" "), limits.line)}`;
9467
9490
  }
9468
9491
  }
9469
9492
  return null;
9470
9493
  }
9471
9494
  return null;
9472
9495
  }
9473
- function formatTranscript(bodies) {
9496
+ function formatTranscript(bodies, options = {}) {
9497
+ const limits = options.full ? NO_LIMITS : DEFAULT_LIMITS;
9474
9498
  const lines = [];
9475
9499
  for (const body of bodies) {
9476
- const line = formatMessageBody(body);
9500
+ const line = formatMessageBody(body, limits);
9477
9501
  if (line !== null) lines.push(line);
9478
9502
  }
9479
9503
  return lines.join("\n");
9480
9504
  }
9481
9505
 
9506
+ function asRecord(value) {
9507
+ return value && typeof value === "object" ? value : null;
9508
+ }
9509
+ function sessionEnvelope(body) {
9510
+ const b = asRecord(body);
9511
+ if (!b || b.role !== "session") return null;
9512
+ const env = asRecord(b.content);
9513
+ if (!env) return null;
9514
+ const ev = asRecord(env.ev);
9515
+ return ev && typeof ev.t === "string" ? { role: env.role, ev } : null;
9516
+ }
9517
+ function isUserPrompt(body) {
9518
+ const b = asRecord(body);
9519
+ return !!b && b.role === "user" && b.content?.type === "text" && typeof b.content.text === "string";
9520
+ }
9521
+ function agentPiece(body) {
9522
+ const env = sessionEnvelope(body);
9523
+ if (env && env.role !== "user") {
9524
+ if (env.ev.t === "text" && !env.ev.thinking) {
9525
+ const text = String(env.ev.text ?? "");
9526
+ return text.trim().length > 0 ? { kind: "text", text } : null;
9527
+ }
9528
+ if (env.ev.t === "tool-call-start") return { kind: "tool" };
9529
+ return null;
9530
+ }
9531
+ const b = asRecord(body);
9532
+ if (b?.role === "agent" && b.content?.type === "acp") {
9533
+ const d = asRecord(b.content.data);
9534
+ if (d?.type === "message") {
9535
+ const text = String(d.message ?? "");
9536
+ return text.trim().length > 0 ? { kind: "text", text } : null;
9537
+ }
9538
+ if (d?.type === "tool-call") return { kind: "tool" };
9539
+ }
9540
+ return null;
9541
+ }
9542
+ function userPromptText(body) {
9543
+ return isUserPrompt(body) ? body.content.text : null;
9544
+ }
9545
+ function analyzeLatestTurn(entries, options = {}) {
9546
+ const lastSeq = entries.length > 0 ? entries[entries.length - 1].seq : 0;
9547
+ let userIndex = -1;
9548
+ for (let i = entries.length - 1; i >= 0; i--) {
9549
+ if (isUserPrompt(entries[i].body) && (!options.anchor || options.anchor(entries[i].body))) {
9550
+ userIndex = i;
9551
+ break;
9552
+ }
9553
+ }
9554
+ let ended = false;
9555
+ let status = null;
9556
+ let error = null;
9557
+ let texts = [];
9558
+ let lastMarker = null;
9559
+ let startedAfterPrompt = false;
9560
+ const emitsStarts = entries.some((entry) => sessionEnvelope(entry.body)?.ev?.t === "turn-start");
9561
+ for (let i = userIndex + 1; i < entries.length; i++) {
9562
+ const body = entries[i].body;
9563
+ if (options.anchor && isUserPrompt(body)) {
9564
+ if (ended) break;
9565
+ continue;
9566
+ }
9567
+ const env = sessionEnvelope(body);
9568
+ if (env?.ev?.t === "turn-start") {
9569
+ lastMarker = "start";
9570
+ startedAfterPrompt = true;
9571
+ ended = false;
9572
+ status = null;
9573
+ error = null;
9574
+ texts = [];
9575
+ continue;
9576
+ }
9577
+ if (env?.ev?.t === "turn-end") {
9578
+ lastMarker = "end";
9579
+ if (userIndex >= 0 && emitsStarts && !startedAfterPrompt) continue;
9580
+ ended = true;
9581
+ status = typeof env.ev.status === "string" ? env.ev.status : null;
9582
+ error = typeof env.ev.error === "string" ? env.ev.error : null;
9583
+ continue;
9584
+ }
9585
+ const piece = agentPiece(body);
9586
+ if (!piece) continue;
9587
+ if (piece.kind === "tool") texts = [];
9588
+ else texts.push(piece.text);
9589
+ }
9590
+ if (userIndex < 0) ended = lastMarker === "end";
9591
+ return {
9592
+ userSeq: userIndex >= 0 ? entries[userIndex].seq : null,
9593
+ ended,
9594
+ status: ended ? status : null,
9595
+ error: ended ? error : null,
9596
+ answer: texts.join("\n\n").trim(),
9597
+ lastSeq
9598
+ };
9599
+ }
9600
+
9482
9601
  const USER_SOCKET_CONNECT_TIMEOUT_MS = 15e3;
9483
9602
  async function openUserScopedSocket(token, options = {}) {
9484
9603
  const socket = socket_ioClient.io(persistence.configuration.serverUrl, {
@@ -9824,19 +9943,21 @@ async function bearerToken() {
9824
9943
  if (!credentials) throw new Error("CLI is not authenticated (no ~/.happy/access.key)");
9825
9944
  return credentials.token;
9826
9945
  }
9827
- async function readSessionTranscript(sessionId, limit) {
9946
+ function requirePersisted(sessionId) {
9828
9947
  const persisted = persistence.readPersistedSessions()[sessionId];
9829
9948
  if (!persisted) {
9830
9949
  throw new Error(
9831
9950
  `No local key for session ${sessionId} \u2014 it was not spawned by this machine's daemon (or is older than 14 days).`
9832
9951
  );
9833
9952
  }
9834
- const bounded = Math.max(1, Math.min(MAX_READ_LIMIT, Math.floor(limit)));
9953
+ return persisted;
9954
+ }
9955
+ async function readSessionLog(sessionId, persisted, limit) {
9835
9956
  const token = await bearerToken();
9836
9957
  const response = await axios.get(
9837
9958
  `${persistence.configuration.serverUrl}/v3/sessions/${encodeURIComponent(sessionId)}/messages`,
9838
9959
  {
9839
- params: { before_seq: 2147483647, limit: bounded },
9960
+ params: { before_seq: 2147483647, limit },
9840
9961
  headers: {
9841
9962
  "Authorization": `Bearer ${token}`,
9842
9963
  "X-Happy-Client": `${SESSION_OPS_CLIENT}/${persistence.configuration.currentCliVersion}`
@@ -9847,21 +9968,62 @@ async function readSessionTranscript(sessionId, limit) {
9847
9968
  const messages = Array.isArray(response.data?.messages) ? response.data.messages : [];
9848
9969
  messages.reverse();
9849
9970
  const key = persistence.decodeBase64(persisted.encryptionKey);
9850
- const live = await index.listDaemonSessions();
9851
- const bodies = messages.map((message) => {
9852
- if (message.content?.t !== "encrypted") return null;
9971
+ return messages.map((message) => {
9972
+ if (message.content?.t !== "encrypted") return { seq: message.seq, body: null };
9853
9973
  try {
9854
- return persistence.decrypt(key, persisted.encryptionVariant, persistence.decodeBase64(message.content.c));
9974
+ return { seq: message.seq, body: persistence.decrypt(key, persisted.encryptionVariant, persistence.decodeBase64(message.content.c)) };
9855
9975
  } catch {
9856
- return null;
9976
+ return { seq: message.seq, body: null };
9857
9977
  }
9858
9978
  });
9979
+ }
9980
+ const TURN_WINDOW = 500;
9981
+ async function readSessionTranscript(sessionId, limit, options = {}) {
9982
+ const persisted = requirePersisted(sessionId);
9983
+ const bounded = Math.max(1, Math.min(MAX_READ_LIMIT, Math.floor(limit)));
9984
+ const entries = await readSessionLog(sessionId, persisted, Math.max(bounded, TURN_WINDOW));
9985
+ const shown = entries.slice(-bounded);
9986
+ const live = await index.listDaemonSessions();
9859
9987
  return {
9860
9988
  summary: toSummary(sessionId, persisted, sessionLiveness(live, sessionId)),
9861
- messageCount: messages.length,
9862
- transcript: formatTranscript(bodies)
9989
+ messageCount: shown.length,
9990
+ transcript: formatTranscript(shown.map((entry) => entry.body), { full: options.full }),
9991
+ turn: analyzeLatestTurn(entries)
9863
9992
  };
9864
9993
  }
9994
+ async function readSessionMetadata(sessionId, persisted) {
9995
+ const token = await bearerToken();
9996
+ const response = await axios.get(`${persistence.configuration.serverUrl}/v1/sessions/${encodeURIComponent(sessionId)}`, {
9997
+ headers: {
9998
+ "Authorization": `Bearer ${token}`,
9999
+ "X-Happy-Client": `${SESSION_OPS_CLIENT}/${persistence.configuration.currentCliVersion}`
10000
+ },
10001
+ timeout: 15e3
10002
+ });
10003
+ const ciphertext = response.data?.session?.metadata;
10004
+ if (typeof ciphertext !== "string" || ciphertext.length === 0) return null;
10005
+ return persistence.decrypt(persistence.decodeBase64(persisted.encryptionKey), persisted.encryptionVariant, persistence.decodeBase64(ciphertext));
10006
+ }
10007
+ class TurnWaitTimeoutError extends Error {
10008
+ constructor(sessionId, timeoutMs, turn) {
10009
+ super(`Session ${sessionId}: the latest turn did not end within ${Math.round(timeoutMs / 1e3)}s`);
10010
+ this.sessionId = sessionId;
10011
+ this.timeoutMs = timeoutMs;
10012
+ this.turn = turn;
10013
+ }
10014
+ }
10015
+ async function waitForTurnEnd(sessionId, options) {
10016
+ const persisted = requirePersisted(sessionId);
10017
+ const pollMs = options.pollMs ?? 3e3;
10018
+ const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
10019
+ const deadline = Date.now() + options.timeoutMs;
10020
+ while (true) {
10021
+ const turn = analyzeLatestTurn(await readSessionLog(sessionId, persisted, TURN_WINDOW));
10022
+ if (turn.ended) return turn;
10023
+ if (Date.now() + pollMs > deadline) throw new TurnWaitTimeoutError(sessionId, options.timeoutMs, turn);
10024
+ await sleep(pollMs);
10025
+ }
10026
+ }
9865
10027
  async function stopSession(sessionId) {
9866
10028
  return await index.stopDaemonSession(sessionId);
9867
10029
  }
@@ -9894,7 +10056,7 @@ function readReceipt(home, id) {
9894
10056
  throw error;
9895
10057
  }
9896
10058
  }
9897
- function writeReceipt(home, receipt) {
10059
+ function writeReceipt$1(home, receipt) {
9898
10060
  persistence.ensurePrivateDirectorySync(path.join(home, "teams", "effects"));
9899
10061
  const target = receiptPath(home, receipt.operationId);
9900
10062
  const tmp = `${target}.${process.pid}.tmp`;
@@ -9971,7 +10133,7 @@ function createTeamWorker(deps) {
9971
10133
  const action = async (teamId, requestId, value) => (await http.post(`/v1/teams/${encodeURIComponent(teamId)}/actions`, { requestId, action: value })).data;
9972
10134
  async function complete(op, receipt) {
9973
10135
  await action(op.teamId, `complete-${op.id}`, { type: "complete-operation", operationId: op.id, machineId: deps.machineId, claimId: receipt.claimId, ...receipt.sessionId ? { sessionId: receipt.sessionId } : {}, ...op.type === "spawn" && receipt.directory ? { workingDirectory: receipt.directory } : {} });
9974
- writeReceipt(home, { ...receipt, phase: "completed" });
10136
+ writeReceipt$1(home, { ...receipt, phase: "completed" });
9975
10137
  }
9976
10138
  async function execute(op) {
9977
10139
  let receipt = readReceipt(home, op.id);
@@ -9988,7 +10150,7 @@ function createTeamWorker(deps) {
9988
10150
  op = claimed.operation;
9989
10151
  credential = claimed.credential;
9990
10152
  receipt = { operationId: op.id, teamId: op.teamId, claimId: op.claimId, phase: "prepared" };
9991
- writeReceipt(home, receipt);
10153
+ writeReceipt$1(home, receipt);
9992
10154
  }
9993
10155
  if (receipt.phase === "completed") return;
9994
10156
  try {
@@ -10001,7 +10163,7 @@ function createTeamWorker(deps) {
10001
10163
  if (!task) throw new Error("Task state unavailable; refusing to start");
10002
10164
  if (task.currentAttemptId !== op.attemptId || ["done", "cancelled"].includes(task.status)) {
10003
10165
  await action(op.teamId, `cancelled-before-spawn-${op.id}`, { type: "fail-operation", operationId: op.id, machineId: deps.machineId, claimId: receipt.claimId, error: "task_closed_before_spawn" });
10004
- writeReceipt(home, { ...receipt, phase: "completed" });
10166
+ writeReceipt$1(home, { ...receipt, phase: "completed" });
10005
10167
  return;
10006
10168
  }
10007
10169
  if (!op.directory) throw new Error("No repository selected for worker");
@@ -10011,17 +10173,17 @@ function createTeamWorker(deps) {
10011
10173
  const scopeFile = path.join(scopes, `${op.id}.json`);
10012
10174
  persistence.writePrivateFileSync(scopeFile, JSON.stringify({ serverUrl: server, scopeToken: credential.token, teamId: op.teamId, botId: op.botId, taskId: op.taskId, attemptId: op.attemptId }));
10013
10175
  receipt = { ...receipt, ...resource, scopeFile, phase: "spawning" };
10014
- writeReceipt(home, receipt);
10176
+ writeReceipt$1(home, receipt);
10015
10177
  const spawned = await deps.spawn({ directory: resource.directory, agent: op.assistant === "pi-acp" ? "pi" : op.assistant, permissionMode: op.permissionMode === "bypassPermissions" ? "bypassPermissions" : "default", spawnedBy: "teams", environmentVariables: { VH_TEAM_SCOPE_FILE: scopeFile, VH_TEAM_OPERATION_ID: op.id } });
10016
10178
  if (spawned.type !== "success") throw new Error(spawned.type === "error" ? spawned.errorMessage : "Directory approval required");
10017
10179
  receipt = { ...receipt, sessionId: spawned.sessionId, phase: "spawned" };
10018
- writeReceipt(home, receipt);
10180
+ writeReceipt$1(home, receipt);
10019
10181
  }
10020
10182
  if (receipt.phase === "spawning") {
10021
10183
  const matches = Object.entries(persistence.readPersistedSessions()).filter(([, entry]) => entry.metadata?.teamOperationId === op.id);
10022
10184
  if (matches.length !== 1) throw new Error("Spawn outcome unknown; reconcile the worker before retrying");
10023
10185
  receipt = { ...receipt, sessionId: matches[0][0], phase: "spawned" };
10024
- writeReceipt(home, receipt);
10186
+ writeReceipt$1(home, receipt);
10025
10187
  }
10026
10188
  if (receipt.phase === "spawned") {
10027
10189
  const current = (await http.get(`/v1/teams/${encodeURIComponent(op.teamId)}`)).data.team;
@@ -10036,14 +10198,14 @@ function createTeamWorker(deps) {
10036
10198
  const key = await waitForSessionKey(receipt.sessionId, 1e4);
10037
10199
  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 } : {} });
10038
10200
  receipt = { ...receipt, phase: "delivered" };
10039
- writeReceipt(home, receipt);
10201
+ writeReceipt$1(home, receipt);
10040
10202
  }
10041
10203
  await complete(op, receipt);
10042
10204
  } else {
10043
10205
  const team = (await http.get(`/v1/teams/${encodeURIComponent(op.teamId)}`)).data.team;
10044
10206
  const latest = team.operations.find((candidate) => candidate.id === op.id);
10045
10207
  if (latest?.status === "completed") {
10046
- writeReceipt(home, { ...receipt, phase: "completed" });
10208
+ writeReceipt$1(home, { ...receipt, phase: "completed" });
10047
10209
  return;
10048
10210
  }
10049
10211
  const bot = team.bots.find((candidate) => candidate.id === op.botId);
@@ -10173,6 +10335,643 @@ Use the teams tools to inspect current task state before acting. This message is
10173
10335
  } };
10174
10336
  }
10175
10337
 
10338
+ function resolveSpawnPermissionMode(agent, explicit) {
10339
+ return explicit ?? persistence.getAgentCodeDefaults(agent ?? "claude").permissionMode;
10340
+ }
10341
+ function outboundPermissionMode(agent, mode) {
10342
+ const key = persistence.normalizeAgentKey(agent ?? "claude");
10343
+ if ((key === "claude" || key === "pi") && mode === "yolo") return "bypassPermissions";
10344
+ return mode;
10345
+ }
10346
+ function resolveFirstMessageMeta(input) {
10347
+ const meta = {
10348
+ permissionMode: outboundPermissionMode(input.agent, input.permissionMode)
10349
+ };
10350
+ if (input.explicitModel !== void 0) {
10351
+ meta.model = input.explicitModel === "default" ? null : input.explicitModel;
10352
+ }
10353
+ if (persistence.normalizeAgentKey(input.agent ?? "claude") !== "claude") return meta;
10354
+ if (meta.model === void 0) {
10355
+ const model = persistence.getAgentCodeDefaults("claude").modelMode;
10356
+ const runnable = !persistence.isClaudeOpus55Model(model) || persistence.supportsClaudeOpus55({ capabilities: input.capabilities ?? null });
10357
+ meta.model = runnable && model !== "default" ? model : null;
10358
+ }
10359
+ meta.effort = null;
10360
+ return meta;
10361
+ }
10362
+
10363
+ function payloadJson(payload) {
10364
+ if (!payload) return void 0;
10365
+ try {
10366
+ return JSON.parse(payload);
10367
+ } catch {
10368
+ return void 0;
10369
+ }
10370
+ }
10371
+ function lookupPath(root, path) {
10372
+ let current = root;
10373
+ for (const segment of path) {
10374
+ if (current === null || typeof current !== "object") return void 0;
10375
+ current = current[segment];
10376
+ }
10377
+ return current;
10378
+ }
10379
+ function scalar(value) {
10380
+ if (value === void 0 || value === null) return "";
10381
+ if (typeof value === "string") return value;
10382
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
10383
+ try {
10384
+ return JSON.stringify(value);
10385
+ } catch {
10386
+ return "";
10387
+ }
10388
+ }
10389
+ function formatNow(now, tz) {
10390
+ if (!tz) return new Date(now).toISOString();
10391
+ try {
10392
+ const parts = new Intl.DateTimeFormat("en-CA", { timeZone: tz, hourCycle: "h23", year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit", timeZoneName: "longOffset" }).formatToParts(new Date(now));
10393
+ const get = (type) => parts.find((p) => p.type === type)?.value ?? "";
10394
+ const offset = get("timeZoneName").replace(/^GMT$/, "+00:00").replace(/^GMT/, "");
10395
+ return `${get("year")}-${get("month")}-${get("day")}T${get("hour")}:${get("minute")}:${get("second")}${offset || "Z"}`;
10396
+ } catch {
10397
+ return new Date(now).toISOString();
10398
+ }
10399
+ }
10400
+ function renderTemplate(text, context) {
10401
+ const json = payloadJson(context.payload);
10402
+ return text.replace(/\{\{\s*([a-zA-Z_][a-zA-Z0-9_.-]*)\s*\}\}/g, (whole, name) => {
10403
+ if (name === "payload") return context.payload ?? "";
10404
+ if (name.startsWith("payload.")) return scalar(lookupPath(json, name.slice("payload.".length).split(".")));
10405
+ if (name === "run.id") return context.runId;
10406
+ if (name === "automation.name") return context.automationName;
10407
+ if (name === "now") return formatNow(context.now ?? Date.now(), context.tz);
10408
+ return whole;
10409
+ });
10410
+ }
10411
+ function renderAction(action, context) {
10412
+ const r = (value) => renderTemplate(value, context);
10413
+ if (action.kind === "spawn") return { ...action, prompt: r(action.prompt) };
10414
+ if (action.kind === "send") return { ...action, prompt: r(action.prompt) };
10415
+ return { ...action, command: action.command.map(r), ...action.env ? { env: Object.fromEntries(Object.entries(action.env).map(([k, v]) => [k, r(v)])) } : {} };
10416
+ }
10417
+ function clipText(text, maxChars, keep = "head") {
10418
+ if (text.length <= maxChars) return text;
10419
+ return keep === "head" ? text.slice(0, maxChars) : text.slice(text.length - maxChars);
10420
+ }
10421
+ const DURATION_UNITS = { ms: 1, s: 1e3, m: 6e4, h: 36e5, d: 864e5 };
10422
+ function parseDuration(raw) {
10423
+ const text = raw.trim();
10424
+ if (/^\d+$/.test(text)) return Number(text);
10425
+ const re = /(\d+(?:\.\d+)?)(ms|s|m|h|d)/gy;
10426
+ let total = 0;
10427
+ let matched = 0;
10428
+ let last = 0;
10429
+ let m;
10430
+ while ((m = re.exec(text)) !== null) {
10431
+ total += Number(m[1]) * DURATION_UNITS[m[2]];
10432
+ matched++;
10433
+ last = re.lastIndex;
10434
+ }
10435
+ if (matched === 0 || last !== text.length) throw new Error(`Invalid duration: ${raw} (use e.g. 90s, 5m, 1h30m, 2d)`);
10436
+ return Math.round(total);
10437
+ }
10438
+ function formatDuration(ms) {
10439
+ if (ms % 864e5 === 0) return `${ms / 864e5}d`;
10440
+ if (ms % 36e5 === 0) return `${ms / 36e5}h`;
10441
+ if (ms % 6e4 === 0) return `${ms / 6e4}m`;
10442
+ if (ms % 1e3 === 0) return `${ms / 1e3}s`;
10443
+ return `${ms}ms`;
10444
+ }
10445
+ function triggerFromFlags(flags, now = Date.now()) {
10446
+ const given = [flags.cron !== void 0, flags.every !== void 0, flags.at !== void 0, flags.manual === true].filter(Boolean).length;
10447
+ if (given === 0) {
10448
+ if (flags.tz !== void 0) throw new Error("--tz only applies with --cron");
10449
+ return void 0;
10450
+ }
10451
+ if (given > 1) throw new Error("Use exactly one of --cron, --every, --at, --manual");
10452
+ if (flags.cron !== void 0) {
10453
+ if (!flags.tz) throw new Error("--cron requires --tz <IANA zone> (e.g. Asia/Singapore)");
10454
+ return { kind: "cron", expr: flags.cron, tz: flags.tz };
10455
+ }
10456
+ if (flags.tz !== void 0) throw new Error("--tz only applies with --cron");
10457
+ if (flags.every !== void 0) return { kind: "interval", everyMs: parseDuration(flags.every), anchorAt: now };
10458
+ if (flags.at !== void 0) {
10459
+ const at = Date.parse(flags.at);
10460
+ if (!Number.isFinite(at)) throw new Error(`--at must be an ISO 8601 timestamp, got: ${flags.at}`);
10461
+ return { kind: "once", at };
10462
+ }
10463
+ return { kind: "manual" };
10464
+ }
10465
+ function describeTrigger(trigger) {
10466
+ switch (trigger.kind) {
10467
+ case "cron":
10468
+ return `cron ${trigger.expr} (${trigger.tz})`;
10469
+ case "interval":
10470
+ return `every ${formatDuration(trigger.everyMs)}`;
10471
+ case "once":
10472
+ return `once at ${new Date(trigger.at).toISOString()}`;
10473
+ default:
10474
+ return "manual";
10475
+ }
10476
+ }
10477
+ function describeAction(action) {
10478
+ if (action.kind === "spawn") return `spawn ${action.agent} in ${action.directory}${action.sticky ? ` (sticky ${action.sticky.key})` : ""}`;
10479
+ if (action.kind === "send") return `send to session ${action.sessionId}`;
10480
+ return `script ${action.command.join(" ")}`;
10481
+ }
10482
+
10483
+ const RECEIPT_RETENTION_MS = 7 * 864e5;
10484
+ const RECEIPT_MAX_ENTRIES = 2e3;
10485
+ function receiptsPath(home) {
10486
+ return path.join(home, "automation-receipts.json");
10487
+ }
10488
+ function readReceipts(home) {
10489
+ let raw;
10490
+ try {
10491
+ raw = node_fs.readFileSync(receiptsPath(home), "utf8");
10492
+ } catch (error) {
10493
+ if (error.code === "ENOENT") return {};
10494
+ throw error;
10495
+ }
10496
+ const parsed = JSON.parse(raw);
10497
+ if (!parsed || typeof parsed !== "object" || typeof parsed.receipts !== "object") throw new Error("Invalid automation receipts file");
10498
+ return parsed.receipts;
10499
+ }
10500
+ function writeReceipt(home, receipt, now = Date.now()) {
10501
+ if (!/^[a-zA-Z0-9_-]{1,128}$/.test(receipt.runId)) throw new Error("Invalid automation run id");
10502
+ const all = readReceipts(home);
10503
+ const next = { ...receipt, updatedAt: now };
10504
+ all[receipt.runId] = next;
10505
+ const entries = Object.values(all).sort((a, b) => b.updatedAt - a.updatedAt);
10506
+ const kept = {};
10507
+ let finished = 0;
10508
+ for (const entry of entries) {
10509
+ if (entry.phase === "finished") {
10510
+ if (now - entry.updatedAt > RECEIPT_RETENTION_MS || finished >= RECEIPT_MAX_ENTRIES) continue;
10511
+ finished++;
10512
+ }
10513
+ kept[entry.runId] = entry;
10514
+ }
10515
+ const target = receiptsPath(home);
10516
+ persistence.ensurePrivateDirectorySync(path.dirname(target));
10517
+ const tmp = `${target}.${process.pid}.tmp`;
10518
+ persistence.writePrivateFileSync(tmp, JSON.stringify({ version: 1, receipts: kept }));
10519
+ const file = node_fs.openSync(tmp, "r");
10520
+ try {
10521
+ node_fs.fsyncSync(file);
10522
+ } finally {
10523
+ node_fs.closeSync(file);
10524
+ }
10525
+ node_fs.renameSync(tmp, target);
10526
+ if (process.platform !== "win32") {
10527
+ const directory = node_fs.openSync(path.dirname(target), "r");
10528
+ try {
10529
+ node_fs.fsyncSync(directory);
10530
+ } finally {
10531
+ node_fs.closeSync(directory);
10532
+ }
10533
+ }
10534
+ return next;
10535
+ }
10536
+
10537
+ const SCRIPT_KILL_GRACE_MS = 1e4;
10538
+ const SCRIPT_DRAIN_MS = 2e3;
10539
+ class TailBuffer {
10540
+ constructor(maxChars) {
10541
+ this.maxChars = maxChars;
10542
+ }
10543
+ text = "";
10544
+ push(chunk) {
10545
+ this.text += chunk;
10546
+ if (this.text.length > this.maxChars * 2) this.text = this.text.slice(this.text.length - this.maxChars);
10547
+ }
10548
+ value() {
10549
+ return this.text.length > this.maxChars ? this.text.slice(this.text.length - this.maxChars) : this.text;
10550
+ }
10551
+ }
10552
+ function killScriptTree(child, signal) {
10553
+ if (child.exitCode !== null || child.signalCode !== null || !child.pid) return;
10554
+ try {
10555
+ if (process.platform !== "win32") process.kill(-child.pid, signal);
10556
+ else child.kill(signal);
10557
+ } catch {
10558
+ try {
10559
+ child.kill(signal);
10560
+ } catch {
10561
+ }
10562
+ }
10563
+ }
10564
+ function terminateScriptTree(child) {
10565
+ killScriptTree(child, "SIGTERM");
10566
+ const killer = setTimeout(() => killScriptTree(child, "SIGKILL"), SCRIPT_KILL_GRACE_MS);
10567
+ killer.unref();
10568
+ child.once("exit", () => clearTimeout(killer));
10569
+ }
10570
+ function runScript(options) {
10571
+ return new Promise((resolve) => {
10572
+ const tail = new TailBuffer(persistence.AUTOMATION_SUMMARY_MAX_CHARS);
10573
+ let timedOut = false;
10574
+ let settled = false;
10575
+ let child;
10576
+ const timers = [];
10577
+ const finish = (result) => {
10578
+ if (settled) return;
10579
+ settled = true;
10580
+ for (const t of timers) clearTimeout(t);
10581
+ resolve(result);
10582
+ };
10583
+ const timer = setTimeout(() => {
10584
+ timedOut = true;
10585
+ terminateScriptTree(child);
10586
+ }, options.timeoutMs);
10587
+ timer.unref();
10588
+ timers.push(timer);
10589
+ try {
10590
+ child = node_child_process.spawn(options.command[0], options.command.slice(1), {
10591
+ cwd: options.cwd,
10592
+ shell: false,
10593
+ windowsHide: true,
10594
+ stdio: ["ignore", "pipe", "pipe"],
10595
+ detached: process.platform !== "win32",
10596
+ env: { ...options.baseEnv ?? process.env, ...options.env ?? {} }
10597
+ });
10598
+ } catch (error) {
10599
+ finish({ exitCode: null, signal: null, timedOut: false, output: "", error: error instanceof Error ? error.message : String(error) });
10600
+ return;
10601
+ }
10602
+ options.onStart?.(child);
10603
+ child.stdout?.setEncoding("utf8").on("data", (chunk) => tail.push(chunk));
10604
+ child.stderr?.setEncoding("utf8").on("data", (chunk) => tail.push(chunk));
10605
+ child.on("error", (error) => finish({ exitCode: null, signal: null, timedOut, output: tail.value(), error: error.message }));
10606
+ child.on("close", (code, signal) => finish({ exitCode: code, signal, timedOut, output: tail.value() }));
10607
+ child.on("exit", (code, signal) => {
10608
+ const drain = setTimeout(() => {
10609
+ killScriptTree(child, "SIGKILL");
10610
+ finish({ exitCode: code, signal, timedOut, output: tail.value() });
10611
+ }, SCRIPT_DRAIN_MS);
10612
+ drain.unref();
10613
+ timers.push(drain);
10614
+ });
10615
+ });
10616
+ }
10617
+
10618
+ const AUTOMATION_TICK_MS = 5e3;
10619
+ const AUTOMATION_LEASE_RENEW_MS = 3e4;
10620
+ const AUTOMATION_LOG_READ_MS = 3e4;
10621
+ const AUTOMATION_UNAVAILABLE_LOG_MS = 10 * 6e4;
10622
+ const AUTOMATION_MAX_CONCURRENT = persistence.AUTOMATION_CLAIM_MAX_RUNS;
10623
+ const AUTOMATION_SPAWNED_BY = "automation";
10624
+ function automationPromptHeader(name, runId) {
10625
+ return `[Very Happy automation "${name}" run ${runId}; finish with automation_report (runId ${runId}) or \`very-happy auto report --run ${runId}\`]`;
10626
+ }
10627
+ function automationRunAnchor(runId) {
10628
+ const marker = ` run ${runId};`;
10629
+ return (body) => userPromptText(body)?.startsWith("[Very Happy automation ") === true && userPromptText(body).includes(marker);
10630
+ }
10631
+ async function defaultReadTurn(sessionId, runId) {
10632
+ const persisted = persistence.readPersistedSessions()[sessionId];
10633
+ if (!persisted) throw new Error(`No local key for session ${sessionId}`);
10634
+ return analyzeLatestTurn(await readSessionLog(sessionId, persisted, TURN_WINDOW), { anchor: automationRunAnchor(runId) });
10635
+ }
10636
+ async function defaultSend(sessionId, text, meta) {
10637
+ const persisted = await waitForSessionKey(sessionId, 15e3);
10638
+ await sendUserMessage(sessionId, persisted, text, "automation", { ...meta, sentFrom: "automation" });
10639
+ }
10640
+ function expandHome(path$1) {
10641
+ if (path$1 === "~") return os.homedir();
10642
+ if (path$1.startsWith("~/")) return path.join(os.homedir(), path$1.slice(2));
10643
+ return path$1;
10644
+ }
10645
+ function isSettledByServer(error) {
10646
+ if (!(error instanceof agentGuidance.AutomationsRequestError)) return false;
10647
+ if (error.status === 409 && (error.code === "run_finished" || error.code === "claim_mismatch")) return true;
10648
+ return error.status === 404 && error.code === "run_not_found";
10649
+ }
10650
+ function scriptAlive(item) {
10651
+ return !!item.child && item.child.exitCode === null && item.child.signalCode === null;
10652
+ }
10653
+ function createAutomationRunner(deps) {
10654
+ const home = deps.home ?? persistence.configuration.happyHomeDir;
10655
+ const client = deps.client ?? agentGuidance.createAutomationsClient({ serverUrl: deps.serverUrl ?? persistence.configuration.serverUrl, token: deps.token, client: "daemon-auto" });
10656
+ const runScript$1 = deps.runScript ?? runScript;
10657
+ const readTurn = deps.readTurn ?? defaultReadTurn;
10658
+ const send = deps.send ?? defaultSend;
10659
+ const prepareWorktree = deps.prepareWorktree ?? prepareTeamWorktree;
10660
+ const now = deps.now ?? (() => Date.now());
10661
+ const tracked = /* @__PURE__ */ new Map();
10662
+ let running = false;
10663
+ let renewing = false;
10664
+ let closed = false;
10665
+ let disabledUntil = 0;
10666
+ let lastUnavailableLogAt = -Infinity;
10667
+ let recovered = false;
10668
+ function persist(receipt) {
10669
+ writeReceipt(home, receipt, now());
10670
+ }
10671
+ async function finish(item, report2) {
10672
+ try {
10673
+ await client.report(item.runId, report2);
10674
+ } catch (error) {
10675
+ if (!isSettledByServer(error)) {
10676
+ item.pendingFinal = report2;
10677
+ deps.log(`Automation run ${item.runId} final report deferred`);
10678
+ return;
10679
+ }
10680
+ }
10681
+ persist({ runId: item.runId, automationId: item.automationId, kind: item.kind, phase: "finished", claimId: item.claimId, sessionId: item.sessionId });
10682
+ tracked.delete(item.runId);
10683
+ }
10684
+ function track(claimed, kind, sessionId, child) {
10685
+ const at = now();
10686
+ const item = { runId: claimed.run.id, automationId: claimed.automation.id, claimId: claimed.run.claimId, kind, sessionId, child, sawTurn: false, startedAt: at, lastLeaseAt: at, nextLogReadAt: at + AUTOMATION_LOG_READ_MS, attentionReported: false, exited: false };
10687
+ tracked.set(item.runId, item);
10688
+ return item;
10689
+ }
10690
+ async function failNow(claimed, kind, error, attentionReason, sessionId) {
10691
+ const item = track(claimed, kind, sessionId);
10692
+ await finish(item, { claimId: item.claimId, status: "failed", error: clipText(error, persistence.AUTOMATION_SUMMARY_MAX_CHARS), ...sessionId ? { sessionId } : {}, ...attentionReason ? { needsAttention: true, attentionReason } : {} });
10693
+ }
10694
+ async function reportRunning(item, sessionId, stickyKey) {
10695
+ const body = { claimId: item.claimId, status: "running", ...sessionId ? { sessionId } : {}, ...stickyKey ? { stickyKey } : {} };
10696
+ try {
10697
+ await client.report(item.runId, body);
10698
+ } catch (error) {
10699
+ if (isSettledByServer(error)) {
10700
+ settleExternally(item);
10701
+ return;
10702
+ }
10703
+ if (sessionId) item.pendingRunning = { sessionId, ...stickyKey ? { stickyKey } : {} };
10704
+ deps.log(`Automation run ${item.runId} running report deferred`);
10705
+ }
10706
+ }
10707
+ async function execute(claimed) {
10708
+ const { run, automation } = claimed;
10709
+ const existing = readReceipts(home)[run.id];
10710
+ if (existing) {
10711
+ if (existing.phase === "finished" || tracked.has(run.id)) return;
10712
+ if (existing.phase === "starting") {
10713
+ await failNow(claimed, existing.kind, "Local execution outcome unknown after restart; verify before re-running", "unknown_outcome");
10714
+ return;
10715
+ }
10716
+ if (existing.kind === "script") {
10717
+ await failNow(claimed, "script", "Daemon restarted while the script was running", "daemon_restarted");
10718
+ return;
10719
+ }
10720
+ track(claimed, existing.kind, existing.sessionId).sawTurn = true;
10721
+ return;
10722
+ }
10723
+ const context = { payload: run.payload, runId: run.id, automationName: automation.name, ...automation.trigger.kind === "cron" ? { tz: automation.trigger.tz } : {}, now: now() };
10724
+ const action = renderAction(automation.action, context);
10725
+ const env = { VH_AUTOMATION_RUN_ID: run.id, VH_AUTOMATION_NAME: automation.name };
10726
+ const header = automationPromptHeader(automation.name, run.id);
10727
+ if (action.kind === "script") {
10728
+ const timeoutMs = action.timeoutMs ?? automation.maxRuntimeMs;
10729
+ persist({ runId: run.id, automationId: automation.id, kind: "script", phase: "starting", claimId: run.claimId });
10730
+ const item2 = track(claimed, "script");
10731
+ const finished = runScript$1({
10732
+ command: action.command,
10733
+ cwd: action.cwd ? expandHome(action.cwd) : void 0,
10734
+ timeoutMs,
10735
+ env: { ...action.env ?? {}, ...env, ...run.payload ? { VH_AUTOMATION_PAYLOAD: run.payload } : {} },
10736
+ onStart: (child) => {
10737
+ item2.child = child;
10738
+ }
10739
+ });
10740
+ persist({ runId: run.id, automationId: automation.id, kind: "script", phase: "running", claimId: run.claimId });
10741
+ await reportRunning(item2);
10742
+ void finished.then(async (result) => {
10743
+ if (!tracked.has(run.id)) return;
10744
+ const ok = result.exitCode === 0 && !result.timedOut && !result.error;
10745
+ const error = result.error ? `Launch failed: ${result.error}` : result.timedOut ? `Timed out after ${timeoutMs}ms` : result.exitCode !== 0 ? `Exit ${result.exitCode ?? `signal ${result.signal}`}` : void 0;
10746
+ await finish(item2, { claimId: run.claimId, status: ok ? "done" : "failed", summary: clipText(result.output, persistence.AUTOMATION_SUMMARY_MAX_CHARS, "tail"), exitCode: result.exitCode, ...error ? { error } : {} });
10747
+ });
10748
+ return;
10749
+ }
10750
+ if (action.kind === "send") {
10751
+ if (!deps.live(action.sessionId)) {
10752
+ await failNow(claimed, "send", `Session ${action.sessionId} is not running on this machine`, "session_not_live", action.sessionId);
10753
+ return;
10754
+ }
10755
+ persist({ runId: run.id, automationId: automation.id, kind: "send", phase: "running", claimId: run.claimId, sessionId: action.sessionId });
10756
+ try {
10757
+ await send(action.sessionId, `${header}
10758
+ ${action.prompt}`, { localId: `automation-${run.id}` });
10759
+ } catch (error) {
10760
+ await failNow(claimed, "send", `Message delivery failed: ${error instanceof Error ? error.message : String(error)}`, "send_failed", action.sessionId);
10761
+ return;
10762
+ }
10763
+ await reportRunning(track(claimed, "send", action.sessionId), action.sessionId);
10764
+ return;
10765
+ }
10766
+ const agent = action.agent;
10767
+ if (!isSpawnAgent(agent)) {
10768
+ await failNow(claimed, "spawn", `Unknown agent "${agent}"`, "invalid_action");
10769
+ return;
10770
+ }
10771
+ if (action.permissionMode && sanitizeSpawnPermissionMode(action.permissionMode) === null) {
10772
+ await failNow(claimed, "spawn", `Invalid permission mode "${action.permissionMode}"`, "invalid_action");
10773
+ return;
10774
+ }
10775
+ const permissionMode = resolveSpawnPermissionMode(agent, action.permissionMode ?? void 0);
10776
+ const stickyKey = action.sticky ? renderTemplate(action.sticky.key, context) : void 0;
10777
+ if (stickyKey) {
10778
+ const bound = claimed.stickies.find((sticky) => sticky.key === stickyKey);
10779
+ if (bound && deps.live(bound.sessionId)) {
10780
+ persist({ runId: run.id, automationId: automation.id, kind: "send", phase: "running", claimId: run.claimId, sessionId: bound.sessionId, stickyKey });
10781
+ try {
10782
+ await send(bound.sessionId, `${header}
10783
+ ${action.prompt}`, { localId: `automation-${run.id}` });
10784
+ } catch (error) {
10785
+ await failNow(claimed, "send", `Sticky session delivery failed: ${error instanceof Error ? error.message : String(error)}`, "send_failed", bound.sessionId);
10786
+ return;
10787
+ }
10788
+ await reportRunning(track(claimed, "send", bound.sessionId), bound.sessionId, stickyKey);
10789
+ return;
10790
+ }
10791
+ if (bound) await client.deleteSticky(automation.id, stickyKey).catch(() => deps.log(`Automation ${automation.name}: stale sticky ${stickyKey} not cleared`));
10792
+ }
10793
+ let directory = expandHome(action.directory);
10794
+ if (!path.isAbsolute(directory)) {
10795
+ await failNow(claimed, "spawn", `Directory must be absolute: ${action.directory}`, "invalid_action");
10796
+ return;
10797
+ }
10798
+ if (action.worktree) {
10799
+ try {
10800
+ directory = (await prepareWorktree(home, directory, `auto-${run.id}`)).directory;
10801
+ } catch (error) {
10802
+ await failNow(claimed, "spawn", `Worktree preparation failed: ${error instanceof Error ? error.message : String(error)}`, "worktree_failed");
10803
+ return;
10804
+ }
10805
+ }
10806
+ persist({ runId: run.id, automationId: automation.id, kind: "spawn", phase: "starting", claimId: run.claimId, stickyKey, ...action.worktree ? { directory } : {} });
10807
+ const spawned = await deps.spawn({ directory, agent, permissionMode, spawnedBy: AUTOMATION_SPAWNED_BY, environmentVariables: env });
10808
+ if (spawned.type !== "success") {
10809
+ await failNow(claimed, "spawn", spawned.type === "error" ? spawned.errorMessage : "Directory approval required", "spawn_failed");
10810
+ return;
10811
+ }
10812
+ persist({ runId: run.id, automationId: automation.id, kind: "spawn", phase: "running", claimId: run.claimId, sessionId: spawned.sessionId, stickyKey, ...action.worktree ? { directory } : {} });
10813
+ const item = track(claimed, "spawn", spawned.sessionId);
10814
+ try {
10815
+ const capabilities = persistence.readPersistedSessions()[spawned.sessionId]?.metadata?.capabilities ?? null;
10816
+ const meta = resolveFirstMessageMeta({ agent, permissionMode, explicitModel: action.model ?? void 0, capabilities });
10817
+ await send(spawned.sessionId, `${header}
10818
+ ${action.prompt}`, { localId: `automation-${run.id}`, ...meta });
10819
+ } catch (error) {
10820
+ await finish(item, { claimId: run.claimId, status: "failed", sessionId: spawned.sessionId, error: `First message failed: ${error instanceof Error ? error.message : String(error)}`, needsAttention: true, attentionReason: "first_message_failed" });
10821
+ return;
10822
+ }
10823
+ await reportRunning(item, spawned.sessionId, stickyKey);
10824
+ }
10825
+ async function monitor() {
10826
+ for (const item of [...tracked.values()]) {
10827
+ if (closed) return;
10828
+ if (item.pendingFinal) {
10829
+ await finish(item, item.pendingFinal);
10830
+ continue;
10831
+ }
10832
+ if (item.kind === "script") continue;
10833
+ const sessionId = item.sessionId;
10834
+ const active = deps.turnActive(sessionId);
10835
+ if (active) item.sawTurn = true;
10836
+ const alive = deps.live(sessionId) && !item.exited;
10837
+ if (!alive) {
10838
+ const turn2 = await readTurn(sessionId, item.runId).catch(() => null);
10839
+ if (turn2?.ended && turn2.status !== "failed" && turn2.status !== "cancelled") await finish(item, { claimId: item.claimId, status: "done", sessionId, summary: clipText(turn2.answer, persistence.AUTOMATION_SUMMARY_MAX_CHARS) });
10840
+ else await finish(item, { claimId: item.claimId, status: "failed", sessionId, error: turn2?.error ?? "Session exited before the turn ended", needsAttention: true, attentionReason: "session_exited" });
10841
+ continue;
10842
+ }
10843
+ const edgeFell = item.sawTurn && !active;
10844
+ if (!edgeFell && now() < item.nextLogReadAt) continue;
10845
+ item.nextLogReadAt = now() + AUTOMATION_LOG_READ_MS;
10846
+ if (edgeFell) item.sawTurn = false;
10847
+ const turn = await readTurn(sessionId, item.runId).catch(() => null);
10848
+ if (!turn?.ended) continue;
10849
+ if (turn.status === "failed" || turn.status === "cancelled") await finish(item, { claimId: item.claimId, status: "failed", sessionId, error: turn.error ?? `Turn ${turn.status}`, needsAttention: true, attentionReason: `turn_${turn.status}` });
10850
+ else await finish(item, { claimId: item.claimId, status: "done", sessionId, summary: clipText(turn.answer, persistence.AUTOMATION_SUMMARY_MAX_CHARS) });
10851
+ }
10852
+ }
10853
+ async function renewAll() {
10854
+ if (renewing || closed) return;
10855
+ renewing = true;
10856
+ try {
10857
+ const at = now();
10858
+ for (const item of [...tracked.values()]) {
10859
+ if (closed) return;
10860
+ if (at - item.lastLeaseAt < AUTOMATION_LEASE_RENEW_MS) continue;
10861
+ item.lastLeaseAt = at;
10862
+ const body = { claimId: item.claimId, leaseMs: persistence.AUTOMATION_DEFAULT_LEASE_MS, ...item.pendingRunning ? { status: "running", ...item.pendingRunning } : {} };
10863
+ try {
10864
+ const run = await client.report(item.runId, body);
10865
+ item.pendingRunning = void 0;
10866
+ if (persistence.isAutomationRunTerminal(run.status)) settleExternally(item);
10867
+ } catch (error) {
10868
+ if (isSettledByServer(error)) settleExternally(item);
10869
+ else deps.log(`Automation run ${item.runId} lease renewal failed`);
10870
+ }
10871
+ }
10872
+ } finally {
10873
+ renewing = false;
10874
+ }
10875
+ }
10876
+ function settleExternally(item) {
10877
+ if (item.child && scriptAlive(item)) terminateScriptTree(item.child);
10878
+ persist({ runId: item.runId, automationId: item.automationId, kind: item.kind, phase: "finished", claimId: item.claimId, sessionId: item.sessionId });
10879
+ tracked.delete(item.runId);
10880
+ }
10881
+ async function recover() {
10882
+ recovered = true;
10883
+ let receipts;
10884
+ try {
10885
+ receipts = readReceipts(home);
10886
+ } catch {
10887
+ deps.log("Automation receipts unreadable; refusing to execute until repaired");
10888
+ closed = true;
10889
+ return;
10890
+ }
10891
+ for (const receipt of Object.values(receipts)) {
10892
+ if (receipt.phase === "finished") continue;
10893
+ const item = track({ run: { id: receipt.runId, claimId: receipt.claimId }, automation: { id: receipt.automationId } }, receipt.kind, receipt.sessionId);
10894
+ item.sawTurn = true;
10895
+ item.lastLeaseAt = 0;
10896
+ if (receipt.phase === "starting") await finish(item, { claimId: receipt.claimId, status: "failed", error: "Daemon restarted before the launch outcome was known", needsAttention: true, attentionReason: "unknown_outcome" });
10897
+ else if (receipt.kind === "script") await finish(item, { claimId: receipt.claimId, status: "failed", error: "Daemon restarted while the script was running", needsAttention: true, attentionReason: "daemon_restarted" });
10898
+ }
10899
+ }
10900
+ async function tick() {
10901
+ if (running || closed) return;
10902
+ running = true;
10903
+ try {
10904
+ if (!recovered) await recover();
10905
+ if (closed) return;
10906
+ await monitor();
10907
+ const capacity = AUTOMATION_MAX_CONCURRENT - tracked.size;
10908
+ if (now() >= disabledUntil && capacity > 0) {
10909
+ try {
10910
+ const claimed = await client.claim({ machineId: deps.machineId, limit: capacity, leaseMs: persistence.AUTOMATION_DEFAULT_LEASE_MS });
10911
+ await Promise.all(claimed.runs.map((entry) => closed ? Promise.resolve() : execute(entry).catch((error) => deps.log(`Automation run ${entry.run.id} failed to start: ${error instanceof Error ? error.message : String(error)}`))));
10912
+ } catch (error) {
10913
+ if (agentGuidance.isAutomationsUnavailable(error)) {
10914
+ disabledUntil = now() + 6e4;
10915
+ if (now() - lastUnavailableLogAt >= AUTOMATION_UNAVAILABLE_LOG_MS) {
10916
+ lastUnavailableLogAt = now();
10917
+ deps.log("Automations unavailable on this server (404); claim loop idle");
10918
+ }
10919
+ } else {
10920
+ disabledUntil = now() + 1e4;
10921
+ deps.log(`Automation claim failed${error instanceof agentGuidance.AutomationsRequestError ? ` (${error.status})` : ""}`);
10922
+ }
10923
+ }
10924
+ }
10925
+ } finally {
10926
+ running = false;
10927
+ }
10928
+ }
10929
+ function report(sessionId, event) {
10930
+ for (const item of tracked.values()) {
10931
+ if (item.sessionId !== sessionId) continue;
10932
+ if (event === "exited") item.exited = true;
10933
+ if (event === "blocked" && !item.attentionReported) {
10934
+ item.attentionReported = true;
10935
+ void client.report(item.runId, { claimId: item.claimId, needsAttention: true, attentionReason: "needs_input" }).catch(() => deps.log(`Automation run ${item.runId} attention report failed`));
10936
+ }
10937
+ }
10938
+ }
10939
+ const tickMs = deps.tickMs ?? AUTOMATION_TICK_MS;
10940
+ const timer = setInterval(() => {
10941
+ void tick();
10942
+ }, tickMs);
10943
+ timer.unref();
10944
+ const leaseTimer = setInterval(() => {
10945
+ void renewAll();
10946
+ }, tickMs);
10947
+ leaseTimer.unref();
10948
+ void tick();
10949
+ return {
10950
+ tick,
10951
+ renewAll,
10952
+ report,
10953
+ hasSession: (id) => [...tracked.values()].some((item) => item.sessionId === id),
10954
+ trackedRunIds: () => [...tracked.keys()],
10955
+ /** Executing a claim, or a script child is still alive — handover and auto-update wait for this. */
10956
+ get busy() {
10957
+ return running || [...tracked.values()].some(scriptAlive);
10958
+ },
10959
+ /**
10960
+ * Stop the loops. `killScripts` (real shutdown) terminates live script
10961
+ * trees; the next daemon's recovery reports them failed + attention from
10962
+ * the receipt, never re-running them. A handover waits on `busy` instead.
10963
+ */
10964
+ stop: (options = {}) => {
10965
+ closed = true;
10966
+ clearInterval(timer);
10967
+ clearInterval(leaseTimer);
10968
+ if (options.killScripts) {
10969
+ for (const item of tracked.values()) if (item.child && scriptAlive(item)) terminateScriptTree(item.child);
10970
+ }
10971
+ }
10972
+ };
10973
+ }
10974
+
10176
10975
  function daemonExitCode(source) {
10177
10976
  return source === "exception" ? 1 : 0;
10178
10977
  }
@@ -10333,6 +11132,7 @@ async function startDaemon() {
10333
11132
  const turnActivity = new index.TurnActivityTracker();
10334
11133
  const sessionIdToFinishedSession = /* @__PURE__ */ new Map();
10335
11134
  let teamWorker;
11135
+ let automationRunner;
10336
11136
  const persisted = persistence.readPersistedSessions();
10337
11137
  const liveHappyProcesses = await findAllHappyProcesses();
10338
11138
  for (const [id, s] of Object.entries(persisted)) {
@@ -11187,6 +11987,7 @@ async function startDaemon() {
11187
11987
  const onChildExited = (pid) => {
11188
11988
  const session = pidToTrackedSession.get(pid);
11189
11989
  if (session?.happySessionId && (session.spawnedBy === "teams" || teamWorker?.hasSession(session.happySessionId))) teamWorker?.report(session.happySessionId, "exited");
11990
+ if (session?.happySessionId && automationRunner?.hasSession(session.happySessionId)) automationRunner.report(session.happySessionId, "exited");
11190
11991
  if (session?.happySessionId) turnActivity.forget(session.happySessionId);
11191
11992
  if (session?.happySessionId && session.encryption) {
11192
11993
  sessionIdToFinishedSession.set(session.happySessionId, session);
@@ -11206,6 +12007,7 @@ async function startDaemon() {
11206
12007
  }
11207
12008
  };
11208
12009
  const onSessionStateEvent = (sessionId, event, spawnedByFromSession) => {
12010
+ if (automationRunner?.hasSession(sessionId)) automationRunner.report(sessionId, event === "completed" ? "idle" : "blocked");
11209
12011
  if (spawnedByFromSession === "teams" || teamWorker?.hasSession(sessionId) || [...pidToTrackedSession.values()].some((s) => s.happySessionId === sessionId && s.spawnedBy === "teams")) {
11210
12012
  teamWorker?.report(sessionId, event === "completed" ? "idle" : "blocked");
11211
12013
  return;
@@ -11444,7 +12246,7 @@ async function startDaemon() {
11444
12246
  } catch {
11445
12247
  }
11446
12248
  }
11447
- if (bundleReplaced && !teamWorker?.busy && !updateController.isRunning() && !turnActivity.hasActiveTurn()) {
12249
+ if (bundleReplaced && !teamWorker?.busy && !automationRunner?.busy && !updateController.isRunning() && !turnActivity.hasActiveTurn()) {
11448
12250
  const preflight = await preflightNewBundle(bundlePath);
11449
12251
  if (preflight.action === "hold") {
11450
12252
  if (lastHandoverHold !== preflight.reason) {
@@ -11454,10 +12256,11 @@ async function startDaemon() {
11454
12256
  updateController.holdHandover(preflight.reason);
11455
12257
  return;
11456
12258
  }
11457
- if (teamWorker?.busy || updateController.isRunning() || turnActivity.hasActiveTurn()) return;
12259
+ if (teamWorker?.busy || automationRunner?.busy || updateController.isRunning() || turnActivity.hasActiveTurn()) return;
11458
12260
  lastHandoverHold = null;
11459
12261
  persistence.logger.debug("[DAEMON RUN] Daemon bundle replaced on disk and verified, handing off to new daemon");
11460
12262
  teamWorker?.stop();
12263
+ automationRunner?.stop();
11461
12264
  clearInterval(restartOnStaleVersionAndHeartbeat);
11462
12265
  clearInterval(cliUpdateInterval);
11463
12266
  stopAgentVersionChecks();
@@ -11509,6 +12312,14 @@ async function startDaemon() {
11509
12312
  live: (id) => [...pidToTrackedSession.values()].some((s) => s.happySessionId === id && isPidAlive(s.pid)),
11510
12313
  log: (message) => persistence.logger.debug(`[DAEMON RUN] ${message}`)
11511
12314
  });
12315
+ automationRunner = createAutomationRunner({
12316
+ machineId,
12317
+ token: credentials.token,
12318
+ spawn: spawnSession,
12319
+ live: (id) => [...pidToTrackedSession.values()].some((s) => s.happySessionId === id && isPidAlive(s.pid)),
12320
+ turnActive: (id) => turnActivity.activeSessions().includes(id),
12321
+ log: (message) => persistence.logger.debug(`[DAEMON RUN] ${message}`)
12322
+ });
11512
12323
  void apiMachine.updateMachineMetadata((current) => ({
11513
12324
  ...current ?? initialMachineMetadata,
11514
12325
  teamsVersion: 1,
@@ -11517,6 +12328,7 @@ async function startDaemon() {
11517
12328
  const cleanupAndShutdown = async (source, errorMessage) => {
11518
12329
  persistence.logger.debug(`[DAEMON RUN] Starting proper cleanup (source: ${source}, errorMessage: ${errorMessage})...`);
11519
12330
  teamWorker?.stop();
12331
+ automationRunner?.stop({ killScripts: true });
11520
12332
  if (restartOnStaleVersionAndHeartbeat) {
11521
12333
  clearInterval(restartOnStaleVersionAndHeartbeat);
11522
12334
  persistence.logger.debug("[DAEMON RUN] Health check interval cleared");
@@ -12021,6 +12833,7 @@ function createMcpServer(handlers, options, sessionId) {
12021
12833
  version: "1.0.0"
12022
12834
  });
12023
12835
  agentGuidance.registerTeamsTools(mcp, sessionId);
12836
+ agentGuidance.registerAutomationTools(mcp);
12024
12837
  mcp.registerTool("change_title", {
12025
12838
  description: "Change the title of the current chat session",
12026
12839
  title: "Change Chat Title",
@@ -12215,7 +13028,7 @@ async function startHappyServer(client, options) {
12215
13028
  return;
12216
13029
  }
12217
13030
  }
12218
- const usage = index.ContextUsageSchema.parse({ ...JSON.parse(body), updatedAt: Date.now() });
13031
+ const usage = persistence.ContextUsageSchema.parse({ ...JSON.parse(body), updatedAt: Date.now() });
12219
13032
  options.onContextUsage(usage);
12220
13033
  res.writeHead(204).end();
12221
13034
  } catch {
@@ -12263,6 +13076,7 @@ async function startHappyServer(client, options) {
12263
13076
  agentGuidance.PREVIEW_TOOL_NAME,
12264
13077
  agentGuidance.REPORT_PROGRESS_TOOL_NAME,
12265
13078
  ...agentGuidance.TEAM_TOOL_NAMES,
13079
+ ...agentGuidance.AUTOMATION_TOOL_NAMES,
12266
13080
  ...options?.assistant && !process.env.VH_TEAM_SCOPE_FILE ? ASSISTANT_TOOL_NAMES : []
12267
13081
  ],
12268
13082
  stop: () => {
@@ -12599,7 +13413,7 @@ function registerSideQuestionHandler(rpc, deps) {
12599
13413
  const retainMs = deps.retainMs ?? SIDE_QUESTION_RETAIN_MS;
12600
13414
  const maxRunMs = deps.maxRunMs ?? SIDE_QUESTION_MAX_RUN_MS;
12601
13415
  const fork = deps.run ?? (async (input) => {
12602
- const { query } = await Promise.resolve().then(function () { return require('./index-DvfCUuDK.cjs'); });
13416
+ const { query } = await Promise.resolve().then(function () { return require('./index-CRaJ9k2p.cjs'); });
12603
13417
  return runSideQuestion(query, input);
12604
13418
  });
12605
13419
  const run = async (input, live) => {
@@ -12718,7 +13532,7 @@ const MAX_TITLE_CHARS = 60;
12718
13532
  const GENERATION_TIMEOUT_MS = 3e4;
12719
13533
  function resolveClaudeBinary() {
12720
13534
  try {
12721
- 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-K6mUS0jF.cjs', document.baseURI).href)));
13535
+ 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-ZIW1jrYm.cjs', document.baseURI).href)));
12722
13536
  const utilsPath = path.resolve(path.join(projectPath.projectPath(), "scripts", "claude_version_utils.cjs"));
12723
13537
  const { getClaudeCliPath } = require$1(utilsPath);
12724
13538
  const path$1 = getClaudeCliPath();
@@ -14947,9 +15761,9 @@ function resolveLocalSignupBootstrap(configuredMode, configuredInviteCodes, gene
14947
15761
  };
14948
15762
  }
14949
15763
 
14950
- 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-K6mUS0jF.cjs', document.baseURI).href)));
15764
+ 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-ZIW1jrYm.cjs', document.baseURI).href)));
14951
15765
  const __dirname$1 = path.dirname(__filename$1);
14952
- const require_ = node_module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index-K6mUS0jF.cjs', document.baseURI).href)));
15766
+ const require_ = node_module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index-ZIW1jrYm.cjs', document.baseURI).href)));
14953
15767
  const PRISMA_QUERY_ENGINE_FILES = {
14954
15768
  "arm64-darwin": "libquery_engine-darwin-arm64.dylib.node",
14955
15769
  "x64-darwin": "libquery_engine-darwin.dylib.node",
@@ -15384,14 +16198,14 @@ function maybeEmitSubagentStart(subagent, opts, startedSubagents, activeSubagent
15384
16198
  if (!subagent || startedSubagents.has(subagent)) {
15385
16199
  return;
15386
16200
  }
15387
- envelopes.push(index.createEnvelope("agent", { t: "start" }, { ...opts, subagent }));
16201
+ envelopes.push(persistence.createEnvelope("agent", { t: "start" }, { ...opts, subagent }));
15388
16202
  startedSubagents.add(subagent);
15389
16203
  activeSubagents.add(subagent);
15390
16204
  }
15391
16205
  function emitSubagentStops(opts, startedSubagents, activeSubagents) {
15392
16206
  const envelopes = [];
15393
16207
  for (const subagent of activeSubagents) {
15394
- envelopes.push(index.createEnvelope("agent", { t: "stop" }, { ...opts, subagent }));
16208
+ envelopes.push(persistence.createEnvelope("agent", { t: "stop" }, { ...opts, subagent }));
15395
16209
  }
15396
16210
  activeSubagents.clear();
15397
16211
  startedSubagents.clear();
@@ -15487,7 +16301,7 @@ function turnStatus(turn) {
15487
16301
  function emitHistoricalToolCall(envelopes, turn, item, name, title, args, output) {
15488
16302
  const time = turnTimestampMs(turn);
15489
16303
  const opts = { turn: turn.id, time, codexItemId: item.id };
15490
- envelopes.push(index.createEnvelope("agent", {
16304
+ envelopes.push(persistence.createEnvelope("agent", {
15491
16305
  t: "tool-call-start",
15492
16306
  call: item.id,
15493
16307
  name,
@@ -15499,7 +16313,7 @@ function emitHistoricalToolCall(envelopes, turn, item, name, title, args, output
15499
16313
  id: `${item.id}:start`
15500
16314
  }));
15501
16315
  if (output && output.trim().length > 0) {
15502
- envelopes.push(index.createEnvelope("agent", {
16316
+ envelopes.push(persistence.createEnvelope("agent", {
15503
16317
  t: "text",
15504
16318
  text: output,
15505
16319
  thinking: true
@@ -15509,7 +16323,7 @@ function emitHistoricalToolCall(envelopes, turn, item, name, title, args, output
15509
16323
  }));
15510
16324
  }
15511
16325
  if ("status" in item && (item.status === "inProgress" || item.status === "running")) return;
15512
- envelopes.push(index.createEnvelope("agent", {
16326
+ envelopes.push(persistence.createEnvelope("agent", {
15513
16327
  t: "tool-call-end",
15514
16328
  call: item.id
15515
16329
  }, {
@@ -15531,7 +16345,7 @@ function mapCodexCollaborationItem(item, opts = {}, source = "history") {
15531
16345
  agentsStates: item.agentsStates ?? {}
15532
16346
  };
15533
16347
  const startId = source === "live" ? `${item.id}:${item.status ?? "unknown"}:start` : `${item.id}:start`;
15534
- const envelopes = [index.createEnvelope("agent", {
16348
+ const envelopes = [persistence.createEnvelope("agent", {
15535
16349
  t: "tool-call-start",
15536
16350
  call: item.id,
15537
16351
  name: "CodexCollaboration",
@@ -15540,7 +16354,7 @@ function mapCodexCollaborationItem(item, opts = {}, source = "history") {
15540
16354
  args
15541
16355
  }, { ...opts, id: startId })];
15542
16356
  if (item.status === "completed" || item.status === "failed" || item.status === "interrupted") {
15543
- envelopes.push(index.createEnvelope("agent", {
16357
+ envelopes.push(persistence.createEnvelope("agent", {
15544
16358
  t: "tool-call-end",
15545
16359
  call: item.id,
15546
16360
  result: { text: JSON.stringify(args, null, 2), isError: item.status !== "completed" }
@@ -15553,7 +16367,7 @@ function mapCodexThreadToSessionEnvelopes(thread) {
15553
16367
  for (const turn of thread.turns ?? []) {
15554
16368
  const startedAt = turnTimestampMs(turn);
15555
16369
  const completedAt = completedTimestampMs(turn);
15556
- envelopes.push(index.createEnvelope("agent", { t: "turn-start" }, {
16370
+ envelopes.push(persistence.createEnvelope("agent", { t: "turn-start" }, {
15557
16371
  id: `${turn.id}:start`,
15558
16372
  turn: turn.id,
15559
16373
  time: startedAt
@@ -15568,7 +16382,7 @@ function mapCodexThreadToSessionEnvelopes(thread) {
15568
16382
  case "userMessage": {
15569
16383
  const text = textFromInputItems(item.content);
15570
16384
  if (text) {
15571
- envelopes.push(index.createEnvelope("user", { t: "text", text }, {
16385
+ envelopes.push(persistence.createEnvelope("user", { t: "text", text }, {
15572
16386
  id: item.id,
15573
16387
  time: startedAt,
15574
16388
  codexItemId: item.id
@@ -15579,7 +16393,7 @@ function mapCodexThreadToSessionEnvelopes(thread) {
15579
16393
  case "agentMessage": {
15580
16394
  const text = typeof item.text === "string" ? item.text.trim() : "";
15581
16395
  if (text.length > 0) {
15582
- envelopes.push(index.createEnvelope("agent", { t: "text", text }, {
16396
+ envelopes.push(persistence.createEnvelope("agent", { t: "text", text }, {
15583
16397
  id: item.id,
15584
16398
  turn: turn.id,
15585
16399
  time: completedAt,
@@ -15591,7 +16405,7 @@ function mapCodexThreadToSessionEnvelopes(thread) {
15591
16405
  case "reasoning": {
15592
16406
  const text = reasoningText(item);
15593
16407
  if (text) {
15594
- envelopes.push(index.createEnvelope("agent", { t: "text", text, thinking: true }, {
16408
+ envelopes.push(persistence.createEnvelope("agent", { t: "text", text, thinking: true }, {
15595
16409
  id: item.id,
15596
16410
  turn: turn.id,
15597
16411
  time: startedAt,
@@ -15646,7 +16460,7 @@ function mapCodexThreadToSessionEnvelopes(thread) {
15646
16460
  }
15647
16461
  }
15648
16462
  }
15649
- envelopes.push(index.createEnvelope("agent", { t: "turn-end", status: turnStatus(turn) }, {
16463
+ envelopes.push(persistence.createEnvelope("agent", { t: "turn-end", status: turnStatus(turn) }, {
15650
16464
  id: `${turn.id}:end`,
15651
16465
  turn: turn.id,
15652
16466
  time: completedAt
@@ -15701,7 +16515,7 @@ function mapCodexMcpMessageToSessionEnvelopes(message, state) {
15701
16515
  }
15702
16516
  if (type === "task_started") {
15703
16517
  const turnId = cuid2.createId();
15704
- const turnStart = index.createEnvelope("agent", { t: "turn-start" }, { turn: turnId });
16518
+ const turnStart = persistence.createEnvelope("agent", { t: "turn-start" }, { turn: turnId });
15705
16519
  startedSubagents.clear();
15706
16520
  activeSubagents.clear();
15707
16521
  providerSubagentToSessionSubagent.clear();
@@ -15732,7 +16546,7 @@ function mapCodexMcpMessageToSessionEnvelopes(message, state) {
15732
16546
  providerSubagentToSessionSubagent,
15733
16547
  envelopes: [
15734
16548
  ...emitSubagentStops(lifecycleOpts, startedSubagents, activeSubagents),
15735
- index.createEnvelope("agent", {
16549
+ persistence.createEnvelope("agent", {
15736
16550
  t: "turn-end",
15737
16551
  status: pickTurnEndStatus(message, type)
15738
16552
  }, lifecycleOpts)
@@ -15762,7 +16576,7 @@ function mapCodexMcpMessageToSessionEnvelopes(message, state) {
15762
16576
  }
15763
16577
  const envelopes = [];
15764
16578
  maybeEmitSubagentStart(subagent, opts, startedSubagents, activeSubagents, envelopes);
15765
- envelopes.push(index.createEnvelope("agent", { t: "text", text: message.message }, opts));
16579
+ envelopes.push(persistence.createEnvelope("agent", { t: "text", text: message.message }, opts));
15766
16580
  return {
15767
16581
  currentTurnId: state.currentTurnId,
15768
16582
  startedSubagents,
@@ -15784,7 +16598,7 @@ function mapCodexMcpMessageToSessionEnvelopes(message, state) {
15784
16598
  }
15785
16599
  const envelopes = [];
15786
16600
  maybeEmitSubagentStart(subagent, opts, startedSubagents, activeSubagents, envelopes);
15787
- envelopes.push(index.createEnvelope("agent", { t: "text", text, thinking: true }, opts));
16601
+ envelopes.push(persistence.createEnvelope("agent", { t: "text", text, thinking: true }, opts));
15788
16602
  return {
15789
16603
  currentTurnId: state.currentTurnId,
15790
16604
  startedSubagents,
@@ -15801,7 +16615,7 @@ function mapCodexMcpMessageToSessionEnvelopes(message, state) {
15801
16615
  const envelopes = [];
15802
16616
  maybeEmitSubagentStart(subagent, opts, startedSubagents, activeSubagents, envelopes);
15803
16617
  envelopes.push(
15804
- index.createEnvelope("agent", {
16618
+ persistence.createEnvelope("agent", {
15805
16619
  t: "tool-call-start",
15806
16620
  call,
15807
16621
  name: "CodexBash",
@@ -15822,7 +16636,7 @@ function mapCodexMcpMessageToSessionEnvelopes(message, state) {
15822
16636
  const call = pickCallId(message);
15823
16637
  const envelopes = [];
15824
16638
  maybeEmitSubagentStart(subagent, opts, startedSubagents, activeSubagents, envelopes);
15825
- envelopes.push(index.createEnvelope("agent", { t: "tool-call-end", call }, opts));
16639
+ envelopes.push(persistence.createEnvelope("agent", { t: "tool-call-end", call }, opts));
15826
16640
  return {
15827
16641
  currentTurnId: state.currentTurnId,
15828
16642
  startedSubagents,
@@ -15838,7 +16652,7 @@ function mapCodexMcpMessageToSessionEnvelopes(message, state) {
15838
16652
  const envelopes = [];
15839
16653
  maybeEmitSubagentStart(subagent, opts, startedSubagents, activeSubagents, envelopes);
15840
16654
  envelopes.push(
15841
- index.createEnvelope("agent", {
16655
+ persistence.createEnvelope("agent", {
15842
16656
  t: "tool-call-start",
15843
16657
  call,
15844
16658
  name: "CodexPatch",
@@ -15862,7 +16676,7 @@ function mapCodexMcpMessageToSessionEnvelopes(message, state) {
15862
16676
  const call = pickCallId(message);
15863
16677
  const envelopes = [];
15864
16678
  maybeEmitSubagentStart(subagent, opts, startedSubagents, activeSubagents, envelopes);
15865
- envelopes.push(index.createEnvelope("agent", { t: "tool-call-end", call }, opts));
16679
+ envelopes.push(persistence.createEnvelope("agent", { t: "tool-call-end", call }, opts));
15866
16680
  return {
15867
16681
  currentTurnId: state.currentTurnId,
15868
16682
  startedSubagents,
@@ -15883,7 +16697,7 @@ function mapCodexProcessorMessageToSessionEnvelopes(message, state) {
15883
16697
  const toolLikeMessage = message;
15884
16698
  const opts = buildEnvelopeOptions(state.currentTurnId);
15885
16699
  if (message.type === "reasoning") {
15886
- return [index.createEnvelope("agent", {
16700
+ return [persistence.createEnvelope("agent", {
15887
16701
  t: "text",
15888
16702
  text: message.message,
15889
16703
  thinking: true
@@ -15891,7 +16705,7 @@ function mapCodexProcessorMessageToSessionEnvelopes(message, state) {
15891
16705
  }
15892
16706
  if (message.type === "tool-call") {
15893
16707
  const title = typeof toolLikeMessage.input?.title === "string" ? toolLikeMessage.input.title : `${toolLikeMessage.name || "Tool"} call`;
15894
- return [index.createEnvelope("agent", {
16708
+ return [persistence.createEnvelope("agent", {
15895
16709
  t: "tool-call-start",
15896
16710
  call: toolLikeMessage.callId,
15897
16711
  name: toolLikeMessage.name || "unknown",
@@ -15904,13 +16718,13 @@ function mapCodexProcessorMessageToSessionEnvelopes(message, state) {
15904
16718
  const envelopes = [];
15905
16719
  const content = toolLikeMessage.output?.content;
15906
16720
  if (typeof content === "string" && content.trim().length > 0) {
15907
- envelopes.push(index.createEnvelope("agent", {
16721
+ envelopes.push(persistence.createEnvelope("agent", {
15908
16722
  t: "text",
15909
16723
  text: content,
15910
16724
  thinking: true
15911
16725
  }, opts));
15912
16726
  }
15913
- envelopes.push(index.createEnvelope("agent", {
16727
+ envelopes.push(persistence.createEnvelope("agent", {
15914
16728
  t: "tool-call-end",
15915
16729
  call: toolLikeMessage.callId
15916
16730
  }, opts));
@@ -16906,7 +17720,7 @@ function buildCodexTurnPrompt(opts) {
16906
17720
  parts.push(opts.message);
16907
17721
  if (opts.includeTitleInstruction) {
16908
17722
  parts.push(CHANGE_TITLE_INSTRUCTION);
16909
- parts.push(index.BUILTIN_TODO_DISCOVERY);
17723
+ parts.push(persistence.BUILTIN_TODO_DISCOVERY);
16910
17724
  }
16911
17725
  return parts.join("\n\n");
16912
17726
  }
@@ -17887,7 +18701,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
17887
18701
  return;
17888
18702
  } else if (subcommand === "install-terminal-hooks") {
17889
18703
  try {
17890
- const { installTerminalHooks, parseTerminalHooksArgs, TERMINAL_HOOKS_HELP } = await Promise.resolve().then(function () { return require('./installTerminalHooks-NIbsQ1wi.cjs'); });
18704
+ const { installTerminalHooks, parseTerminalHooksArgs, TERMINAL_HOOKS_HELP } = await Promise.resolve().then(function () { return require('./installTerminalHooks-BoDxpaBO.cjs'); });
17891
18705
  const command = parseTerminalHooksArgs(args.slice(1));
17892
18706
  if (command.action === "help") {
17893
18707
  console.log(TERMINAL_HOOKS_HELP);
@@ -17904,7 +18718,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
17904
18718
  }
17905
18719
  } else if (subcommand === "agent-home") {
17906
18720
  try {
17907
- const { parseAgentHomeArgs, runAgentHomeCommand } = await Promise.resolve().then(function () { return require('./agentHome-rylNIkix.cjs'); });
18721
+ const { parseAgentHomeArgs, runAgentHomeCommand } = await Promise.resolve().then(function () { return require('./agentHome-CpJjrQZc.cjs'); });
17908
18722
  await runAgentHomeCommand(parseAgentHomeArgs(args.slice(1)));
17909
18723
  process.exit(0);
17910
18724
  } catch (error) {
@@ -17913,7 +18727,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
17913
18727
  }
17914
18728
  } else if (subcommand === "spawn") {
17915
18729
  try {
17916
- const { handleSpawnCommand } = await Promise.resolve().then(function () { return require('./spawn-DqzG1bTH.cjs'); });
18730
+ const { handleSpawnCommand } = await Promise.resolve().then(function () { return require('./spawn-DA8WAFmm.cjs'); });
17917
18731
  await handleSpawnCommand(args.slice(1));
17918
18732
  } catch (error) {
17919
18733
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
@@ -17925,7 +18739,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
17925
18739
  return;
17926
18740
  } else if (subcommand === "sessions") {
17927
18741
  try {
17928
- const { handleSessionsCommand } = await Promise.resolve().then(function () { return require('./sessions-CpIdDhH7.cjs'); });
18742
+ const { handleSessionsCommand } = await Promise.resolve().then(function () { return require('./sessions-Tad8elmy.cjs'); });
17929
18743
  await handleSessionsCommand(args.slice(1));
17930
18744
  } catch (error) {
17931
18745
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
@@ -17937,7 +18751,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
17937
18751
  return;
17938
18752
  } else if (subcommand === "send") {
17939
18753
  try {
17940
- const { handleSendCommand } = await Promise.resolve().then(function () { return require('./send-qXWGilm8.cjs'); });
18754
+ const { handleSendCommand } = await Promise.resolve().then(function () { return require('./send-wl9ei8yG.cjs'); });
17941
18755
  await handleSendCommand(args.slice(1));
17942
18756
  } catch (error) {
17943
18757
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
@@ -18043,9 +18857,9 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18043
18857
  if (geminiSubcommand === "project" && args[2] === "set" && args[3]) {
18044
18858
  const projectId = args[3];
18045
18859
  try {
18046
- const { saveGoogleCloudProjectToConfig } = await Promise.resolve().then(function () { return require('./config-CgPu2wtS.cjs'); });
18047
- const { readCredentialsForConfiguredRelay: readCredentialsForConfiguredRelay2 } = await Promise.resolve().then(function () { return require('./limits-Dyma1REp.cjs'); }).then(function (n) { return n.persistence; });
18048
- const { ApiClient: ApiClient2 } = await Promise.resolve().then(function () { return require('./types-D2Gkpe0N.cjs'); }).then(function (n) { return n.api; });
18860
+ const { saveGoogleCloudProjectToConfig } = await Promise.resolve().then(function () { return require('./config-DeJgSMh-.cjs'); });
18861
+ const { readCredentialsForConfiguredRelay: readCredentialsForConfiguredRelay2 } = await Promise.resolve().then(function () { return require('./limits-BNTWyUaZ.cjs'); }).then(function (n) { return n.persistence; });
18862
+ const { ApiClient: ApiClient2 } = await Promise.resolve().then(function () { return require('./types-CN28qJl7.cjs'); }).then(function (n) { return n.api; });
18049
18863
  let userEmail = void 0;
18050
18864
  try {
18051
18865
  const credentials = await readCredentialsForConfiguredRelay2();
@@ -18076,7 +18890,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18076
18890
  }
18077
18891
  if (geminiSubcommand === "project" && args[2] === "get") {
18078
18892
  try {
18079
- const { readGeminiLocalConfig } = await Promise.resolve().then(function () { return require('./config-CgPu2wtS.cjs'); });
18893
+ const { readGeminiLocalConfig } = await Promise.resolve().then(function () { return require('./config-DeJgSMh-.cjs'); });
18080
18894
  const config = readGeminiLocalConfig();
18081
18895
  if (config.googleCloudProject) {
18082
18896
  console.log(`Current Google Cloud Project: ${config.googleCloudProject}`);
@@ -18116,7 +18930,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18116
18930
  process.exit(0);
18117
18931
  }
18118
18932
  try {
18119
- const { runGemini } = await Promise.resolve().then(function () { return require('./runGemini-BMYCU16c.cjs'); });
18933
+ const { runGemini } = await Promise.resolve().then(function () { return require('./runGemini-Bgg1BzDK.cjs'); });
18120
18934
  let startedBy = void 0;
18121
18935
  for (let i = 1; i < args.length; i++) {
18122
18936
  if (args[i] === "--started-by") {
@@ -18139,7 +18953,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18139
18953
  } else if (subcommand === "pi") {
18140
18954
  if (args[1] === "--terminal") {
18141
18955
  try {
18142
- const { runPiTerminal } = await Promise.resolve().then(function () { return require('./piTerminal-DHxGB3Ic.cjs'); });
18956
+ const { runPiTerminal } = await Promise.resolve().then(function () { return require('./piTerminal-B7K75aXB.cjs'); });
18143
18957
  process.exitCode = await runPiTerminal(args.slice(2));
18144
18958
  } catch (error) {
18145
18959
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
@@ -18156,7 +18970,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18156
18970
  }
18157
18971
  };
18158
18972
  try {
18159
- const { runAcp, resolveAcpAgentConfig } = await Promise.resolve().then(function () { return require('./index-DEVkDRas.cjs'); });
18973
+ const { runAcp, resolveAcpAgentConfig } = await Promise.resolve().then(function () { return require('./index-imt4mC41.cjs'); });
18160
18974
  const parsed = parsePiRunnerArgs(args.slice(1));
18161
18975
  const resolved = resolveAcpAgentConfig(["pi", ...parsed.passthrough]);
18162
18976
  const { credentials } = await authAndSetupMachineIfNeeded();
@@ -18184,7 +18998,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18184
18998
  return;
18185
18999
  } else if (subcommand === "acp") {
18186
19000
  try {
18187
- const { runAcp, resolveAcpAgentConfig } = await Promise.resolve().then(function () { return require('./index-DEVkDRas.cjs'); });
19001
+ const { runAcp, resolveAcpAgentConfig } = await Promise.resolve().then(function () { return require('./index-imt4mC41.cjs'); });
18188
19002
  let startedBy = void 0;
18189
19003
  let verbose = false;
18190
19004
  const acpArgs = [];
@@ -18224,7 +19038,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18224
19038
  return;
18225
19039
  } else if (subcommand === "openclaw") {
18226
19040
  try {
18227
- const { runOpenClaw } = await Promise.resolve().then(function () { return require('./runOpenClaw-Qs4eFtcS.cjs'); });
19041
+ const { runOpenClaw } = await Promise.resolve().then(function () { return require('./runOpenClaw-B9qWHybc.cjs'); });
18228
19042
  let startedBy = void 0;
18229
19043
  let verbose = false;
18230
19044
  let gatewayUrl;
@@ -18275,7 +19089,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18275
19089
  return;
18276
19090
  } else if (subcommand === "todo") {
18277
19091
  try {
18278
- const { handleTodoCommand } = await Promise.resolve().then(function () { return require('./todo-D7SDXL0c.cjs'); });
19092
+ const { handleTodoCommand } = await Promise.resolve().then(function () { return require('./todo-mCMoGjJk.cjs'); });
18279
19093
  await handleTodoCommand(args.slice(1));
18280
19094
  } catch (error) {
18281
19095
  console.error(JSON.stringify({ error: error instanceof Error ? error.message : "Todo command failed" }));
@@ -18284,16 +19098,28 @@ Conversation history is preserved on the server, but in-flight tool calls are in
18284
19098
  return;
18285
19099
  } else if (subcommand === "teams") {
18286
19100
  try {
18287
- const { handleTeamsCommand } = await Promise.resolve().then(function () { return require('./teams-ByEgUzsH.cjs'); });
19101
+ const { handleTeamsCommand } = await Promise.resolve().then(function () { return require('./teams-qVLrFcdd.cjs'); });
18288
19102
  await handleTeamsCommand(args.slice(1));
18289
19103
  } catch (error) {
18290
19104
  console.error(error instanceof Error ? error.message : "Teams command failed");
18291
19105
  process.exitCode = 1;
18292
19106
  }
18293
19107
  return;
19108
+ } else if (subcommand === "auto") {
19109
+ try {
19110
+ const { handleAutoCommand } = await Promise.resolve().then(function () { return require('./auto-DncmTDvw.cjs'); });
19111
+ await handleAutoCommand(args.slice(1));
19112
+ } catch (error) {
19113
+ console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Automation command failed");
19114
+ if (process.env.DEBUG) {
19115
+ console.error(error);
19116
+ }
19117
+ process.exitCode = 1;
19118
+ }
19119
+ return;
18294
19120
  } else if (subcommand === "mcp" && (args.length === 1 || args.length === 2 && args[1] === "--terminal-tools")) {
18295
19121
  try {
18296
- const { handleMcpCommand } = await Promise.resolve().then(function () { return require('./mcp-_TVUSg8W.cjs'); });
19122
+ const { handleMcpCommand } = await Promise.resolve().then(function () { return require('./mcp-CCKCKL6l.cjs'); });
18297
19123
  await handleMcpCommand(args[1] === "--terminal-tools");
18298
19124
  } catch (error) {
18299
19125
  process.stderr.write(`[very-happy mcp] Fatal: ${error instanceof Error ? error.message : String(error)}
@@ -18527,6 +19353,8 @@ ${chalk.bold("Usage:")}
18527
19353
  conversations on this machine (agent-home --help)
18528
19354
  very-happy connect Connect AI vendor API keys
18529
19355
  very-happy todo Built-in Todos and official agent skill (todo --help)
19356
+ very-happy auto Scheduled / triggered automations: spawn, sticky
19357
+ sessions or scripts with tracked runs (auto --help)
18530
19358
  very-happy sandbox Configure and manage OS-level sandboxing
18531
19359
  very-happy notify Send push notification
18532
19360
  very-happy mcp Stdio MCP server exposing copy_to_clipboard for a
@@ -18679,23 +19507,30 @@ exports.SPAWN_AGENTS = SPAWN_AGENTS;
18679
19507
  exports.StreamRelay = StreamRelay;
18680
19508
  exports.TEAM_SKILL = TEAM_SKILL;
18681
19509
  exports.TitleGenerator = TitleGenerator;
19510
+ exports.TurnWaitTimeoutError = TurnWaitTimeoutError;
18682
19511
  exports.appendStagedAttachmentsToPrompt = appendStagedAttachmentsToPrompt;
18683
19512
  exports.archiveSession = archiveSession;
18684
19513
  exports.checkPreviewPath = checkPreviewPath;
18685
19514
  exports.createSessionMetadata = createSessionMetadata;
19515
+ exports.describeAction = describeAction;
19516
+ exports.describeTrigger = describeTrigger;
18686
19517
  exports.hashObject = hashObject;
18687
19518
  exports.initialMachineMetadata = initialMachineMetadata;
18688
19519
  exports.isValidSpawnOrigin = isValidSpawnOrigin;
18689
19520
  exports.listAccountSessions = listAccountSessions;
18690
19521
  exports.listSessions = listSessions;
18691
19522
  exports.normalizeAcpPermissionMode = normalizeAcpPermissionMode;
19523
+ exports.parseDuration = parseDuration;
18692
19524
  exports.query = query;
19525
+ exports.readSessionMetadata = readSessionMetadata;
18693
19526
  exports.readSessionTranscript = readSessionTranscript;
18694
19527
  exports.registerAgentAttachmentDownloads = registerAgentAttachmentDownloads;
18695
19528
  exports.registerAssistantSessionTools = registerAssistantSessionTools;
18696
19529
  exports.registerKillSessionHandler = registerKillSessionHandler;
18697
19530
  exports.removeSessionModeFile = removeSessionModeFile;
19531
+ exports.resolveFirstMessageMeta = resolveFirstMessageMeta;
18698
19532
  exports.resolvePermissionRequest = resolvePermissionRequest;
19533
+ exports.resolveSpawnPermissionMode = resolveSpawnPermissionMode;
18699
19534
  exports.sanitizeSpawnPermissionMode = sanitizeSpawnPermissionMode;
18700
19535
  exports.sendUserMessage = sendUserMessage;
18701
19536
  exports.sessionWebUrl = sessionWebUrl;
@@ -18704,5 +19539,7 @@ exports.stageClaudeAttachments = stageClaudeAttachments;
18704
19539
  exports.startHappyServer = startHappyServer;
18705
19540
  exports.stopSession = stopSession;
18706
19541
  exports.tmuxSupportsSessionEnv = tmuxSupportsSessionEnv;
19542
+ exports.triggerFromFlags = triggerFromFlags;
18707
19543
  exports.waitForSessionKey = waitForSessionKey;
19544
+ exports.waitForTurnEnd = waitForTurnEnd;
18708
19545
  exports.writeSessionModeFile = writeSessionModeFile;