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.
- package/dist/{AcpBackend-C-dhGJgd.mjs → AcpBackend-Bs3wwYbt.mjs} +2 -2
- package/dist/{AcpBackend-CXawaWWx.cjs → AcpBackend-CogiKCng.cjs} +2 -2
- package/dist/{AcpSessionManager-DKOtSqJc.mjs → AcpSessionManager-BT8PJMEm.mjs} +1 -1
- package/dist/{AcpSessionManager-TrwEZR7y.cjs → AcpSessionManager-BazkBuFo.cjs} +9 -9
- package/dist/agentGuidance-CqBn77AI.cjs +553 -0
- package/dist/agentGuidance-Daxn5Nvj.mjs +529 -0
- package/dist/{agentHome-Dm-Wjs38.mjs → agentHome-CCYfs8Ec.mjs} +3 -3
- package/dist/{agentHome-rylNIkix.cjs → agentHome-CpJjrQZc.cjs} +3 -3
- package/dist/auto-DV5emt1-.mjs +466 -0
- package/dist/auto-DncmTDvw.cjs +472 -0
- package/dist/codex/happyMcpStdioBridge.cjs +4 -2
- package/dist/codex/happyMcpStdioBridge.mjs +4 -2
- package/dist/{config-Dhu6LU0h.mjs → config-DbKpQpdL.mjs} +5 -5
- package/dist/{config-CgPu2wtS.cjs → config-DeJgSMh-.cjs} +5 -5
- package/dist/{index-CpNYvMTJ.mjs → index-BauTv2jB.mjs} +900 -72
- package/dist/{index-BsYH1sjV.mjs → index-CN7OF2cU.mjs} +5 -5
- package/dist/{index-DvfCUuDK.cjs → index-CRaJ9k2p.cjs} +5 -5
- package/dist/{index-CzpooENj.mjs → index-DahaLbu4.mjs} +19 -19
- package/dist/{index-K6mUS0jF.cjs → index-ZIW1jrYm.cjs} +941 -104
- package/dist/{index-DEVkDRas.cjs → index-imt4mC41.cjs} +19 -19
- package/dist/index.cjs +4 -4
- package/dist/index.mjs +4 -4
- package/dist/install-C1K3nrxF.mjs +114 -0
- package/dist/install-CDR3qBhs.cjs +118 -0
- package/dist/{installTerminalHooks-NIbsQ1wi.cjs → installTerminalHooks-BoDxpaBO.cjs} +5 -5
- package/dist/{installTerminalHooks-BF_tmw9V.mjs → installTerminalHooks-VvmlO6Ue.mjs} +5 -5
- package/dist/lib.cjs +3 -3
- package/dist/lib.mjs +3 -3
- package/dist/limits-BNTWyUaZ.cjs +2175 -0
- package/dist/limits-CKR_HJnO.mjs +2080 -0
- package/dist/{mcp-_TVUSg8W.cjs → mcp-CCKCKL6l.cjs} +5 -5
- package/dist/{mcp-DzydgXpf.mjs → mcp-D4dAo5BQ.mjs} +5 -5
- package/dist/{piRuntime-CSAiJk0X.cjs → piRuntime-Bn5ypbjH.cjs} +2 -2
- package/dist/{piRuntime-ACOhil6t.mjs → piRuntime-CW-XA4rz.mjs} +2 -2
- package/dist/{piTerminal-DHxGB3Ic.cjs → piTerminal-B7K75aXB.cjs} +6 -6
- package/dist/{piTerminal-BlE4vbvZ.mjs → piTerminal-BLSXK1u2.mjs} +6 -6
- package/dist/{runGemini-BMYCU16c.cjs → runGemini-Bgg1BzDK.cjs} +7 -7
- package/dist/{runGemini-D5IV1MoF.mjs → runGemini-D5N11LxO.mjs} +7 -7
- package/dist/{runOpenClaw-Qs4eFtcS.cjs → runOpenClaw-B9qWHybc.cjs} +6 -6
- package/dist/{runOpenClaw-CWPXrKMJ.mjs → runOpenClaw-CCpyKbrn.mjs} +6 -6
- package/dist/{send-Dv0km8UI.mjs → send-C44kcXz5.mjs} +21 -7
- package/dist/{send-qXWGilm8.cjs → send-wl9ei8yG.cjs} +21 -7
- package/dist/{sessions-DoULfKui.mjs → sessions-DXJEcUg6.mjs} +57 -11
- package/dist/{sessions-CpIdDhH7.cjs → sessions-Tad8elmy.cjs} +57 -11
- package/dist/{spawn-DQuOmUP7.mjs → spawn-BOlb5y1_.mjs} +142 -28
- package/dist/{spawn-DqzG1bTH.cjs → spawn-DA8WAFmm.cjs} +142 -28
- package/dist/{teams-4mhNh5cj.mjs → teams-Bs8lI887.mjs} +15 -72
- package/dist/{teams-ByEgUzsH.cjs → teams-qVLrFcdd.cjs} +13 -70
- package/dist/{todo-D7SDXL0c.cjs → todo-mCMoGjJk.cjs} +4 -23
- package/dist/{todo-C9IAUf0K.mjs → todo-nzsqGZqp.mjs} +2 -21
- package/dist/{types-D2Gkpe0N.cjs → types-CN28qJl7.cjs} +38 -813
- package/dist/{types-BD542hb1.mjs → types-xKrxbCRv.mjs} +208 -978
- package/package.json +7 -7
- package/dist/agentGuidance-Dla-BXO8.cjs +0 -267
- package/dist/agentGuidance-DrfhtVP1.mjs +0 -251
- package/dist/limits-Dyma1REp.cjs +0 -1236
- package/dist/limits-FThmm7WR.mjs +0 -1165
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import{createRequire as _pkgrollCR}from"node:module";const require=_pkgrollCR(import.meta.url);import chalk from 'chalk';
|
|
2
2
|
import os, { homedir, userInfo, tmpdir } from 'node:os';
|
|
3
3
|
import { randomUUID, randomInt, randomBytes, createHash as createHash$1, timingSafeEqual } from 'node:crypto';
|
|
4
|
-
import { v as notifyDaemonSessionEvent,
|
|
5
|
-
import { l as logger,
|
|
4
|
+
import { v as notifyDaemonSessionEvent, g as getProjectPath, w as initializeSandbox, x as wrapCommand, u as contentLogMetadata, I as InvalidateSync, R as RawJSONLinesSchema, c as delay, y as AsyncLock, M as MAX_CHAT_ATTACHMENT_SOURCE_BYTES, z as notifyDaemonClaudeAuthFailed, B as commandOnPath, j as checkIfDaemonRunningAndCleanupStaleState, D as deriveLocalCliUpdateSummary, E as isClaudeSessionId, G as getLocalHappyAgentCredentialPath, H as readLocalHappyAgentCredentials, J as parseExactVersion, K as compareExactVersions, L as extractCompleteLines, N as decideBackfill, O as parseTerminalHookPayload, P as tmuxArgs, Q as scrubTmuxClientEnv, S as decideMirrorBinding, T as MIRROR_BACKFILL_LINES_DEFAULT, U as mirrorLineKey, V as mirrorLocalId, W as listDaemonSessions, k as stopDaemonSession, X as detectCLIAvailability, Y as detectResumeSupport, Z as refreshAgentHomes, d as describeAgentHome, _ as isDaemonRunningCurrentlyInstalledHappyVersion, $ as stopDaemon, a0 as TurnActivityTracker, a1 as summarizeSpawnSessionForLog, a2 as locateClaudeConversation, a3 as agentHomeSpawnEnv, a4 as shellescape, A as ApiClient, a5 as fetchCliUpdateState, a6 as resolveCliUpdateCheckInterval, a7 as DEFAULT_CLI_UPDATE_CHECK_INTERVAL_MS, a8 as cleanupDaemonState, i as isValidSessionId, s as spawnDaemonSession, a9 as listVhTerminals, aa as isValidTerminalId, ab as readVhTerminal, ac as sendToVhTerminal, ad as TITLE_PROMPT_PREFIX, ae as BOARD_ANALYZER_PROMPT_PREFIX, o as connectionState, af as startOfflineReconnection, q as notifyDaemonSessionStarted, C as CodexAppServerClient } from './types-xKrxbCRv.mjs';
|
|
5
|
+
import { l as logger, k as BUILTIN_TODO_DISCOVERY, U as STREAM_DELTA_MAX_CHARS, V as ensurePrivateDirectory, W as hardenPrivateFile, c as configuration, p as packageJson, r as readSettings, X as readCredentials, L as credentialRelayProblem, F as readDaemonState, Y as AGENT_VERSION_SOURCES, j as encodeBase64, Z as readSettingsOutcome, u as updateSettings, _ as encodeBase64Url, E as decodeBase64, $ as writeCredentialsLegacy, a0 as writeCredentialsDataKey, f as readCredentialsForConfiguredRelay, z as encrypt, d as readPersistedSessions, w as writePrivateFileSync, a1 as decryptWithDataKey, a2 as decryptLegacy, a3 as persistSession, D as decrypt, m as ensurePrivateDirectorySync, a4 as getAgentCodeDefaults, a5 as normalizeAgentKey, a6 as isClaudeOpus55Model, a7 as supportsClaudeOpus55, a8 as AUTOMATION_SUMMARY_MAX_CHARS, a9 as AUTOMATION_CLAIM_MAX_RUNS, aa as AUTOMATION_DEFAULT_LEASE_MS, i as isAutomationRunTerminal, ab as acquireDaemonLock, ac as deletePersistedSessions, ad as writeDaemonState, ae as writeSettings, af as releaseDaemonLock, ag as ContextUsageSchema, C as CLIPBOARD_TOOL_NAME, a as CLIPBOARD_TOOL_TITLE, b as CLIPBOARD_TOOL_DESCRIPTION, h as CLIPBOARD_MAX_BYTES, ah as clearCredentials, ai as clearMachineId, aj as SandboxConfigSchema, e as createEnvelope, ak as getLatestDaemonLog } from './limits-CKR_HJnO.mjs';
|
|
6
6
|
import spawn$1, { spawn } from 'cross-spawn';
|
|
7
7
|
import path, { join, resolve, basename, delimiter, isAbsolute, dirname, sep, normalize } from 'node:path';
|
|
8
8
|
import { createInterface } from 'node:readline';
|
|
9
9
|
import { existsSync, readFileSync, readdirSync, statSync, mkdirSync, realpathSync, unlinkSync, renameSync, writeFileSync, accessSync, constants as constants$1, openSync, fsyncSync, closeSync, rmSync as rmSync$1, chmodSync as chmodSync$1 } from 'node:fs';
|
|
10
10
|
import { p as projectPath } from './projectPath-CSpqWdxp.mjs';
|
|
11
|
-
import {
|
|
11
|
+
import { j as trimIdent, k as AGENT_GUIDANCE, l as createAutomationsClient, i as isAutomationsUnavailable, m as AutomationsRequestError, t as teamScopePath, T as TeamConnectionPreflightError, h as createTeamsClient, P as PREVIEW_TOOL_NAME, R as REPORT_PROGRESS_TOOL_NAME, n as TEAM_TOOL_NAMES, o as AUTOMATION_TOOL_NAMES, r as registerTeamsTools, c as registerAutomationTools, a as PREVIEW_TOOL_TITLE, b as PREVIEW_TOOL_DESCRIPTION, p as REPORT_PROGRESS_TOOL_TITLE, q as REPORT_PROGRESS_TOOL_DESCRIPTION } from './agentGuidance-Daxn5Nvj.mjs';
|
|
12
12
|
import { readFile, writeFile, rename, unlink, mkdir, access as access$1, symlink, rm, readlink, lstat, constants, stat, open as open$1, appendFile } from 'node:fs/promises';
|
|
13
13
|
import fs, { watch, access } from 'fs/promises';
|
|
14
14
|
import { useStdout, useInput, Box, Text, render } from 'ink';
|
|
@@ -456,17 +456,23 @@ function readClaudeSettings() {
|
|
|
456
456
|
}
|
|
457
457
|
function shouldIncludeCoAuthoredBy() {
|
|
458
458
|
const settings = readClaudeSettings();
|
|
459
|
-
if (!settings
|
|
459
|
+
if (!settings) {
|
|
460
|
+
return true;
|
|
461
|
+
}
|
|
462
|
+
if (settings.attribution?.commit === "") {
|
|
463
|
+
return false;
|
|
464
|
+
}
|
|
465
|
+
if (settings.includeCoAuthoredBy === void 0) {
|
|
460
466
|
return true;
|
|
461
467
|
}
|
|
462
468
|
return settings.includeCoAuthoredBy;
|
|
463
469
|
}
|
|
464
470
|
|
|
465
471
|
const BASE_SYSTEM_PROMPT = (() => trimIdent(`
|
|
466
|
-
|
|
472
|
+
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.
|
|
467
473
|
`))() + "\n\n" + AGENT_GUIDANCE + "\n\n" + BUILTIN_TODO_DISCOVERY;
|
|
468
474
|
const CO_AUTHORED_CREDITS = (() => trimIdent(`
|
|
469
|
-
When
|
|
475
|
+
When you yourself create a git commit, instead of just giving co-credit to Claude, also give credit to Happy like so:
|
|
470
476
|
|
|
471
477
|
<main commit message>
|
|
472
478
|
|
|
@@ -474,6 +480,8 @@ const CO_AUTHORED_CREDITS = (() => trimIdent(`
|
|
|
474
480
|
via [Very Happy](https://github.com/Mereithhh/very-happy)
|
|
475
481
|
|
|
476
482
|
Co-Authored-By: Claude <noreply@anthropic.com>
|
|
483
|
+
|
|
484
|
+
Do not add these lines to commit message text you only draft for the user.
|
|
477
485
|
`))();
|
|
478
486
|
const systemPrompt = (() => {
|
|
479
487
|
const includeCoAuthored = shouldIncludeCoAuthoredBy();
|
|
@@ -870,7 +878,7 @@ async function createSessionScanner(opts) {
|
|
|
870
878
|
let processedMessageKeys = /* @__PURE__ */ new Set();
|
|
871
879
|
let deadSessions = /* @__PURE__ */ new Set();
|
|
872
880
|
if (opts.sessionId) {
|
|
873
|
-
let messages = await readSessionLog(projectDir, opts.sessionId);
|
|
881
|
+
let messages = await readSessionLog$1(projectDir, opts.sessionId);
|
|
874
882
|
logger.debug(`[SESSION_SCANNER] Marking ${messages.length} existing messages as processed from session ${opts.sessionId}`);
|
|
875
883
|
for (let m of messages) {
|
|
876
884
|
processedMessageKeys.add(messageKey(m));
|
|
@@ -893,7 +901,7 @@ async function createSessionScanner(opts) {
|
|
|
893
901
|
}
|
|
894
902
|
}
|
|
895
903
|
for (let session of sessions) {
|
|
896
|
-
const sessionMessages = await readSessionLog(projectDir, session);
|
|
904
|
+
const sessionMessages = await readSessionLog$1(projectDir, session);
|
|
897
905
|
let skipped = 0;
|
|
898
906
|
let sent = 0;
|
|
899
907
|
for (let file of sessionMessages) {
|
|
@@ -970,7 +978,7 @@ async function createSessionScanner(opts) {
|
|
|
970
978
|
return;
|
|
971
979
|
}
|
|
972
980
|
if (options?.treatExistingAsProcessed) {
|
|
973
|
-
const existing = await readSessionLog(projectDir, sessionId);
|
|
981
|
+
const existing = await readSessionLog$1(projectDir, sessionId);
|
|
974
982
|
logger.debug(`[SESSION_SCANNER] Pre-marking ${existing.length} existing messages as processed for new session ${sessionId}`);
|
|
975
983
|
for (const m of existing) {
|
|
976
984
|
processedMessageKeys.add(messageKey(m));
|
|
@@ -998,7 +1006,7 @@ function messageKey(message) {
|
|
|
998
1006
|
throw Error();
|
|
999
1007
|
}
|
|
1000
1008
|
}
|
|
1001
|
-
async function readSessionLog(projectDir, sessionId) {
|
|
1009
|
+
async function readSessionLog$1(projectDir, sessionId) {
|
|
1002
1010
|
const expectedSessionFile = join(projectDir, `${sessionId}.jsonl`);
|
|
1003
1011
|
logger.debug(`[SESSION_SCANNER] Reading session file: ${expectedSessionFile}`);
|
|
1004
1012
|
let file;
|
|
@@ -5372,8 +5380,8 @@ ${typeLabels[type] || type}:`));
|
|
|
5372
5380
|
}
|
|
5373
5381
|
console.log(chalk.gray(" An external claude command is only needed for native terminal/mirror use."));
|
|
5374
5382
|
try {
|
|
5375
|
-
const { resolveAgentHomesForCli } = await import('./agentHome-
|
|
5376
|
-
const { describeAgentHome } = await import('./types-
|
|
5383
|
+
const { resolveAgentHomesForCli } = await import('./agentHome-CCYfs8Ec.mjs');
|
|
5384
|
+
const { describeAgentHome } = await import('./types-xKrxbCRv.mjs').then(function (n) { return n.ag; });
|
|
5377
5385
|
const homes = await resolveAgentHomesForCli();
|
|
5378
5386
|
console.log(chalk.green(`\u2713 Claude config dir: ${describeAgentHome(homes.claudeConfigDir)}`));
|
|
5379
5387
|
console.log(chalk.green(`\u2713 Codex home: ${describeAgentHome(homes.codexHome)}`));
|
|
@@ -6084,7 +6092,9 @@ async function sendUserMessage(sessionId, persisted, text, client, options = {})
|
|
|
6084
6092
|
},
|
|
6085
6093
|
meta: {
|
|
6086
6094
|
sentFrom: options.sentFrom ?? "cli",
|
|
6087
|
-
...options.model ? { model: options.model } : {}
|
|
6095
|
+
...options.model !== void 0 ? { model: options.model } : {},
|
|
6096
|
+
...options.permissionMode !== void 0 ? { permissionMode: options.permissionMode } : {},
|
|
6097
|
+
...options.effort !== void 0 ? { effort: options.effort } : {}
|
|
6088
6098
|
}
|
|
6089
6099
|
};
|
|
6090
6100
|
const encrypted = encodeBase64(encrypt(
|
|
@@ -6198,7 +6208,7 @@ const DENIED_BASENAME_PATTERNS = [
|
|
|
6198
6208
|
const DENIED_PATH_SUFFIXES = [
|
|
6199
6209
|
`${sep}.git${sep}config`
|
|
6200
6210
|
];
|
|
6201
|
-
function expandHome(input, home) {
|
|
6211
|
+
function expandHome$1(input, home) {
|
|
6202
6212
|
if (input === "~") return home;
|
|
6203
6213
|
if (input.startsWith(`~${sep}`) || input.startsWith("~/")) {
|
|
6204
6214
|
return resolve(home, input.slice(2));
|
|
@@ -6221,7 +6231,7 @@ function checkPreviewPath(input, opts) {
|
|
|
6221
6231
|
if (input.includes("\0")) {
|
|
6222
6232
|
return { resolved: "", deniedReason: "path contains a NUL byte" };
|
|
6223
6233
|
}
|
|
6224
|
-
const expanded = expandHome(input.trim(), home);
|
|
6234
|
+
const expanded = expandHome$1(input.trim(), home);
|
|
6225
6235
|
const resolved = isAbsolute(expanded) ? resolve(expanded) : resolve(cwd, expanded);
|
|
6226
6236
|
for (const dir of DENIED_HOME_DIRS) {
|
|
6227
6237
|
if (isUnder(resolved, resolve(home, dir))) {
|
|
@@ -6395,13 +6405,22 @@ function startDaemonControlServer({
|
|
|
6395
6405
|
permissionMode: z.string().optional(),
|
|
6396
6406
|
// B-069: spawn-origin tag ('assistant' = session_spawn). Recorded on
|
|
6397
6407
|
// the TrackedSession; old clients never send it.
|
|
6398
|
-
spawnedBy: z.string().optional()
|
|
6408
|
+
spawnedBy: z.string().optional(),
|
|
6409
|
+
// B-492: `very-happy spawn --fork` — same fields the web's fork flow
|
|
6410
|
+
// sends over the machine RPC. Old CLIs never send them.
|
|
6411
|
+
resumeClaudeSessionId: 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(),
|
|
6412
|
+
resumeCodexThreadId: z.string().min(1).max(200).optional(),
|
|
6413
|
+
parentSessionId: z.string().min(1).max(200).optional()
|
|
6399
6414
|
}),
|
|
6400
6415
|
response: {
|
|
6401
6416
|
200: z.object({
|
|
6402
6417
|
success: z.boolean(),
|
|
6403
6418
|
sessionId: z.string().optional(),
|
|
6404
|
-
approvedNewDirectoryCreation: z.boolean().optional()
|
|
6419
|
+
approvedNewDirectoryCreation: z.boolean().optional(),
|
|
6420
|
+
// B-492: true when resumeClaudeSessionId / resumeCodexThreadId was
|
|
6421
|
+
// honoured. A caller that asked for a fork and does not get this
|
|
6422
|
+
// back is talking to an older daemon that spawned a fresh session.
|
|
6423
|
+
resumed: z.boolean().optional()
|
|
6405
6424
|
}),
|
|
6406
6425
|
409: z.object({
|
|
6407
6426
|
success: z.boolean(),
|
|
@@ -6416,13 +6435,14 @@ function startDaemonControlServer({
|
|
|
6416
6435
|
}
|
|
6417
6436
|
}
|
|
6418
6437
|
}, async (request, reply) => {
|
|
6419
|
-
const { directory, sessionId, agent, environmentVariables, variant, forceNew, permissionMode, spawnedBy } = request.body;
|
|
6438
|
+
const { directory, sessionId, agent, environmentVariables, variant, forceNew, permissionMode, spawnedBy, resumeClaudeSessionId, resumeCodexThreadId, parentSessionId } = request.body;
|
|
6420
6439
|
if (!directory && variant !== "assistant") {
|
|
6421
6440
|
reply.code(500);
|
|
6422
6441
|
return { success: false, error: "directory is required" };
|
|
6423
6442
|
}
|
|
6424
6443
|
logger.debug(`[CONTROL SERVER] Spawn session request: dir=${directory}, sessionId=${sessionId || "new"}, agent=${agent || "default"}, variant=${variant || "none"}, forceNew=${forceNew === true}, spawnedBy=${spawnedBy || "unset"}`);
|
|
6425
|
-
const result = await spawnSession({ directory, sessionId, agent, environmentVariables, variant, forceNew, permissionMode, spawnedBy });
|
|
6444
|
+
const result = await spawnSession({ directory, sessionId, agent, environmentVariables, variant, forceNew, permissionMode, spawnedBy, resumeClaudeSessionId, resumeCodexThreadId, parentSessionId });
|
|
6445
|
+
const resumed = Boolean(resumeClaudeSessionId || resumeCodexThreadId);
|
|
6426
6446
|
switch (result.type) {
|
|
6427
6447
|
case "success":
|
|
6428
6448
|
if (!result.sessionId) {
|
|
@@ -6435,7 +6455,8 @@ function startDaemonControlServer({
|
|
|
6435
6455
|
return {
|
|
6436
6456
|
success: true,
|
|
6437
6457
|
sessionId: result.sessionId,
|
|
6438
|
-
approvedNewDirectoryCreation: true
|
|
6458
|
+
approvedNewDirectoryCreation: true,
|
|
6459
|
+
...resumed ? { resumed: true } : {}
|
|
6439
6460
|
};
|
|
6440
6461
|
case "requestToApproveDirectoryCreation":
|
|
6441
6462
|
reply.code(409);
|
|
@@ -9367,6 +9388,8 @@ ${goal}`;
|
|
|
9367
9388
|
|
|
9368
9389
|
const MAX_LINE_CHARS = 500;
|
|
9369
9390
|
const MAX_ARGS_CHARS = 160;
|
|
9391
|
+
const DEFAULT_LIMITS = { line: MAX_LINE_CHARS, args: MAX_ARGS_CHARS };
|
|
9392
|
+
const NO_LIMITS = { line: Number.POSITIVE_INFINITY, args: Number.POSITIVE_INFINITY };
|
|
9370
9393
|
function truncateText(text, max = MAX_LINE_CHARS) {
|
|
9371
9394
|
const chars = Array.from(text);
|
|
9372
9395
|
if (chars.length <= max) return text;
|
|
@@ -9382,12 +9405,12 @@ function safeJson(value, max) {
|
|
|
9382
9405
|
return "[unserializable]";
|
|
9383
9406
|
}
|
|
9384
9407
|
}
|
|
9385
|
-
function formatMessageBody(body) {
|
|
9408
|
+
function formatMessageBody(body, limits = DEFAULT_LIMITS) {
|
|
9386
9409
|
if (!body || typeof body !== "object") return null;
|
|
9387
9410
|
const b = body;
|
|
9388
9411
|
if (b.role === "user" && b.content?.type === "text" && typeof b.content.text === "string") {
|
|
9389
9412
|
const text = squash(b.content.text);
|
|
9390
|
-
return text.length > 0 ? `user: ${truncateText(text)}` : null;
|
|
9413
|
+
return text.length > 0 ? `user: ${truncateText(text, limits.line)}` : null;
|
|
9391
9414
|
}
|
|
9392
9415
|
if (b.role === "session" && b.content && typeof b.content === "object") {
|
|
9393
9416
|
const env = b.content;
|
|
@@ -9399,18 +9422,18 @@ function formatMessageBody(body) {
|
|
|
9399
9422
|
const text = squash(String(ev.text ?? ""));
|
|
9400
9423
|
if (text.length === 0) return null;
|
|
9401
9424
|
const role = env.role === "user" ? "user" : "assistant";
|
|
9402
|
-
return `${role}: ${truncateText(text)}`;
|
|
9425
|
+
return `${role}: ${truncateText(text, limits.line)}`;
|
|
9403
9426
|
}
|
|
9404
9427
|
case "tool-call-start": {
|
|
9405
9428
|
const name = String(ev.name ?? "tool");
|
|
9406
9429
|
const title = squash(String(ev.title ?? ""));
|
|
9407
|
-
const args = ev.args ? safeJson(ev.args,
|
|
9430
|
+
const args = ev.args ? safeJson(ev.args, limits.args) : "";
|
|
9408
9431
|
const detail = title || args;
|
|
9409
|
-
return `[tool] ${name}${detail ? `: ${truncateText(detail,
|
|
9432
|
+
return `[tool] ${name}${detail ? `: ${truncateText(detail, limits.args)}` : ""}`;
|
|
9410
9433
|
}
|
|
9411
9434
|
case "service": {
|
|
9412
9435
|
const text = squash(String(ev.text ?? ""));
|
|
9413
|
-
return text.length > 0 ? `[service] ${truncateText(text,
|
|
9436
|
+
return text.length > 0 ? `[service] ${truncateText(text, limits.args)}` : null;
|
|
9414
9437
|
}
|
|
9415
9438
|
case "turn-end": {
|
|
9416
9439
|
if (ev.status && ev.status !== "completed") return `[turn ${ev.status}]`;
|
|
@@ -9425,12 +9448,12 @@ function formatMessageBody(body) {
|
|
|
9425
9448
|
switch (d.type) {
|
|
9426
9449
|
case "message": {
|
|
9427
9450
|
const text = squash(String(d.message ?? ""));
|
|
9428
|
-
return text.length > 0 ? `assistant: ${truncateText(text)}` : null;
|
|
9451
|
+
return text.length > 0 ? `assistant: ${truncateText(text, limits.line)}` : null;
|
|
9429
9452
|
}
|
|
9430
9453
|
case "tool-call":
|
|
9431
|
-
return `[tool] ${String(d.name ?? "tool")}: ${safeJson(d.input,
|
|
9454
|
+
return `[tool] ${String(d.name ?? "tool")}: ${safeJson(d.input, limits.args)}`;
|
|
9432
9455
|
case "permission-request":
|
|
9433
|
-
return `[permission] ${String(d.toolName ?? "")}: ${truncateText(squash(String(d.description ?? "")),
|
|
9456
|
+
return `[permission] ${String(d.toolName ?? "")}: ${truncateText(squash(String(d.description ?? "")), limits.args)}`;
|
|
9434
9457
|
default:
|
|
9435
9458
|
return null;
|
|
9436
9459
|
}
|
|
@@ -9441,22 +9464,118 @@ function formatMessageBody(body) {
|
|
|
9441
9464
|
const texts = msg.content.filter((c) => c?.type === "text" && typeof c.text === "string").map((c) => squash(c.text)).filter((t) => t.length > 0);
|
|
9442
9465
|
if (texts.length > 0) {
|
|
9443
9466
|
const role = msg.role === "user" ? "user" : "assistant";
|
|
9444
|
-
return `${role}: ${truncateText(texts.join(" "))}`;
|
|
9467
|
+
return `${role}: ${truncateText(texts.join(" "), limits.line)}`;
|
|
9445
9468
|
}
|
|
9446
9469
|
}
|
|
9447
9470
|
return null;
|
|
9448
9471
|
}
|
|
9449
9472
|
return null;
|
|
9450
9473
|
}
|
|
9451
|
-
function formatTranscript(bodies) {
|
|
9474
|
+
function formatTranscript(bodies, options = {}) {
|
|
9475
|
+
const limits = options.full ? NO_LIMITS : DEFAULT_LIMITS;
|
|
9452
9476
|
const lines = [];
|
|
9453
9477
|
for (const body of bodies) {
|
|
9454
|
-
const line = formatMessageBody(body);
|
|
9478
|
+
const line = formatMessageBody(body, limits);
|
|
9455
9479
|
if (line !== null) lines.push(line);
|
|
9456
9480
|
}
|
|
9457
9481
|
return lines.join("\n");
|
|
9458
9482
|
}
|
|
9459
9483
|
|
|
9484
|
+
function asRecord(value) {
|
|
9485
|
+
return value && typeof value === "object" ? value : null;
|
|
9486
|
+
}
|
|
9487
|
+
function sessionEnvelope(body) {
|
|
9488
|
+
const b = asRecord(body);
|
|
9489
|
+
if (!b || b.role !== "session") return null;
|
|
9490
|
+
const env = asRecord(b.content);
|
|
9491
|
+
if (!env) return null;
|
|
9492
|
+
const ev = asRecord(env.ev);
|
|
9493
|
+
return ev && typeof ev.t === "string" ? { role: env.role, ev } : null;
|
|
9494
|
+
}
|
|
9495
|
+
function isUserPrompt(body) {
|
|
9496
|
+
const b = asRecord(body);
|
|
9497
|
+
return !!b && b.role === "user" && b.content?.type === "text" && typeof b.content.text === "string";
|
|
9498
|
+
}
|
|
9499
|
+
function agentPiece(body) {
|
|
9500
|
+
const env = sessionEnvelope(body);
|
|
9501
|
+
if (env && env.role !== "user") {
|
|
9502
|
+
if (env.ev.t === "text" && !env.ev.thinking) {
|
|
9503
|
+
const text = String(env.ev.text ?? "");
|
|
9504
|
+
return text.trim().length > 0 ? { kind: "text", text } : null;
|
|
9505
|
+
}
|
|
9506
|
+
if (env.ev.t === "tool-call-start") return { kind: "tool" };
|
|
9507
|
+
return null;
|
|
9508
|
+
}
|
|
9509
|
+
const b = asRecord(body);
|
|
9510
|
+
if (b?.role === "agent" && b.content?.type === "acp") {
|
|
9511
|
+
const d = asRecord(b.content.data);
|
|
9512
|
+
if (d?.type === "message") {
|
|
9513
|
+
const text = String(d.message ?? "");
|
|
9514
|
+
return text.trim().length > 0 ? { kind: "text", text } : null;
|
|
9515
|
+
}
|
|
9516
|
+
if (d?.type === "tool-call") return { kind: "tool" };
|
|
9517
|
+
}
|
|
9518
|
+
return null;
|
|
9519
|
+
}
|
|
9520
|
+
function userPromptText(body) {
|
|
9521
|
+
return isUserPrompt(body) ? body.content.text : null;
|
|
9522
|
+
}
|
|
9523
|
+
function analyzeLatestTurn(entries, options = {}) {
|
|
9524
|
+
const lastSeq = entries.length > 0 ? entries[entries.length - 1].seq : 0;
|
|
9525
|
+
let userIndex = -1;
|
|
9526
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
9527
|
+
if (isUserPrompt(entries[i].body) && (!options.anchor || options.anchor(entries[i].body))) {
|
|
9528
|
+
userIndex = i;
|
|
9529
|
+
break;
|
|
9530
|
+
}
|
|
9531
|
+
}
|
|
9532
|
+
let ended = false;
|
|
9533
|
+
let status = null;
|
|
9534
|
+
let error = null;
|
|
9535
|
+
let texts = [];
|
|
9536
|
+
let lastMarker = null;
|
|
9537
|
+
let startedAfterPrompt = false;
|
|
9538
|
+
const emitsStarts = entries.some((entry) => sessionEnvelope(entry.body)?.ev?.t === "turn-start");
|
|
9539
|
+
for (let i = userIndex + 1; i < entries.length; i++) {
|
|
9540
|
+
const body = entries[i].body;
|
|
9541
|
+
if (options.anchor && isUserPrompt(body)) {
|
|
9542
|
+
if (ended) break;
|
|
9543
|
+
continue;
|
|
9544
|
+
}
|
|
9545
|
+
const env = sessionEnvelope(body);
|
|
9546
|
+
if (env?.ev?.t === "turn-start") {
|
|
9547
|
+
lastMarker = "start";
|
|
9548
|
+
startedAfterPrompt = true;
|
|
9549
|
+
ended = false;
|
|
9550
|
+
status = null;
|
|
9551
|
+
error = null;
|
|
9552
|
+
texts = [];
|
|
9553
|
+
continue;
|
|
9554
|
+
}
|
|
9555
|
+
if (env?.ev?.t === "turn-end") {
|
|
9556
|
+
lastMarker = "end";
|
|
9557
|
+
if (userIndex >= 0 && emitsStarts && !startedAfterPrompt) continue;
|
|
9558
|
+
ended = true;
|
|
9559
|
+
status = typeof env.ev.status === "string" ? env.ev.status : null;
|
|
9560
|
+
error = typeof env.ev.error === "string" ? env.ev.error : null;
|
|
9561
|
+
continue;
|
|
9562
|
+
}
|
|
9563
|
+
const piece = agentPiece(body);
|
|
9564
|
+
if (!piece) continue;
|
|
9565
|
+
if (piece.kind === "tool") texts = [];
|
|
9566
|
+
else texts.push(piece.text);
|
|
9567
|
+
}
|
|
9568
|
+
if (userIndex < 0) ended = lastMarker === "end";
|
|
9569
|
+
return {
|
|
9570
|
+
userSeq: userIndex >= 0 ? entries[userIndex].seq : null,
|
|
9571
|
+
ended,
|
|
9572
|
+
status: ended ? status : null,
|
|
9573
|
+
error: ended ? error : null,
|
|
9574
|
+
answer: texts.join("\n\n").trim(),
|
|
9575
|
+
lastSeq
|
|
9576
|
+
};
|
|
9577
|
+
}
|
|
9578
|
+
|
|
9460
9579
|
const USER_SOCKET_CONNECT_TIMEOUT_MS = 15e3;
|
|
9461
9580
|
async function openUserScopedSocket(token, options = {}) {
|
|
9462
9581
|
const socket = io(configuration.serverUrl, {
|
|
@@ -9802,19 +9921,21 @@ async function bearerToken() {
|
|
|
9802
9921
|
if (!credentials) throw new Error("CLI is not authenticated (no ~/.happy/access.key)");
|
|
9803
9922
|
return credentials.token;
|
|
9804
9923
|
}
|
|
9805
|
-
|
|
9924
|
+
function requirePersisted(sessionId) {
|
|
9806
9925
|
const persisted = readPersistedSessions()[sessionId];
|
|
9807
9926
|
if (!persisted) {
|
|
9808
9927
|
throw new Error(
|
|
9809
9928
|
`No local key for session ${sessionId} \u2014 it was not spawned by this machine's daemon (or is older than 14 days).`
|
|
9810
9929
|
);
|
|
9811
9930
|
}
|
|
9812
|
-
|
|
9931
|
+
return persisted;
|
|
9932
|
+
}
|
|
9933
|
+
async function readSessionLog(sessionId, persisted, limit) {
|
|
9813
9934
|
const token = await bearerToken();
|
|
9814
9935
|
const response = await axios.get(
|
|
9815
9936
|
`${configuration.serverUrl}/v3/sessions/${encodeURIComponent(sessionId)}/messages`,
|
|
9816
9937
|
{
|
|
9817
|
-
params: { before_seq: 2147483647, limit
|
|
9938
|
+
params: { before_seq: 2147483647, limit },
|
|
9818
9939
|
headers: {
|
|
9819
9940
|
"Authorization": `Bearer ${token}`,
|
|
9820
9941
|
"X-Happy-Client": `${SESSION_OPS_CLIENT}/${configuration.currentCliVersion}`
|
|
@@ -9825,21 +9946,62 @@ async function readSessionTranscript(sessionId, limit) {
|
|
|
9825
9946
|
const messages = Array.isArray(response.data?.messages) ? response.data.messages : [];
|
|
9826
9947
|
messages.reverse();
|
|
9827
9948
|
const key = decodeBase64(persisted.encryptionKey);
|
|
9828
|
-
|
|
9829
|
-
|
|
9830
|
-
if (message.content?.t !== "encrypted") return null;
|
|
9949
|
+
return messages.map((message) => {
|
|
9950
|
+
if (message.content?.t !== "encrypted") return { seq: message.seq, body: null };
|
|
9831
9951
|
try {
|
|
9832
|
-
return decrypt(key, persisted.encryptionVariant, decodeBase64(message.content.c));
|
|
9952
|
+
return { seq: message.seq, body: decrypt(key, persisted.encryptionVariant, decodeBase64(message.content.c)) };
|
|
9833
9953
|
} catch {
|
|
9834
|
-
return null;
|
|
9954
|
+
return { seq: message.seq, body: null };
|
|
9835
9955
|
}
|
|
9836
9956
|
});
|
|
9957
|
+
}
|
|
9958
|
+
const TURN_WINDOW = 500;
|
|
9959
|
+
async function readSessionTranscript(sessionId, limit, options = {}) {
|
|
9960
|
+
const persisted = requirePersisted(sessionId);
|
|
9961
|
+
const bounded = Math.max(1, Math.min(MAX_READ_LIMIT, Math.floor(limit)));
|
|
9962
|
+
const entries = await readSessionLog(sessionId, persisted, Math.max(bounded, TURN_WINDOW));
|
|
9963
|
+
const shown = entries.slice(-bounded);
|
|
9964
|
+
const live = await listDaemonSessions();
|
|
9837
9965
|
return {
|
|
9838
9966
|
summary: toSummary(sessionId, persisted, sessionLiveness(live, sessionId)),
|
|
9839
|
-
messageCount:
|
|
9840
|
-
transcript: formatTranscript(
|
|
9967
|
+
messageCount: shown.length,
|
|
9968
|
+
transcript: formatTranscript(shown.map((entry) => entry.body), { full: options.full }),
|
|
9969
|
+
turn: analyzeLatestTurn(entries)
|
|
9841
9970
|
};
|
|
9842
9971
|
}
|
|
9972
|
+
async function readSessionMetadata(sessionId, persisted) {
|
|
9973
|
+
const token = await bearerToken();
|
|
9974
|
+
const response = await axios.get(`${configuration.serverUrl}/v1/sessions/${encodeURIComponent(sessionId)}`, {
|
|
9975
|
+
headers: {
|
|
9976
|
+
"Authorization": `Bearer ${token}`,
|
|
9977
|
+
"X-Happy-Client": `${SESSION_OPS_CLIENT}/${configuration.currentCliVersion}`
|
|
9978
|
+
},
|
|
9979
|
+
timeout: 15e3
|
|
9980
|
+
});
|
|
9981
|
+
const ciphertext = response.data?.session?.metadata;
|
|
9982
|
+
if (typeof ciphertext !== "string" || ciphertext.length === 0) return null;
|
|
9983
|
+
return decrypt(decodeBase64(persisted.encryptionKey), persisted.encryptionVariant, decodeBase64(ciphertext));
|
|
9984
|
+
}
|
|
9985
|
+
class TurnWaitTimeoutError extends Error {
|
|
9986
|
+
constructor(sessionId, timeoutMs, turn) {
|
|
9987
|
+
super(`Session ${sessionId}: the latest turn did not end within ${Math.round(timeoutMs / 1e3)}s`);
|
|
9988
|
+
this.sessionId = sessionId;
|
|
9989
|
+
this.timeoutMs = timeoutMs;
|
|
9990
|
+
this.turn = turn;
|
|
9991
|
+
}
|
|
9992
|
+
}
|
|
9993
|
+
async function waitForTurnEnd(sessionId, options) {
|
|
9994
|
+
const persisted = requirePersisted(sessionId);
|
|
9995
|
+
const pollMs = options.pollMs ?? 3e3;
|
|
9996
|
+
const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
9997
|
+
const deadline = Date.now() + options.timeoutMs;
|
|
9998
|
+
while (true) {
|
|
9999
|
+
const turn = analyzeLatestTurn(await readSessionLog(sessionId, persisted, TURN_WINDOW));
|
|
10000
|
+
if (turn.ended) return turn;
|
|
10001
|
+
if (Date.now() + pollMs > deadline) throw new TurnWaitTimeoutError(sessionId, options.timeoutMs, turn);
|
|
10002
|
+
await sleep(pollMs);
|
|
10003
|
+
}
|
|
10004
|
+
}
|
|
9843
10005
|
async function stopSession(sessionId) {
|
|
9844
10006
|
return await stopDaemonSession(sessionId);
|
|
9845
10007
|
}
|
|
@@ -9872,7 +10034,7 @@ function readReceipt(home, id) {
|
|
|
9872
10034
|
throw error;
|
|
9873
10035
|
}
|
|
9874
10036
|
}
|
|
9875
|
-
function writeReceipt(home, receipt) {
|
|
10037
|
+
function writeReceipt$1(home, receipt) {
|
|
9876
10038
|
ensurePrivateDirectorySync(join(home, "teams", "effects"));
|
|
9877
10039
|
const target = receiptPath(home, receipt.operationId);
|
|
9878
10040
|
const tmp = `${target}.${process.pid}.tmp`;
|
|
@@ -9949,7 +10111,7 @@ function createTeamWorker(deps) {
|
|
|
9949
10111
|
const action = async (teamId, requestId, value) => (await http.post(`/v1/teams/${encodeURIComponent(teamId)}/actions`, { requestId, action: value })).data;
|
|
9950
10112
|
async function complete(op, receipt) {
|
|
9951
10113
|
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 } : {} });
|
|
9952
|
-
writeReceipt(home, { ...receipt, phase: "completed" });
|
|
10114
|
+
writeReceipt$1(home, { ...receipt, phase: "completed" });
|
|
9953
10115
|
}
|
|
9954
10116
|
async function execute(op) {
|
|
9955
10117
|
let receipt = readReceipt(home, op.id);
|
|
@@ -9966,7 +10128,7 @@ function createTeamWorker(deps) {
|
|
|
9966
10128
|
op = claimed.operation;
|
|
9967
10129
|
credential = claimed.credential;
|
|
9968
10130
|
receipt = { operationId: op.id, teamId: op.teamId, claimId: op.claimId, phase: "prepared" };
|
|
9969
|
-
writeReceipt(home, receipt);
|
|
10131
|
+
writeReceipt$1(home, receipt);
|
|
9970
10132
|
}
|
|
9971
10133
|
if (receipt.phase === "completed") return;
|
|
9972
10134
|
try {
|
|
@@ -9979,7 +10141,7 @@ function createTeamWorker(deps) {
|
|
|
9979
10141
|
if (!task) throw new Error("Task state unavailable; refusing to start");
|
|
9980
10142
|
if (task.currentAttemptId !== op.attemptId || ["done", "cancelled"].includes(task.status)) {
|
|
9981
10143
|
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" });
|
|
9982
|
-
writeReceipt(home, { ...receipt, phase: "completed" });
|
|
10144
|
+
writeReceipt$1(home, { ...receipt, phase: "completed" });
|
|
9983
10145
|
return;
|
|
9984
10146
|
}
|
|
9985
10147
|
if (!op.directory) throw new Error("No repository selected for worker");
|
|
@@ -9989,17 +10151,17 @@ function createTeamWorker(deps) {
|
|
|
9989
10151
|
const scopeFile = join(scopes, `${op.id}.json`);
|
|
9990
10152
|
writePrivateFileSync(scopeFile, JSON.stringify({ serverUrl: server, scopeToken: credential.token, teamId: op.teamId, botId: op.botId, taskId: op.taskId, attemptId: op.attemptId }));
|
|
9991
10153
|
receipt = { ...receipt, ...resource, scopeFile, phase: "spawning" };
|
|
9992
|
-
writeReceipt(home, receipt);
|
|
10154
|
+
writeReceipt$1(home, receipt);
|
|
9993
10155
|
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 } });
|
|
9994
10156
|
if (spawned.type !== "success") throw new Error(spawned.type === "error" ? spawned.errorMessage : "Directory approval required");
|
|
9995
10157
|
receipt = { ...receipt, sessionId: spawned.sessionId, phase: "spawned" };
|
|
9996
|
-
writeReceipt(home, receipt);
|
|
10158
|
+
writeReceipt$1(home, receipt);
|
|
9997
10159
|
}
|
|
9998
10160
|
if (receipt.phase === "spawning") {
|
|
9999
10161
|
const matches = Object.entries(readPersistedSessions()).filter(([, entry]) => entry.metadata?.teamOperationId === op.id);
|
|
10000
10162
|
if (matches.length !== 1) throw new Error("Spawn outcome unknown; reconcile the worker before retrying");
|
|
10001
10163
|
receipt = { ...receipt, sessionId: matches[0][0], phase: "spawned" };
|
|
10002
|
-
writeReceipt(home, receipt);
|
|
10164
|
+
writeReceipt$1(home, receipt);
|
|
10003
10165
|
}
|
|
10004
10166
|
if (receipt.phase === "spawned") {
|
|
10005
10167
|
const current = (await http.get(`/v1/teams/${encodeURIComponent(op.teamId)}`)).data.team;
|
|
@@ -10014,14 +10176,14 @@ function createTeamWorker(deps) {
|
|
|
10014
10176
|
const key = await waitForSessionKey(receipt.sessionId, 1e4);
|
|
10015
10177
|
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 } : {} });
|
|
10016
10178
|
receipt = { ...receipt, phase: "delivered" };
|
|
10017
|
-
writeReceipt(home, receipt);
|
|
10179
|
+
writeReceipt$1(home, receipt);
|
|
10018
10180
|
}
|
|
10019
10181
|
await complete(op, receipt);
|
|
10020
10182
|
} else {
|
|
10021
10183
|
const team = (await http.get(`/v1/teams/${encodeURIComponent(op.teamId)}`)).data.team;
|
|
10022
10184
|
const latest = team.operations.find((candidate) => candidate.id === op.id);
|
|
10023
10185
|
if (latest?.status === "completed") {
|
|
10024
|
-
writeReceipt(home, { ...receipt, phase: "completed" });
|
|
10186
|
+
writeReceipt$1(home, { ...receipt, phase: "completed" });
|
|
10025
10187
|
return;
|
|
10026
10188
|
}
|
|
10027
10189
|
const bot = team.bots.find((candidate) => candidate.id === op.botId);
|
|
@@ -10151,6 +10313,643 @@ Use the teams tools to inspect current task state before acting. This message is
|
|
|
10151
10313
|
} };
|
|
10152
10314
|
}
|
|
10153
10315
|
|
|
10316
|
+
function resolveSpawnPermissionMode(agent, explicit) {
|
|
10317
|
+
return explicit ?? getAgentCodeDefaults(agent ?? "claude").permissionMode;
|
|
10318
|
+
}
|
|
10319
|
+
function outboundPermissionMode(agent, mode) {
|
|
10320
|
+
const key = normalizeAgentKey(agent ?? "claude");
|
|
10321
|
+
if ((key === "claude" || key === "pi") && mode === "yolo") return "bypassPermissions";
|
|
10322
|
+
return mode;
|
|
10323
|
+
}
|
|
10324
|
+
function resolveFirstMessageMeta(input) {
|
|
10325
|
+
const meta = {
|
|
10326
|
+
permissionMode: outboundPermissionMode(input.agent, input.permissionMode)
|
|
10327
|
+
};
|
|
10328
|
+
if (input.explicitModel !== void 0) {
|
|
10329
|
+
meta.model = input.explicitModel === "default" ? null : input.explicitModel;
|
|
10330
|
+
}
|
|
10331
|
+
if (normalizeAgentKey(input.agent ?? "claude") !== "claude") return meta;
|
|
10332
|
+
if (meta.model === void 0) {
|
|
10333
|
+
const model = getAgentCodeDefaults("claude").modelMode;
|
|
10334
|
+
const runnable = !isClaudeOpus55Model(model) || supportsClaudeOpus55({ capabilities: input.capabilities ?? null });
|
|
10335
|
+
meta.model = runnable && model !== "default" ? model : null;
|
|
10336
|
+
}
|
|
10337
|
+
meta.effort = null;
|
|
10338
|
+
return meta;
|
|
10339
|
+
}
|
|
10340
|
+
|
|
10341
|
+
function payloadJson(payload) {
|
|
10342
|
+
if (!payload) return void 0;
|
|
10343
|
+
try {
|
|
10344
|
+
return JSON.parse(payload);
|
|
10345
|
+
} catch {
|
|
10346
|
+
return void 0;
|
|
10347
|
+
}
|
|
10348
|
+
}
|
|
10349
|
+
function lookupPath(root, path) {
|
|
10350
|
+
let current = root;
|
|
10351
|
+
for (const segment of path) {
|
|
10352
|
+
if (current === null || typeof current !== "object") return void 0;
|
|
10353
|
+
current = current[segment];
|
|
10354
|
+
}
|
|
10355
|
+
return current;
|
|
10356
|
+
}
|
|
10357
|
+
function scalar(value) {
|
|
10358
|
+
if (value === void 0 || value === null) return "";
|
|
10359
|
+
if (typeof value === "string") return value;
|
|
10360
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
10361
|
+
try {
|
|
10362
|
+
return JSON.stringify(value);
|
|
10363
|
+
} catch {
|
|
10364
|
+
return "";
|
|
10365
|
+
}
|
|
10366
|
+
}
|
|
10367
|
+
function formatNow(now, tz) {
|
|
10368
|
+
if (!tz) return new Date(now).toISOString();
|
|
10369
|
+
try {
|
|
10370
|
+
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));
|
|
10371
|
+
const get = (type) => parts.find((p) => p.type === type)?.value ?? "";
|
|
10372
|
+
const offset = get("timeZoneName").replace(/^GMT$/, "+00:00").replace(/^GMT/, "");
|
|
10373
|
+
return `${get("year")}-${get("month")}-${get("day")}T${get("hour")}:${get("minute")}:${get("second")}${offset || "Z"}`;
|
|
10374
|
+
} catch {
|
|
10375
|
+
return new Date(now).toISOString();
|
|
10376
|
+
}
|
|
10377
|
+
}
|
|
10378
|
+
function renderTemplate(text, context) {
|
|
10379
|
+
const json = payloadJson(context.payload);
|
|
10380
|
+
return text.replace(/\{\{\s*([a-zA-Z_][a-zA-Z0-9_.-]*)\s*\}\}/g, (whole, name) => {
|
|
10381
|
+
if (name === "payload") return context.payload ?? "";
|
|
10382
|
+
if (name.startsWith("payload.")) return scalar(lookupPath(json, name.slice("payload.".length).split(".")));
|
|
10383
|
+
if (name === "run.id") return context.runId;
|
|
10384
|
+
if (name === "automation.name") return context.automationName;
|
|
10385
|
+
if (name === "now") return formatNow(context.now ?? Date.now(), context.tz);
|
|
10386
|
+
return whole;
|
|
10387
|
+
});
|
|
10388
|
+
}
|
|
10389
|
+
function renderAction(action, context) {
|
|
10390
|
+
const r = (value) => renderTemplate(value, context);
|
|
10391
|
+
if (action.kind === "spawn") return { ...action, prompt: r(action.prompt) };
|
|
10392
|
+
if (action.kind === "send") return { ...action, prompt: r(action.prompt) };
|
|
10393
|
+
return { ...action, command: action.command.map(r), ...action.env ? { env: Object.fromEntries(Object.entries(action.env).map(([k, v]) => [k, r(v)])) } : {} };
|
|
10394
|
+
}
|
|
10395
|
+
function clipText(text, maxChars, keep = "head") {
|
|
10396
|
+
if (text.length <= maxChars) return text;
|
|
10397
|
+
return keep === "head" ? text.slice(0, maxChars) : text.slice(text.length - maxChars);
|
|
10398
|
+
}
|
|
10399
|
+
const DURATION_UNITS = { ms: 1, s: 1e3, m: 6e4, h: 36e5, d: 864e5 };
|
|
10400
|
+
function parseDuration(raw) {
|
|
10401
|
+
const text = raw.trim();
|
|
10402
|
+
if (/^\d+$/.test(text)) return Number(text);
|
|
10403
|
+
const re = /(\d+(?:\.\d+)?)(ms|s|m|h|d)/gy;
|
|
10404
|
+
let total = 0;
|
|
10405
|
+
let matched = 0;
|
|
10406
|
+
let last = 0;
|
|
10407
|
+
let m;
|
|
10408
|
+
while ((m = re.exec(text)) !== null) {
|
|
10409
|
+
total += Number(m[1]) * DURATION_UNITS[m[2]];
|
|
10410
|
+
matched++;
|
|
10411
|
+
last = re.lastIndex;
|
|
10412
|
+
}
|
|
10413
|
+
if (matched === 0 || last !== text.length) throw new Error(`Invalid duration: ${raw} (use e.g. 90s, 5m, 1h30m, 2d)`);
|
|
10414
|
+
return Math.round(total);
|
|
10415
|
+
}
|
|
10416
|
+
function formatDuration(ms) {
|
|
10417
|
+
if (ms % 864e5 === 0) return `${ms / 864e5}d`;
|
|
10418
|
+
if (ms % 36e5 === 0) return `${ms / 36e5}h`;
|
|
10419
|
+
if (ms % 6e4 === 0) return `${ms / 6e4}m`;
|
|
10420
|
+
if (ms % 1e3 === 0) return `${ms / 1e3}s`;
|
|
10421
|
+
return `${ms}ms`;
|
|
10422
|
+
}
|
|
10423
|
+
function triggerFromFlags(flags, now = Date.now()) {
|
|
10424
|
+
const given = [flags.cron !== void 0, flags.every !== void 0, flags.at !== void 0, flags.manual === true].filter(Boolean).length;
|
|
10425
|
+
if (given === 0) {
|
|
10426
|
+
if (flags.tz !== void 0) throw new Error("--tz only applies with --cron");
|
|
10427
|
+
return void 0;
|
|
10428
|
+
}
|
|
10429
|
+
if (given > 1) throw new Error("Use exactly one of --cron, --every, --at, --manual");
|
|
10430
|
+
if (flags.cron !== void 0) {
|
|
10431
|
+
if (!flags.tz) throw new Error("--cron requires --tz <IANA zone> (e.g. Asia/Singapore)");
|
|
10432
|
+
return { kind: "cron", expr: flags.cron, tz: flags.tz };
|
|
10433
|
+
}
|
|
10434
|
+
if (flags.tz !== void 0) throw new Error("--tz only applies with --cron");
|
|
10435
|
+
if (flags.every !== void 0) return { kind: "interval", everyMs: parseDuration(flags.every), anchorAt: now };
|
|
10436
|
+
if (flags.at !== void 0) {
|
|
10437
|
+
const at = Date.parse(flags.at);
|
|
10438
|
+
if (!Number.isFinite(at)) throw new Error(`--at must be an ISO 8601 timestamp, got: ${flags.at}`);
|
|
10439
|
+
return { kind: "once", at };
|
|
10440
|
+
}
|
|
10441
|
+
return { kind: "manual" };
|
|
10442
|
+
}
|
|
10443
|
+
function describeTrigger(trigger) {
|
|
10444
|
+
switch (trigger.kind) {
|
|
10445
|
+
case "cron":
|
|
10446
|
+
return `cron ${trigger.expr} (${trigger.tz})`;
|
|
10447
|
+
case "interval":
|
|
10448
|
+
return `every ${formatDuration(trigger.everyMs)}`;
|
|
10449
|
+
case "once":
|
|
10450
|
+
return `once at ${new Date(trigger.at).toISOString()}`;
|
|
10451
|
+
default:
|
|
10452
|
+
return "manual";
|
|
10453
|
+
}
|
|
10454
|
+
}
|
|
10455
|
+
function describeAction(action) {
|
|
10456
|
+
if (action.kind === "spawn") return `spawn ${action.agent} in ${action.directory}${action.sticky ? ` (sticky ${action.sticky.key})` : ""}`;
|
|
10457
|
+
if (action.kind === "send") return `send to session ${action.sessionId}`;
|
|
10458
|
+
return `script ${action.command.join(" ")}`;
|
|
10459
|
+
}
|
|
10460
|
+
|
|
10461
|
+
const RECEIPT_RETENTION_MS = 7 * 864e5;
|
|
10462
|
+
const RECEIPT_MAX_ENTRIES = 2e3;
|
|
10463
|
+
function receiptsPath(home) {
|
|
10464
|
+
return join(home, "automation-receipts.json");
|
|
10465
|
+
}
|
|
10466
|
+
function readReceipts(home) {
|
|
10467
|
+
let raw;
|
|
10468
|
+
try {
|
|
10469
|
+
raw = readFileSync(receiptsPath(home), "utf8");
|
|
10470
|
+
} catch (error) {
|
|
10471
|
+
if (error.code === "ENOENT") return {};
|
|
10472
|
+
throw error;
|
|
10473
|
+
}
|
|
10474
|
+
const parsed = JSON.parse(raw);
|
|
10475
|
+
if (!parsed || typeof parsed !== "object" || typeof parsed.receipts !== "object") throw new Error("Invalid automation receipts file");
|
|
10476
|
+
return parsed.receipts;
|
|
10477
|
+
}
|
|
10478
|
+
function writeReceipt(home, receipt, now = Date.now()) {
|
|
10479
|
+
if (!/^[a-zA-Z0-9_-]{1,128}$/.test(receipt.runId)) throw new Error("Invalid automation run id");
|
|
10480
|
+
const all = readReceipts(home);
|
|
10481
|
+
const next = { ...receipt, updatedAt: now };
|
|
10482
|
+
all[receipt.runId] = next;
|
|
10483
|
+
const entries = Object.values(all).sort((a, b) => b.updatedAt - a.updatedAt);
|
|
10484
|
+
const kept = {};
|
|
10485
|
+
let finished = 0;
|
|
10486
|
+
for (const entry of entries) {
|
|
10487
|
+
if (entry.phase === "finished") {
|
|
10488
|
+
if (now - entry.updatedAt > RECEIPT_RETENTION_MS || finished >= RECEIPT_MAX_ENTRIES) continue;
|
|
10489
|
+
finished++;
|
|
10490
|
+
}
|
|
10491
|
+
kept[entry.runId] = entry;
|
|
10492
|
+
}
|
|
10493
|
+
const target = receiptsPath(home);
|
|
10494
|
+
ensurePrivateDirectorySync(dirname(target));
|
|
10495
|
+
const tmp = `${target}.${process.pid}.tmp`;
|
|
10496
|
+
writePrivateFileSync(tmp, JSON.stringify({ version: 1, receipts: kept }));
|
|
10497
|
+
const file = openSync(tmp, "r");
|
|
10498
|
+
try {
|
|
10499
|
+
fsyncSync(file);
|
|
10500
|
+
} finally {
|
|
10501
|
+
closeSync(file);
|
|
10502
|
+
}
|
|
10503
|
+
renameSync(tmp, target);
|
|
10504
|
+
if (process.platform !== "win32") {
|
|
10505
|
+
const directory = openSync(dirname(target), "r");
|
|
10506
|
+
try {
|
|
10507
|
+
fsyncSync(directory);
|
|
10508
|
+
} finally {
|
|
10509
|
+
closeSync(directory);
|
|
10510
|
+
}
|
|
10511
|
+
}
|
|
10512
|
+
return next;
|
|
10513
|
+
}
|
|
10514
|
+
|
|
10515
|
+
const SCRIPT_KILL_GRACE_MS = 1e4;
|
|
10516
|
+
const SCRIPT_DRAIN_MS = 2e3;
|
|
10517
|
+
class TailBuffer {
|
|
10518
|
+
constructor(maxChars) {
|
|
10519
|
+
this.maxChars = maxChars;
|
|
10520
|
+
}
|
|
10521
|
+
text = "";
|
|
10522
|
+
push(chunk) {
|
|
10523
|
+
this.text += chunk;
|
|
10524
|
+
if (this.text.length > this.maxChars * 2) this.text = this.text.slice(this.text.length - this.maxChars);
|
|
10525
|
+
}
|
|
10526
|
+
value() {
|
|
10527
|
+
return this.text.length > this.maxChars ? this.text.slice(this.text.length - this.maxChars) : this.text;
|
|
10528
|
+
}
|
|
10529
|
+
}
|
|
10530
|
+
function killScriptTree(child, signal) {
|
|
10531
|
+
if (child.exitCode !== null || child.signalCode !== null || !child.pid) return;
|
|
10532
|
+
try {
|
|
10533
|
+
if (process.platform !== "win32") process.kill(-child.pid, signal);
|
|
10534
|
+
else child.kill(signal);
|
|
10535
|
+
} catch {
|
|
10536
|
+
try {
|
|
10537
|
+
child.kill(signal);
|
|
10538
|
+
} catch {
|
|
10539
|
+
}
|
|
10540
|
+
}
|
|
10541
|
+
}
|
|
10542
|
+
function terminateScriptTree(child) {
|
|
10543
|
+
killScriptTree(child, "SIGTERM");
|
|
10544
|
+
const killer = setTimeout(() => killScriptTree(child, "SIGKILL"), SCRIPT_KILL_GRACE_MS);
|
|
10545
|
+
killer.unref();
|
|
10546
|
+
child.once("exit", () => clearTimeout(killer));
|
|
10547
|
+
}
|
|
10548
|
+
function runScript(options) {
|
|
10549
|
+
return new Promise((resolve) => {
|
|
10550
|
+
const tail = new TailBuffer(AUTOMATION_SUMMARY_MAX_CHARS);
|
|
10551
|
+
let timedOut = false;
|
|
10552
|
+
let settled = false;
|
|
10553
|
+
let child;
|
|
10554
|
+
const timers = [];
|
|
10555
|
+
const finish = (result) => {
|
|
10556
|
+
if (settled) return;
|
|
10557
|
+
settled = true;
|
|
10558
|
+
for (const t of timers) clearTimeout(t);
|
|
10559
|
+
resolve(result);
|
|
10560
|
+
};
|
|
10561
|
+
const timer = setTimeout(() => {
|
|
10562
|
+
timedOut = true;
|
|
10563
|
+
terminateScriptTree(child);
|
|
10564
|
+
}, options.timeoutMs);
|
|
10565
|
+
timer.unref();
|
|
10566
|
+
timers.push(timer);
|
|
10567
|
+
try {
|
|
10568
|
+
child = spawn$3(options.command[0], options.command.slice(1), {
|
|
10569
|
+
cwd: options.cwd,
|
|
10570
|
+
shell: false,
|
|
10571
|
+
windowsHide: true,
|
|
10572
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
10573
|
+
detached: process.platform !== "win32",
|
|
10574
|
+
env: { ...options.baseEnv ?? process.env, ...options.env ?? {} }
|
|
10575
|
+
});
|
|
10576
|
+
} catch (error) {
|
|
10577
|
+
finish({ exitCode: null, signal: null, timedOut: false, output: "", error: error instanceof Error ? error.message : String(error) });
|
|
10578
|
+
return;
|
|
10579
|
+
}
|
|
10580
|
+
options.onStart?.(child);
|
|
10581
|
+
child.stdout?.setEncoding("utf8").on("data", (chunk) => tail.push(chunk));
|
|
10582
|
+
child.stderr?.setEncoding("utf8").on("data", (chunk) => tail.push(chunk));
|
|
10583
|
+
child.on("error", (error) => finish({ exitCode: null, signal: null, timedOut, output: tail.value(), error: error.message }));
|
|
10584
|
+
child.on("close", (code, signal) => finish({ exitCode: code, signal, timedOut, output: tail.value() }));
|
|
10585
|
+
child.on("exit", (code, signal) => {
|
|
10586
|
+
const drain = setTimeout(() => {
|
|
10587
|
+
killScriptTree(child, "SIGKILL");
|
|
10588
|
+
finish({ exitCode: code, signal, timedOut, output: tail.value() });
|
|
10589
|
+
}, SCRIPT_DRAIN_MS);
|
|
10590
|
+
drain.unref();
|
|
10591
|
+
timers.push(drain);
|
|
10592
|
+
});
|
|
10593
|
+
});
|
|
10594
|
+
}
|
|
10595
|
+
|
|
10596
|
+
const AUTOMATION_TICK_MS = 5e3;
|
|
10597
|
+
const AUTOMATION_LEASE_RENEW_MS = 3e4;
|
|
10598
|
+
const AUTOMATION_LOG_READ_MS = 3e4;
|
|
10599
|
+
const AUTOMATION_UNAVAILABLE_LOG_MS = 10 * 6e4;
|
|
10600
|
+
const AUTOMATION_MAX_CONCURRENT = AUTOMATION_CLAIM_MAX_RUNS;
|
|
10601
|
+
const AUTOMATION_SPAWNED_BY = "automation";
|
|
10602
|
+
function automationPromptHeader(name, runId) {
|
|
10603
|
+
return `[Very Happy automation "${name}" run ${runId}; finish with automation_report (runId ${runId}) or \`very-happy auto report --run ${runId}\`]`;
|
|
10604
|
+
}
|
|
10605
|
+
function automationRunAnchor(runId) {
|
|
10606
|
+
const marker = ` run ${runId};`;
|
|
10607
|
+
return (body) => userPromptText(body)?.startsWith("[Very Happy automation ") === true && userPromptText(body).includes(marker);
|
|
10608
|
+
}
|
|
10609
|
+
async function defaultReadTurn(sessionId, runId) {
|
|
10610
|
+
const persisted = readPersistedSessions()[sessionId];
|
|
10611
|
+
if (!persisted) throw new Error(`No local key for session ${sessionId}`);
|
|
10612
|
+
return analyzeLatestTurn(await readSessionLog(sessionId, persisted, TURN_WINDOW), { anchor: automationRunAnchor(runId) });
|
|
10613
|
+
}
|
|
10614
|
+
async function defaultSend(sessionId, text, meta) {
|
|
10615
|
+
const persisted = await waitForSessionKey(sessionId, 15e3);
|
|
10616
|
+
await sendUserMessage(sessionId, persisted, text, "automation", { ...meta, sentFrom: "automation" });
|
|
10617
|
+
}
|
|
10618
|
+
function expandHome(path) {
|
|
10619
|
+
if (path === "~") return homedir();
|
|
10620
|
+
if (path.startsWith("~/")) return join(homedir(), path.slice(2));
|
|
10621
|
+
return path;
|
|
10622
|
+
}
|
|
10623
|
+
function isSettledByServer(error) {
|
|
10624
|
+
if (!(error instanceof AutomationsRequestError)) return false;
|
|
10625
|
+
if (error.status === 409 && (error.code === "run_finished" || error.code === "claim_mismatch")) return true;
|
|
10626
|
+
return error.status === 404 && error.code === "run_not_found";
|
|
10627
|
+
}
|
|
10628
|
+
function scriptAlive(item) {
|
|
10629
|
+
return !!item.child && item.child.exitCode === null && item.child.signalCode === null;
|
|
10630
|
+
}
|
|
10631
|
+
function createAutomationRunner(deps) {
|
|
10632
|
+
const home = deps.home ?? configuration.happyHomeDir;
|
|
10633
|
+
const client = deps.client ?? createAutomationsClient({ serverUrl: deps.serverUrl ?? configuration.serverUrl, token: deps.token, client: "daemon-auto" });
|
|
10634
|
+
const runScript$1 = deps.runScript ?? runScript;
|
|
10635
|
+
const readTurn = deps.readTurn ?? defaultReadTurn;
|
|
10636
|
+
const send = deps.send ?? defaultSend;
|
|
10637
|
+
const prepareWorktree = deps.prepareWorktree ?? prepareTeamWorktree;
|
|
10638
|
+
const now = deps.now ?? (() => Date.now());
|
|
10639
|
+
const tracked = /* @__PURE__ */ new Map();
|
|
10640
|
+
let running = false;
|
|
10641
|
+
let renewing = false;
|
|
10642
|
+
let closed = false;
|
|
10643
|
+
let disabledUntil = 0;
|
|
10644
|
+
let lastUnavailableLogAt = -Infinity;
|
|
10645
|
+
let recovered = false;
|
|
10646
|
+
function persist(receipt) {
|
|
10647
|
+
writeReceipt(home, receipt, now());
|
|
10648
|
+
}
|
|
10649
|
+
async function finish(item, report2) {
|
|
10650
|
+
try {
|
|
10651
|
+
await client.report(item.runId, report2);
|
|
10652
|
+
} catch (error) {
|
|
10653
|
+
if (!isSettledByServer(error)) {
|
|
10654
|
+
item.pendingFinal = report2;
|
|
10655
|
+
deps.log(`Automation run ${item.runId} final report deferred`);
|
|
10656
|
+
return;
|
|
10657
|
+
}
|
|
10658
|
+
}
|
|
10659
|
+
persist({ runId: item.runId, automationId: item.automationId, kind: item.kind, phase: "finished", claimId: item.claimId, sessionId: item.sessionId });
|
|
10660
|
+
tracked.delete(item.runId);
|
|
10661
|
+
}
|
|
10662
|
+
function track(claimed, kind, sessionId, child) {
|
|
10663
|
+
const at = now();
|
|
10664
|
+
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 };
|
|
10665
|
+
tracked.set(item.runId, item);
|
|
10666
|
+
return item;
|
|
10667
|
+
}
|
|
10668
|
+
async function failNow(claimed, kind, error, attentionReason, sessionId) {
|
|
10669
|
+
const item = track(claimed, kind, sessionId);
|
|
10670
|
+
await finish(item, { claimId: item.claimId, status: "failed", error: clipText(error, AUTOMATION_SUMMARY_MAX_CHARS), ...sessionId ? { sessionId } : {}, ...attentionReason ? { needsAttention: true, attentionReason } : {} });
|
|
10671
|
+
}
|
|
10672
|
+
async function reportRunning(item, sessionId, stickyKey) {
|
|
10673
|
+
const body = { claimId: item.claimId, status: "running", ...sessionId ? { sessionId } : {}, ...stickyKey ? { stickyKey } : {} };
|
|
10674
|
+
try {
|
|
10675
|
+
await client.report(item.runId, body);
|
|
10676
|
+
} catch (error) {
|
|
10677
|
+
if (isSettledByServer(error)) {
|
|
10678
|
+
settleExternally(item);
|
|
10679
|
+
return;
|
|
10680
|
+
}
|
|
10681
|
+
if (sessionId) item.pendingRunning = { sessionId, ...stickyKey ? { stickyKey } : {} };
|
|
10682
|
+
deps.log(`Automation run ${item.runId} running report deferred`);
|
|
10683
|
+
}
|
|
10684
|
+
}
|
|
10685
|
+
async function execute(claimed) {
|
|
10686
|
+
const { run, automation } = claimed;
|
|
10687
|
+
const existing = readReceipts(home)[run.id];
|
|
10688
|
+
if (existing) {
|
|
10689
|
+
if (existing.phase === "finished" || tracked.has(run.id)) return;
|
|
10690
|
+
if (existing.phase === "starting") {
|
|
10691
|
+
await failNow(claimed, existing.kind, "Local execution outcome unknown after restart; verify before re-running", "unknown_outcome");
|
|
10692
|
+
return;
|
|
10693
|
+
}
|
|
10694
|
+
if (existing.kind === "script") {
|
|
10695
|
+
await failNow(claimed, "script", "Daemon restarted while the script was running", "daemon_restarted");
|
|
10696
|
+
return;
|
|
10697
|
+
}
|
|
10698
|
+
track(claimed, existing.kind, existing.sessionId).sawTurn = true;
|
|
10699
|
+
return;
|
|
10700
|
+
}
|
|
10701
|
+
const context = { payload: run.payload, runId: run.id, automationName: automation.name, ...automation.trigger.kind === "cron" ? { tz: automation.trigger.tz } : {}, now: now() };
|
|
10702
|
+
const action = renderAction(automation.action, context);
|
|
10703
|
+
const env = { VH_AUTOMATION_RUN_ID: run.id, VH_AUTOMATION_NAME: automation.name };
|
|
10704
|
+
const header = automationPromptHeader(automation.name, run.id);
|
|
10705
|
+
if (action.kind === "script") {
|
|
10706
|
+
const timeoutMs = action.timeoutMs ?? automation.maxRuntimeMs;
|
|
10707
|
+
persist({ runId: run.id, automationId: automation.id, kind: "script", phase: "starting", claimId: run.claimId });
|
|
10708
|
+
const item2 = track(claimed, "script");
|
|
10709
|
+
const finished = runScript$1({
|
|
10710
|
+
command: action.command,
|
|
10711
|
+
cwd: action.cwd ? expandHome(action.cwd) : void 0,
|
|
10712
|
+
timeoutMs,
|
|
10713
|
+
env: { ...action.env ?? {}, ...env, ...run.payload ? { VH_AUTOMATION_PAYLOAD: run.payload } : {} },
|
|
10714
|
+
onStart: (child) => {
|
|
10715
|
+
item2.child = child;
|
|
10716
|
+
}
|
|
10717
|
+
});
|
|
10718
|
+
persist({ runId: run.id, automationId: automation.id, kind: "script", phase: "running", claimId: run.claimId });
|
|
10719
|
+
await reportRunning(item2);
|
|
10720
|
+
void finished.then(async (result) => {
|
|
10721
|
+
if (!tracked.has(run.id)) return;
|
|
10722
|
+
const ok = result.exitCode === 0 && !result.timedOut && !result.error;
|
|
10723
|
+
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;
|
|
10724
|
+
await finish(item2, { claimId: run.claimId, status: ok ? "done" : "failed", summary: clipText(result.output, AUTOMATION_SUMMARY_MAX_CHARS, "tail"), exitCode: result.exitCode, ...error ? { error } : {} });
|
|
10725
|
+
});
|
|
10726
|
+
return;
|
|
10727
|
+
}
|
|
10728
|
+
if (action.kind === "send") {
|
|
10729
|
+
if (!deps.live(action.sessionId)) {
|
|
10730
|
+
await failNow(claimed, "send", `Session ${action.sessionId} is not running on this machine`, "session_not_live", action.sessionId);
|
|
10731
|
+
return;
|
|
10732
|
+
}
|
|
10733
|
+
persist({ runId: run.id, automationId: automation.id, kind: "send", phase: "running", claimId: run.claimId, sessionId: action.sessionId });
|
|
10734
|
+
try {
|
|
10735
|
+
await send(action.sessionId, `${header}
|
|
10736
|
+
${action.prompt}`, { localId: `automation-${run.id}` });
|
|
10737
|
+
} catch (error) {
|
|
10738
|
+
await failNow(claimed, "send", `Message delivery failed: ${error instanceof Error ? error.message : String(error)}`, "send_failed", action.sessionId);
|
|
10739
|
+
return;
|
|
10740
|
+
}
|
|
10741
|
+
await reportRunning(track(claimed, "send", action.sessionId), action.sessionId);
|
|
10742
|
+
return;
|
|
10743
|
+
}
|
|
10744
|
+
const agent = action.agent;
|
|
10745
|
+
if (!isSpawnAgent(agent)) {
|
|
10746
|
+
await failNow(claimed, "spawn", `Unknown agent "${agent}"`, "invalid_action");
|
|
10747
|
+
return;
|
|
10748
|
+
}
|
|
10749
|
+
if (action.permissionMode && sanitizeSpawnPermissionMode(action.permissionMode) === null) {
|
|
10750
|
+
await failNow(claimed, "spawn", `Invalid permission mode "${action.permissionMode}"`, "invalid_action");
|
|
10751
|
+
return;
|
|
10752
|
+
}
|
|
10753
|
+
const permissionMode = resolveSpawnPermissionMode(agent, action.permissionMode ?? void 0);
|
|
10754
|
+
const stickyKey = action.sticky ? renderTemplate(action.sticky.key, context) : void 0;
|
|
10755
|
+
if (stickyKey) {
|
|
10756
|
+
const bound = claimed.stickies.find((sticky) => sticky.key === stickyKey);
|
|
10757
|
+
if (bound && deps.live(bound.sessionId)) {
|
|
10758
|
+
persist({ runId: run.id, automationId: automation.id, kind: "send", phase: "running", claimId: run.claimId, sessionId: bound.sessionId, stickyKey });
|
|
10759
|
+
try {
|
|
10760
|
+
await send(bound.sessionId, `${header}
|
|
10761
|
+
${action.prompt}`, { localId: `automation-${run.id}` });
|
|
10762
|
+
} catch (error) {
|
|
10763
|
+
await failNow(claimed, "send", `Sticky session delivery failed: ${error instanceof Error ? error.message : String(error)}`, "send_failed", bound.sessionId);
|
|
10764
|
+
return;
|
|
10765
|
+
}
|
|
10766
|
+
await reportRunning(track(claimed, "send", bound.sessionId), bound.sessionId, stickyKey);
|
|
10767
|
+
return;
|
|
10768
|
+
}
|
|
10769
|
+
if (bound) await client.deleteSticky(automation.id, stickyKey).catch(() => deps.log(`Automation ${automation.name}: stale sticky ${stickyKey} not cleared`));
|
|
10770
|
+
}
|
|
10771
|
+
let directory = expandHome(action.directory);
|
|
10772
|
+
if (!isAbsolute(directory)) {
|
|
10773
|
+
await failNow(claimed, "spawn", `Directory must be absolute: ${action.directory}`, "invalid_action");
|
|
10774
|
+
return;
|
|
10775
|
+
}
|
|
10776
|
+
if (action.worktree) {
|
|
10777
|
+
try {
|
|
10778
|
+
directory = (await prepareWorktree(home, directory, `auto-${run.id}`)).directory;
|
|
10779
|
+
} catch (error) {
|
|
10780
|
+
await failNow(claimed, "spawn", `Worktree preparation failed: ${error instanceof Error ? error.message : String(error)}`, "worktree_failed");
|
|
10781
|
+
return;
|
|
10782
|
+
}
|
|
10783
|
+
}
|
|
10784
|
+
persist({ runId: run.id, automationId: automation.id, kind: "spawn", phase: "starting", claimId: run.claimId, stickyKey, ...action.worktree ? { directory } : {} });
|
|
10785
|
+
const spawned = await deps.spawn({ directory, agent, permissionMode, spawnedBy: AUTOMATION_SPAWNED_BY, environmentVariables: env });
|
|
10786
|
+
if (spawned.type !== "success") {
|
|
10787
|
+
await failNow(claimed, "spawn", spawned.type === "error" ? spawned.errorMessage : "Directory approval required", "spawn_failed");
|
|
10788
|
+
return;
|
|
10789
|
+
}
|
|
10790
|
+
persist({ runId: run.id, automationId: automation.id, kind: "spawn", phase: "running", claimId: run.claimId, sessionId: spawned.sessionId, stickyKey, ...action.worktree ? { directory } : {} });
|
|
10791
|
+
const item = track(claimed, "spawn", spawned.sessionId);
|
|
10792
|
+
try {
|
|
10793
|
+
const capabilities = readPersistedSessions()[spawned.sessionId]?.metadata?.capabilities ?? null;
|
|
10794
|
+
const meta = resolveFirstMessageMeta({ agent, permissionMode, explicitModel: action.model ?? void 0, capabilities });
|
|
10795
|
+
await send(spawned.sessionId, `${header}
|
|
10796
|
+
${action.prompt}`, { localId: `automation-${run.id}`, ...meta });
|
|
10797
|
+
} catch (error) {
|
|
10798
|
+
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" });
|
|
10799
|
+
return;
|
|
10800
|
+
}
|
|
10801
|
+
await reportRunning(item, spawned.sessionId, stickyKey);
|
|
10802
|
+
}
|
|
10803
|
+
async function monitor() {
|
|
10804
|
+
for (const item of [...tracked.values()]) {
|
|
10805
|
+
if (closed) return;
|
|
10806
|
+
if (item.pendingFinal) {
|
|
10807
|
+
await finish(item, item.pendingFinal);
|
|
10808
|
+
continue;
|
|
10809
|
+
}
|
|
10810
|
+
if (item.kind === "script") continue;
|
|
10811
|
+
const sessionId = item.sessionId;
|
|
10812
|
+
const active = deps.turnActive(sessionId);
|
|
10813
|
+
if (active) item.sawTurn = true;
|
|
10814
|
+
const alive = deps.live(sessionId) && !item.exited;
|
|
10815
|
+
if (!alive) {
|
|
10816
|
+
const turn2 = await readTurn(sessionId, item.runId).catch(() => null);
|
|
10817
|
+
if (turn2?.ended && turn2.status !== "failed" && turn2.status !== "cancelled") await finish(item, { claimId: item.claimId, status: "done", sessionId, summary: clipText(turn2.answer, AUTOMATION_SUMMARY_MAX_CHARS) });
|
|
10818
|
+
else await finish(item, { claimId: item.claimId, status: "failed", sessionId, error: turn2?.error ?? "Session exited before the turn ended", needsAttention: true, attentionReason: "session_exited" });
|
|
10819
|
+
continue;
|
|
10820
|
+
}
|
|
10821
|
+
const edgeFell = item.sawTurn && !active;
|
|
10822
|
+
if (!edgeFell && now() < item.nextLogReadAt) continue;
|
|
10823
|
+
item.nextLogReadAt = now() + AUTOMATION_LOG_READ_MS;
|
|
10824
|
+
if (edgeFell) item.sawTurn = false;
|
|
10825
|
+
const turn = await readTurn(sessionId, item.runId).catch(() => null);
|
|
10826
|
+
if (!turn?.ended) continue;
|
|
10827
|
+
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}` });
|
|
10828
|
+
else await finish(item, { claimId: item.claimId, status: "done", sessionId, summary: clipText(turn.answer, AUTOMATION_SUMMARY_MAX_CHARS) });
|
|
10829
|
+
}
|
|
10830
|
+
}
|
|
10831
|
+
async function renewAll() {
|
|
10832
|
+
if (renewing || closed) return;
|
|
10833
|
+
renewing = true;
|
|
10834
|
+
try {
|
|
10835
|
+
const at = now();
|
|
10836
|
+
for (const item of [...tracked.values()]) {
|
|
10837
|
+
if (closed) return;
|
|
10838
|
+
if (at - item.lastLeaseAt < AUTOMATION_LEASE_RENEW_MS) continue;
|
|
10839
|
+
item.lastLeaseAt = at;
|
|
10840
|
+
const body = { claimId: item.claimId, leaseMs: AUTOMATION_DEFAULT_LEASE_MS, ...item.pendingRunning ? { status: "running", ...item.pendingRunning } : {} };
|
|
10841
|
+
try {
|
|
10842
|
+
const run = await client.report(item.runId, body);
|
|
10843
|
+
item.pendingRunning = void 0;
|
|
10844
|
+
if (isAutomationRunTerminal(run.status)) settleExternally(item);
|
|
10845
|
+
} catch (error) {
|
|
10846
|
+
if (isSettledByServer(error)) settleExternally(item);
|
|
10847
|
+
else deps.log(`Automation run ${item.runId} lease renewal failed`);
|
|
10848
|
+
}
|
|
10849
|
+
}
|
|
10850
|
+
} finally {
|
|
10851
|
+
renewing = false;
|
|
10852
|
+
}
|
|
10853
|
+
}
|
|
10854
|
+
function settleExternally(item) {
|
|
10855
|
+
if (item.child && scriptAlive(item)) terminateScriptTree(item.child);
|
|
10856
|
+
persist({ runId: item.runId, automationId: item.automationId, kind: item.kind, phase: "finished", claimId: item.claimId, sessionId: item.sessionId });
|
|
10857
|
+
tracked.delete(item.runId);
|
|
10858
|
+
}
|
|
10859
|
+
async function recover() {
|
|
10860
|
+
recovered = true;
|
|
10861
|
+
let receipts;
|
|
10862
|
+
try {
|
|
10863
|
+
receipts = readReceipts(home);
|
|
10864
|
+
} catch {
|
|
10865
|
+
deps.log("Automation receipts unreadable; refusing to execute until repaired");
|
|
10866
|
+
closed = true;
|
|
10867
|
+
return;
|
|
10868
|
+
}
|
|
10869
|
+
for (const receipt of Object.values(receipts)) {
|
|
10870
|
+
if (receipt.phase === "finished") continue;
|
|
10871
|
+
const item = track({ run: { id: receipt.runId, claimId: receipt.claimId }, automation: { id: receipt.automationId } }, receipt.kind, receipt.sessionId);
|
|
10872
|
+
item.sawTurn = true;
|
|
10873
|
+
item.lastLeaseAt = 0;
|
|
10874
|
+
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" });
|
|
10875
|
+
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" });
|
|
10876
|
+
}
|
|
10877
|
+
}
|
|
10878
|
+
async function tick() {
|
|
10879
|
+
if (running || closed) return;
|
|
10880
|
+
running = true;
|
|
10881
|
+
try {
|
|
10882
|
+
if (!recovered) await recover();
|
|
10883
|
+
if (closed) return;
|
|
10884
|
+
await monitor();
|
|
10885
|
+
const capacity = AUTOMATION_MAX_CONCURRENT - tracked.size;
|
|
10886
|
+
if (now() >= disabledUntil && capacity > 0) {
|
|
10887
|
+
try {
|
|
10888
|
+
const claimed = await client.claim({ machineId: deps.machineId, limit: capacity, leaseMs: AUTOMATION_DEFAULT_LEASE_MS });
|
|
10889
|
+
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)}`))));
|
|
10890
|
+
} catch (error) {
|
|
10891
|
+
if (isAutomationsUnavailable(error)) {
|
|
10892
|
+
disabledUntil = now() + 6e4;
|
|
10893
|
+
if (now() - lastUnavailableLogAt >= AUTOMATION_UNAVAILABLE_LOG_MS) {
|
|
10894
|
+
lastUnavailableLogAt = now();
|
|
10895
|
+
deps.log("Automations unavailable on this server (404); claim loop idle");
|
|
10896
|
+
}
|
|
10897
|
+
} else {
|
|
10898
|
+
disabledUntil = now() + 1e4;
|
|
10899
|
+
deps.log(`Automation claim failed${error instanceof AutomationsRequestError ? ` (${error.status})` : ""}`);
|
|
10900
|
+
}
|
|
10901
|
+
}
|
|
10902
|
+
}
|
|
10903
|
+
} finally {
|
|
10904
|
+
running = false;
|
|
10905
|
+
}
|
|
10906
|
+
}
|
|
10907
|
+
function report(sessionId, event) {
|
|
10908
|
+
for (const item of tracked.values()) {
|
|
10909
|
+
if (item.sessionId !== sessionId) continue;
|
|
10910
|
+
if (event === "exited") item.exited = true;
|
|
10911
|
+
if (event === "blocked" && !item.attentionReported) {
|
|
10912
|
+
item.attentionReported = true;
|
|
10913
|
+
void client.report(item.runId, { claimId: item.claimId, needsAttention: true, attentionReason: "needs_input" }).catch(() => deps.log(`Automation run ${item.runId} attention report failed`));
|
|
10914
|
+
}
|
|
10915
|
+
}
|
|
10916
|
+
}
|
|
10917
|
+
const tickMs = deps.tickMs ?? AUTOMATION_TICK_MS;
|
|
10918
|
+
const timer = setInterval(() => {
|
|
10919
|
+
void tick();
|
|
10920
|
+
}, tickMs);
|
|
10921
|
+
timer.unref();
|
|
10922
|
+
const leaseTimer = setInterval(() => {
|
|
10923
|
+
void renewAll();
|
|
10924
|
+
}, tickMs);
|
|
10925
|
+
leaseTimer.unref();
|
|
10926
|
+
void tick();
|
|
10927
|
+
return {
|
|
10928
|
+
tick,
|
|
10929
|
+
renewAll,
|
|
10930
|
+
report,
|
|
10931
|
+
hasSession: (id) => [...tracked.values()].some((item) => item.sessionId === id),
|
|
10932
|
+
trackedRunIds: () => [...tracked.keys()],
|
|
10933
|
+
/** Executing a claim, or a script child is still alive — handover and auto-update wait for this. */
|
|
10934
|
+
get busy() {
|
|
10935
|
+
return running || [...tracked.values()].some(scriptAlive);
|
|
10936
|
+
},
|
|
10937
|
+
/**
|
|
10938
|
+
* Stop the loops. `killScripts` (real shutdown) terminates live script
|
|
10939
|
+
* trees; the next daemon's recovery reports them failed + attention from
|
|
10940
|
+
* the receipt, never re-running them. A handover waits on `busy` instead.
|
|
10941
|
+
*/
|
|
10942
|
+
stop: (options = {}) => {
|
|
10943
|
+
closed = true;
|
|
10944
|
+
clearInterval(timer);
|
|
10945
|
+
clearInterval(leaseTimer);
|
|
10946
|
+
if (options.killScripts) {
|
|
10947
|
+
for (const item of tracked.values()) if (item.child && scriptAlive(item)) terminateScriptTree(item.child);
|
|
10948
|
+
}
|
|
10949
|
+
}
|
|
10950
|
+
};
|
|
10951
|
+
}
|
|
10952
|
+
|
|
10154
10953
|
function daemonExitCode(source) {
|
|
10155
10954
|
return source === "exception" ? 1 : 0;
|
|
10156
10955
|
}
|
|
@@ -10311,6 +11110,7 @@ async function startDaemon() {
|
|
|
10311
11110
|
const turnActivity = new TurnActivityTracker();
|
|
10312
11111
|
const sessionIdToFinishedSession = /* @__PURE__ */ new Map();
|
|
10313
11112
|
let teamWorker;
|
|
11113
|
+
let automationRunner;
|
|
10314
11114
|
const persisted = readPersistedSessions();
|
|
10315
11115
|
const liveHappyProcesses = await findAllHappyProcesses();
|
|
10316
11116
|
for (const [id, s] of Object.entries(persisted)) {
|
|
@@ -11165,6 +11965,7 @@ async function startDaemon() {
|
|
|
11165
11965
|
const onChildExited = (pid) => {
|
|
11166
11966
|
const session = pidToTrackedSession.get(pid);
|
|
11167
11967
|
if (session?.happySessionId && (session.spawnedBy === "teams" || teamWorker?.hasSession(session.happySessionId))) teamWorker?.report(session.happySessionId, "exited");
|
|
11968
|
+
if (session?.happySessionId && automationRunner?.hasSession(session.happySessionId)) automationRunner.report(session.happySessionId, "exited");
|
|
11168
11969
|
if (session?.happySessionId) turnActivity.forget(session.happySessionId);
|
|
11169
11970
|
if (session?.happySessionId && session.encryption) {
|
|
11170
11971
|
sessionIdToFinishedSession.set(session.happySessionId, session);
|
|
@@ -11184,6 +11985,7 @@ async function startDaemon() {
|
|
|
11184
11985
|
}
|
|
11185
11986
|
};
|
|
11186
11987
|
const onSessionStateEvent = (sessionId, event, spawnedByFromSession) => {
|
|
11988
|
+
if (automationRunner?.hasSession(sessionId)) automationRunner.report(sessionId, event === "completed" ? "idle" : "blocked");
|
|
11187
11989
|
if (spawnedByFromSession === "teams" || teamWorker?.hasSession(sessionId) || [...pidToTrackedSession.values()].some((s) => s.happySessionId === sessionId && s.spawnedBy === "teams")) {
|
|
11188
11990
|
teamWorker?.report(sessionId, event === "completed" ? "idle" : "blocked");
|
|
11189
11991
|
return;
|
|
@@ -11422,7 +12224,7 @@ async function startDaemon() {
|
|
|
11422
12224
|
} catch {
|
|
11423
12225
|
}
|
|
11424
12226
|
}
|
|
11425
|
-
if (bundleReplaced && !teamWorker?.busy && !updateController.isRunning() && !turnActivity.hasActiveTurn()) {
|
|
12227
|
+
if (bundleReplaced && !teamWorker?.busy && !automationRunner?.busy && !updateController.isRunning() && !turnActivity.hasActiveTurn()) {
|
|
11426
12228
|
const preflight = await preflightNewBundle(bundlePath);
|
|
11427
12229
|
if (preflight.action === "hold") {
|
|
11428
12230
|
if (lastHandoverHold !== preflight.reason) {
|
|
@@ -11432,10 +12234,11 @@ async function startDaemon() {
|
|
|
11432
12234
|
updateController.holdHandover(preflight.reason);
|
|
11433
12235
|
return;
|
|
11434
12236
|
}
|
|
11435
|
-
if (teamWorker?.busy || updateController.isRunning() || turnActivity.hasActiveTurn()) return;
|
|
12237
|
+
if (teamWorker?.busy || automationRunner?.busy || updateController.isRunning() || turnActivity.hasActiveTurn()) return;
|
|
11436
12238
|
lastHandoverHold = null;
|
|
11437
12239
|
logger.debug("[DAEMON RUN] Daemon bundle replaced on disk and verified, handing off to new daemon");
|
|
11438
12240
|
teamWorker?.stop();
|
|
12241
|
+
automationRunner?.stop();
|
|
11439
12242
|
clearInterval(restartOnStaleVersionAndHeartbeat);
|
|
11440
12243
|
clearInterval(cliUpdateInterval);
|
|
11441
12244
|
stopAgentVersionChecks();
|
|
@@ -11487,6 +12290,14 @@ async function startDaemon() {
|
|
|
11487
12290
|
live: (id) => [...pidToTrackedSession.values()].some((s) => s.happySessionId === id && isPidAlive(s.pid)),
|
|
11488
12291
|
log: (message) => logger.debug(`[DAEMON RUN] ${message}`)
|
|
11489
12292
|
});
|
|
12293
|
+
automationRunner = createAutomationRunner({
|
|
12294
|
+
machineId,
|
|
12295
|
+
token: credentials.token,
|
|
12296
|
+
spawn: spawnSession,
|
|
12297
|
+
live: (id) => [...pidToTrackedSession.values()].some((s) => s.happySessionId === id && isPidAlive(s.pid)),
|
|
12298
|
+
turnActive: (id) => turnActivity.activeSessions().includes(id),
|
|
12299
|
+
log: (message) => logger.debug(`[DAEMON RUN] ${message}`)
|
|
12300
|
+
});
|
|
11490
12301
|
void apiMachine.updateMachineMetadata((current) => ({
|
|
11491
12302
|
...current ?? initialMachineMetadata,
|
|
11492
12303
|
teamsVersion: 1,
|
|
@@ -11495,6 +12306,7 @@ async function startDaemon() {
|
|
|
11495
12306
|
const cleanupAndShutdown = async (source, errorMessage) => {
|
|
11496
12307
|
logger.debug(`[DAEMON RUN] Starting proper cleanup (source: ${source}, errorMessage: ${errorMessage})...`);
|
|
11497
12308
|
teamWorker?.stop();
|
|
12309
|
+
automationRunner?.stop({ killScripts: true });
|
|
11498
12310
|
if (restartOnStaleVersionAndHeartbeat) {
|
|
11499
12311
|
clearInterval(restartOnStaleVersionAndHeartbeat);
|
|
11500
12312
|
logger.debug("[DAEMON RUN] Health check interval cleared");
|
|
@@ -11999,6 +12811,7 @@ function createMcpServer(handlers, options, sessionId) {
|
|
|
11999
12811
|
version: "1.0.0"
|
|
12000
12812
|
});
|
|
12001
12813
|
registerTeamsTools(mcp, sessionId);
|
|
12814
|
+
registerAutomationTools(mcp);
|
|
12002
12815
|
mcp.registerTool("change_title", {
|
|
12003
12816
|
description: "Change the title of the current chat session",
|
|
12004
12817
|
title: "Change Chat Title",
|
|
@@ -12241,6 +13054,7 @@ async function startHappyServer(client, options) {
|
|
|
12241
13054
|
PREVIEW_TOOL_NAME,
|
|
12242
13055
|
REPORT_PROGRESS_TOOL_NAME,
|
|
12243
13056
|
...TEAM_TOOL_NAMES,
|
|
13057
|
+
...AUTOMATION_TOOL_NAMES,
|
|
12244
13058
|
...options?.assistant && !process.env.VH_TEAM_SCOPE_FILE ? ASSISTANT_TOOL_NAMES : []
|
|
12245
13059
|
],
|
|
12246
13060
|
stop: () => {
|
|
@@ -12577,7 +13391,7 @@ function registerSideQuestionHandler(rpc, deps) {
|
|
|
12577
13391
|
const retainMs = deps.retainMs ?? SIDE_QUESTION_RETAIN_MS;
|
|
12578
13392
|
const maxRunMs = deps.maxRunMs ?? SIDE_QUESTION_MAX_RUN_MS;
|
|
12579
13393
|
const fork = deps.run ?? (async (input) => {
|
|
12580
|
-
const { query } = await import('./index-
|
|
13394
|
+
const { query } = await import('./index-CN7OF2cU.mjs');
|
|
12581
13395
|
return runSideQuestion(query, input);
|
|
12582
13396
|
});
|
|
12583
13397
|
const run = async (input, live) => {
|
|
@@ -17865,7 +18679,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
17865
18679
|
return;
|
|
17866
18680
|
} else if (subcommand === "install-terminal-hooks") {
|
|
17867
18681
|
try {
|
|
17868
|
-
const { installTerminalHooks, parseTerminalHooksArgs, TERMINAL_HOOKS_HELP } = await import('./installTerminalHooks-
|
|
18682
|
+
const { installTerminalHooks, parseTerminalHooksArgs, TERMINAL_HOOKS_HELP } = await import('./installTerminalHooks-VvmlO6Ue.mjs');
|
|
17869
18683
|
const command = parseTerminalHooksArgs(args.slice(1));
|
|
17870
18684
|
if (command.action === "help") {
|
|
17871
18685
|
console.log(TERMINAL_HOOKS_HELP);
|
|
@@ -17882,7 +18696,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
17882
18696
|
}
|
|
17883
18697
|
} else if (subcommand === "agent-home") {
|
|
17884
18698
|
try {
|
|
17885
|
-
const { parseAgentHomeArgs, runAgentHomeCommand } = await import('./agentHome-
|
|
18699
|
+
const { parseAgentHomeArgs, runAgentHomeCommand } = await import('./agentHome-CCYfs8Ec.mjs');
|
|
17886
18700
|
await runAgentHomeCommand(parseAgentHomeArgs(args.slice(1)));
|
|
17887
18701
|
process.exit(0);
|
|
17888
18702
|
} catch (error) {
|
|
@@ -17891,7 +18705,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
17891
18705
|
}
|
|
17892
18706
|
} else if (subcommand === "spawn") {
|
|
17893
18707
|
try {
|
|
17894
|
-
const { handleSpawnCommand } = await import('./spawn-
|
|
18708
|
+
const { handleSpawnCommand } = await import('./spawn-BOlb5y1_.mjs');
|
|
17895
18709
|
await handleSpawnCommand(args.slice(1));
|
|
17896
18710
|
} catch (error) {
|
|
17897
18711
|
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|
|
@@ -17903,7 +18717,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
17903
18717
|
return;
|
|
17904
18718
|
} else if (subcommand === "sessions") {
|
|
17905
18719
|
try {
|
|
17906
|
-
const { handleSessionsCommand } = await import('./sessions-
|
|
18720
|
+
const { handleSessionsCommand } = await import('./sessions-DXJEcUg6.mjs');
|
|
17907
18721
|
await handleSessionsCommand(args.slice(1));
|
|
17908
18722
|
} catch (error) {
|
|
17909
18723
|
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|
|
@@ -17915,7 +18729,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
17915
18729
|
return;
|
|
17916
18730
|
} else if (subcommand === "send") {
|
|
17917
18731
|
try {
|
|
17918
|
-
const { handleSendCommand } = await import('./send-
|
|
18732
|
+
const { handleSendCommand } = await import('./send-C44kcXz5.mjs');
|
|
17919
18733
|
await handleSendCommand(args.slice(1));
|
|
17920
18734
|
} catch (error) {
|
|
17921
18735
|
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|
|
@@ -18021,9 +18835,9 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
18021
18835
|
if (geminiSubcommand === "project" && args[2] === "set" && args[3]) {
|
|
18022
18836
|
const projectId = args[3];
|
|
18023
18837
|
try {
|
|
18024
|
-
const { saveGoogleCloudProjectToConfig } = await import('./config-
|
|
18025
|
-
const { readCredentialsForConfiguredRelay: readCredentialsForConfiguredRelay2 } = await import('./limits-
|
|
18026
|
-
const { ApiClient: ApiClient2 } = await import('./types-
|
|
18838
|
+
const { saveGoogleCloudProjectToConfig } = await import('./config-DbKpQpdL.mjs');
|
|
18839
|
+
const { readCredentialsForConfiguredRelay: readCredentialsForConfiguredRelay2 } = await import('./limits-CKR_HJnO.mjs').then(function (n) { return n.al; });
|
|
18840
|
+
const { ApiClient: ApiClient2 } = await import('./types-xKrxbCRv.mjs').then(function (n) { return n.ah; });
|
|
18027
18841
|
let userEmail = void 0;
|
|
18028
18842
|
try {
|
|
18029
18843
|
const credentials = await readCredentialsForConfiguredRelay2();
|
|
@@ -18054,7 +18868,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
18054
18868
|
}
|
|
18055
18869
|
if (geminiSubcommand === "project" && args[2] === "get") {
|
|
18056
18870
|
try {
|
|
18057
|
-
const { readGeminiLocalConfig } = await import('./config-
|
|
18871
|
+
const { readGeminiLocalConfig } = await import('./config-DbKpQpdL.mjs');
|
|
18058
18872
|
const config = readGeminiLocalConfig();
|
|
18059
18873
|
if (config.googleCloudProject) {
|
|
18060
18874
|
console.log(`Current Google Cloud Project: ${config.googleCloudProject}`);
|
|
@@ -18094,7 +18908,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
18094
18908
|
process.exit(0);
|
|
18095
18909
|
}
|
|
18096
18910
|
try {
|
|
18097
|
-
const { runGemini } = await import('./runGemini-
|
|
18911
|
+
const { runGemini } = await import('./runGemini-D5N11LxO.mjs');
|
|
18098
18912
|
let startedBy = void 0;
|
|
18099
18913
|
for (let i = 1; i < args.length; i++) {
|
|
18100
18914
|
if (args[i] === "--started-by") {
|
|
@@ -18117,7 +18931,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
18117
18931
|
} else if (subcommand === "pi") {
|
|
18118
18932
|
if (args[1] === "--terminal") {
|
|
18119
18933
|
try {
|
|
18120
|
-
const { runPiTerminal } = await import('./piTerminal-
|
|
18934
|
+
const { runPiTerminal } = await import('./piTerminal-BLSXK1u2.mjs');
|
|
18121
18935
|
process.exitCode = await runPiTerminal(args.slice(2));
|
|
18122
18936
|
} catch (error) {
|
|
18123
18937
|
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|
|
@@ -18134,7 +18948,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
18134
18948
|
}
|
|
18135
18949
|
};
|
|
18136
18950
|
try {
|
|
18137
|
-
const { runAcp, resolveAcpAgentConfig } = await import('./index-
|
|
18951
|
+
const { runAcp, resolveAcpAgentConfig } = await import('./index-DahaLbu4.mjs');
|
|
18138
18952
|
const parsed = parsePiRunnerArgs(args.slice(1));
|
|
18139
18953
|
const resolved = resolveAcpAgentConfig(["pi", ...parsed.passthrough]);
|
|
18140
18954
|
const { credentials } = await authAndSetupMachineIfNeeded();
|
|
@@ -18162,7 +18976,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
18162
18976
|
return;
|
|
18163
18977
|
} else if (subcommand === "acp") {
|
|
18164
18978
|
try {
|
|
18165
|
-
const { runAcp, resolveAcpAgentConfig } = await import('./index-
|
|
18979
|
+
const { runAcp, resolveAcpAgentConfig } = await import('./index-DahaLbu4.mjs');
|
|
18166
18980
|
let startedBy = void 0;
|
|
18167
18981
|
let verbose = false;
|
|
18168
18982
|
const acpArgs = [];
|
|
@@ -18202,7 +19016,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
18202
19016
|
return;
|
|
18203
19017
|
} else if (subcommand === "openclaw") {
|
|
18204
19018
|
try {
|
|
18205
|
-
const { runOpenClaw } = await import('./runOpenClaw-
|
|
19019
|
+
const { runOpenClaw } = await import('./runOpenClaw-CCpyKbrn.mjs');
|
|
18206
19020
|
let startedBy = void 0;
|
|
18207
19021
|
let verbose = false;
|
|
18208
19022
|
let gatewayUrl;
|
|
@@ -18253,7 +19067,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
18253
19067
|
return;
|
|
18254
19068
|
} else if (subcommand === "todo") {
|
|
18255
19069
|
try {
|
|
18256
|
-
const { handleTodoCommand } = await import('./todo-
|
|
19070
|
+
const { handleTodoCommand } = await import('./todo-nzsqGZqp.mjs');
|
|
18257
19071
|
await handleTodoCommand(args.slice(1));
|
|
18258
19072
|
} catch (error) {
|
|
18259
19073
|
console.error(JSON.stringify({ error: error instanceof Error ? error.message : "Todo command failed" }));
|
|
@@ -18262,16 +19076,28 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
18262
19076
|
return;
|
|
18263
19077
|
} else if (subcommand === "teams") {
|
|
18264
19078
|
try {
|
|
18265
|
-
const { handleTeamsCommand } = await import('./teams-
|
|
19079
|
+
const { handleTeamsCommand } = await import('./teams-Bs8lI887.mjs');
|
|
18266
19080
|
await handleTeamsCommand(args.slice(1));
|
|
18267
19081
|
} catch (error) {
|
|
18268
19082
|
console.error(error instanceof Error ? error.message : "Teams command failed");
|
|
18269
19083
|
process.exitCode = 1;
|
|
18270
19084
|
}
|
|
18271
19085
|
return;
|
|
19086
|
+
} else if (subcommand === "auto") {
|
|
19087
|
+
try {
|
|
19088
|
+
const { handleAutoCommand } = await import('./auto-DV5emt1-.mjs');
|
|
19089
|
+
await handleAutoCommand(args.slice(1));
|
|
19090
|
+
} catch (error) {
|
|
19091
|
+
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Automation command failed");
|
|
19092
|
+
if (process.env.DEBUG) {
|
|
19093
|
+
console.error(error);
|
|
19094
|
+
}
|
|
19095
|
+
process.exitCode = 1;
|
|
19096
|
+
}
|
|
19097
|
+
return;
|
|
18272
19098
|
} else if (subcommand === "mcp" && (args.length === 1 || args.length === 2 && args[1] === "--terminal-tools")) {
|
|
18273
19099
|
try {
|
|
18274
|
-
const { handleMcpCommand } = await import('./mcp-
|
|
19100
|
+
const { handleMcpCommand } = await import('./mcp-D4dAo5BQ.mjs');
|
|
18275
19101
|
await handleMcpCommand(args[1] === "--terminal-tools");
|
|
18276
19102
|
} catch (error) {
|
|
18277
19103
|
process.stderr.write(`[very-happy mcp] Fatal: ${error instanceof Error ? error.message : String(error)}
|
|
@@ -18505,6 +19331,8 @@ ${chalk.bold("Usage:")}
|
|
|
18505
19331
|
conversations on this machine (agent-home --help)
|
|
18506
19332
|
very-happy connect Connect AI vendor API keys
|
|
18507
19333
|
very-happy todo Built-in Todos and official agent skill (todo --help)
|
|
19334
|
+
very-happy auto Scheduled / triggered automations: spawn, sticky
|
|
19335
|
+
sessions or scripts with tracked runs (auto --help)
|
|
18508
19336
|
very-happy sandbox Configure and manage OS-level sandboxing
|
|
18509
19337
|
very-happy notify Send push notification
|
|
18510
19338
|
very-happy mcp Stdio MCP server exposing copy_to_clipboard for a
|
|
@@ -18639,4 +19467,4 @@ ${chalk.bold("Examples:")}
|
|
|
18639
19467
|
}
|
|
18640
19468
|
}
|
|
18641
19469
|
|
|
18642
|
-
export { ALLOWED_SPAWN_PERMISSION_MODES as A,
|
|
19470
|
+
export { CHANGE_TITLE_INSTRUCTION as $, ALLOWED_SPAWN_PERMISSION_MODES as A, createSessionMetadata as B, setupOfflineReconnection as C, DEFAULT_RECENT_LIMIT as D, StreamRelay as E, MessageQueue2 as F, GEMINI_MODEL_ENV as G, hashObject as H, startHappyServer as I, registerAgentAttachmentDownloads as J, TitleGenerator as K, registerKillSessionHandler as L, MAX_READ_LIMIT as M, CLAUDE_ATTACHMENT_KINDS as N, stageClaudeAttachments as O, PI_TEAMS_EXTENSION as P, appendStagedAttachmentsToPrompt as Q, BasePermissionHandler as R, SPAWN_AGENTS as S, TurnWaitTimeoutError as T, writeSessionModeFile as U, normalizeAcpPermissionMode as V, removeSessionModeFile as W, GEMINI_API_KEY_ENV as X, GOOGLE_API_KEY_ENV as Y, BaseReasoningProcessor as Z, MessageBuffer as _, listSessions as a, resolvePermissionRequest as b, archiveSession as c, sendUserMessage as d, sessionWebUrl as e, DEFAULT_GEMINI_MODEL as f, describeTrigger as g, describeAction as h, sanitizeSpawnPermissionMode as i, tmuxSupportsSessionEnv as j, readSessionMetadata as k, listAccountSessions as l, resolveSpawnPermissionMode as m, resolveFirstMessageMeta as n, isValidSpawnOrigin as o, parseDuration as p, query as q, readSessionTranscript as r, stopSession as s, triggerFromFlags as t, waitForSessionKey as u, checkPreviewPath as v, waitForTurnEnd as w, registerAssistantSessionTools as x, TEAM_SKILL as y, initialMachineMetadata as z };
|