very-happy-cli 0.2.151 → 0.2.153
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-blE9IEod.cjs → AcpBackend-DSt8nTYm.cjs} +2 -2
- package/dist/{AcpBackend-BjTA5gUB.mjs → AcpBackend-YDGv4E-g.mjs} +2 -2
- package/dist/{AcpSessionManager-D-9mrp40.mjs → AcpSessionManager-Cxf5HQX1.mjs} +1 -1
- package/dist/{AcpSessionManager-bF8PYu_B.cjs → AcpSessionManager-uov6I9C8.cjs} +9 -9
- package/dist/agentGuidance-BM4kMNyr.cjs +553 -0
- package/dist/agentGuidance-C8unGWp7.mjs +529 -0
- package/dist/{agentHome-D6MSfwnp.cjs → agentHome-BJ7bde35.cjs} +4 -4
- package/dist/{agentHome-CeFDNzvq.mjs → agentHome-D68Xh2ae.mjs} +4 -4
- package/dist/auto-C-90ojIH.cjs +472 -0
- package/dist/auto-CcXxfqkC.mjs +466 -0
- package/dist/codex/happyMcpStdioBridge.cjs +4 -2
- package/dist/codex/happyMcpStdioBridge.mjs +4 -2
- package/dist/{config-C-A1XF62.mjs → config-CHJ189UJ.mjs} +6 -6
- package/dist/{config-Cadw1sAk.cjs → config-D23kF0_u.cjs} +6 -6
- package/dist/{index-DYQgcgL2.mjs → index-BXA9fxbK.mjs} +6 -6
- package/dist/{index-CIyjJy8D.mjs → index-BacwoXzs.mjs} +748 -55
- package/dist/{index-DEp6CmmS.mjs → index-BgYZGODP.mjs} +20 -20
- package/dist/{index-vyRqLjDv.cjs → index-D49WvbDT.cjs} +6 -6
- package/dist/{index-BKGspSWk.cjs → index-DEOVn46R.cjs} +21 -21
- package/dist/{index-CqtfRzug.cjs → index-DHrwHxHf.cjs} +805 -105
- package/dist/index.cjs +5 -5
- package/dist/index.mjs +5 -5
- package/dist/install-BiOXBnnS.cjs +118 -0
- package/dist/install-Ca7_Z3bV.mjs +114 -0
- package/dist/{installPiTools-C-Lxv4Bt.mjs → installPiTools-Cj7yU9Fc.mjs} +2 -1
- package/dist/{installPiTools-BKnXjrVu.cjs → installPiTools-DuZ6jZvX.cjs} +3 -2
- package/dist/{installTerminalHooks-Bf_adbl3.cjs → installTerminalHooks-BF0kL0ae.cjs} +8 -8
- package/dist/{installTerminalHooks-Ch_yTtEA.mjs → installTerminalHooks-Bpl32K75.mjs} +7 -7
- package/dist/lib.cjs +4 -4
- package/dist/lib.mjs +4 -4
- package/dist/limits-BtAuKRl2.mjs +2080 -0
- package/dist/limits-DY_sFA3J.cjs +2175 -0
- package/dist/{mcp-zu8n8TkR.mjs → mcp-DQkmk4VV.mjs} +15 -8
- package/dist/{mcp-CMxEZ6-0.cjs → mcp-JpqI2POW.cjs} +15 -7
- package/dist/piExtension.d.mts +4 -1
- package/dist/piExtension.mjs +24 -1
- package/dist/{piRuntime-Cu6cXZ6N.mjs → piRuntime-DBpe12NN.mjs} +2 -2
- package/dist/{piRuntime-D3ny0mVg.cjs → piRuntime-DRbbeQ72.cjs} +2 -2
- package/dist/{piTerminal-M677UP7y.mjs → piTerminal-BoHvPEZ_.mjs} +8 -7
- package/dist/{piTerminal-kDSd_r9X.cjs → piTerminal-qdJ9KZVL.cjs} +8 -7
- package/dist/{runGemini-DuwdVV1O.cjs → runGemini-CtOEFvhf.cjs} +9 -9
- package/dist/{runGemini-DvJwQVGp.mjs → runGemini-YqH7a-7O.mjs} +8 -8
- package/dist/{runOpenClaw-BG82bF18.mjs → runOpenClaw-DSSyFQBb.mjs} +7 -7
- package/dist/{runOpenClaw-BsvtCE_H.cjs → runOpenClaw-DoqZXBvL.cjs} +7 -7
- package/dist/{send-aUkjhey7.cjs → send-9075kHZP.cjs} +6 -6
- package/dist/{send-o6WM-1G8.mjs → send-Cs-RwqNf.mjs} +6 -6
- package/dist/{sessions-DlU3foY_.mjs → sessions-iBnYzVUK.mjs} +6 -6
- package/dist/{sessions-B3vP18kz.cjs → sessions-qULzUzlI.cjs} +6 -6
- package/dist/{spawn-DxHIIqhi.mjs → spawn-C6fRkedx.mjs} +7 -32
- package/dist/{spawn-DH6_p4-f.cjs → spawn-od9V23cc.cjs} +8 -33
- package/dist/{teams-DoAA_CcN.mjs → teams-CkrWQ4uM.mjs} +16 -73
- package/dist/{teams-BW8D1n3o.cjs → teams-CrCzU-AV.cjs} +14 -71
- package/dist/terminalTitleSuggest-DKgX3osW.mjs +38 -0
- package/dist/terminalTitleSuggest-Dji8I0Jy.cjs +45 -0
- package/dist/{todo-Bcc2XxGh.cjs → todo-BCT_ZPuV.cjs} +4 -23
- package/dist/{todo-BhJbG5eq.mjs → todo-aCZmSt27.mjs} +2 -21
- package/dist/{types-BN3eiipi.mjs → types-BvqOVUOW.mjs} +214 -1022
- package/dist/{types-CPubg05Z.cjs → types-CUCU7Vw8.cjs} +43 -863
- package/package.json +7 -7
- package/dist/agentGuidance-BXa9ikrZ.cjs +0 -267
- package/dist/agentGuidance-Dvo3cZMW.mjs +0 -251
- package/dist/limits-DS-bnYYp.mjs +0 -1165
- package/dist/limits-DwcY3kyi.cjs +0 -1236
- package/dist/projectPath-CSpqWdxp.mjs +0 -10
- package/dist/projectPath-ChLhdOyt.cjs +0 -13
|
@@ -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 {
|
|
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-BvqOVUOW.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-BtAuKRl2.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
|
-
import { p as projectPath } from './
|
|
11
|
-
import {
|
|
10
|
+
import { p as projectPath } from './terminalTitleSuggest-DKgX3osW.mjs';
|
|
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-C8unGWp7.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';
|
|
@@ -5380,8 +5380,8 @@ ${typeLabels[type] || type}:`));
|
|
|
5380
5380
|
}
|
|
5381
5381
|
console.log(chalk.gray(" An external claude command is only needed for native terminal/mirror use."));
|
|
5382
5382
|
try {
|
|
5383
|
-
const { resolveAgentHomesForCli } = await import('./agentHome-
|
|
5384
|
-
const { describeAgentHome } = await import('./types-
|
|
5383
|
+
const { resolveAgentHomesForCli } = await import('./agentHome-D68Xh2ae.mjs');
|
|
5384
|
+
const { describeAgentHome } = await import('./types-BvqOVUOW.mjs').then(function (n) { return n.ag; });
|
|
5385
5385
|
const homes = await resolveAgentHomesForCli();
|
|
5386
5386
|
console.log(chalk.green(`\u2713 Claude config dir: ${describeAgentHome(homes.claudeConfigDir)}`));
|
|
5387
5387
|
console.log(chalk.green(`\u2713 Codex home: ${describeAgentHome(homes.codexHome)}`));
|
|
@@ -6208,7 +6208,7 @@ const DENIED_BASENAME_PATTERNS = [
|
|
|
6208
6208
|
const DENIED_PATH_SUFFIXES = [
|
|
6209
6209
|
`${sep}.git${sep}config`
|
|
6210
6210
|
];
|
|
6211
|
-
function expandHome(input, home) {
|
|
6211
|
+
function expandHome$1(input, home) {
|
|
6212
6212
|
if (input === "~") return home;
|
|
6213
6213
|
if (input.startsWith(`~${sep}`) || input.startsWith("~/")) {
|
|
6214
6214
|
return resolve(home, input.slice(2));
|
|
@@ -6231,7 +6231,7 @@ function checkPreviewPath(input, opts) {
|
|
|
6231
6231
|
if (input.includes("\0")) {
|
|
6232
6232
|
return { resolved: "", deniedReason: "path contains a NUL byte" };
|
|
6233
6233
|
}
|
|
6234
|
-
const expanded = expandHome(input.trim(), home);
|
|
6234
|
+
const expanded = expandHome$1(input.trim(), home);
|
|
6235
6235
|
const resolved = isAbsolute(expanded) ? resolve(expanded) : resolve(cwd, expanded);
|
|
6236
6236
|
for (const dir of DENIED_HOME_DIRS) {
|
|
6237
6237
|
if (isUnder(resolved, resolve(home, dir))) {
|
|
@@ -9358,6 +9358,20 @@ export default async function(pi) {
|
|
|
9358
9358
|
return data.result;
|
|
9359
9359
|
}
|
|
9360
9360
|
await rpc('initialize', {protocolVersion:'2024-11-05',capabilities:{},clientInfo:{name:'very-happy-pi-teams',version:'1'}});
|
|
9361
|
+
if (!managed && pi.setSessionName) {
|
|
9362
|
+
// B-500: name the pi session from its first prompt (managed sessions are titled by TitleGenerator).
|
|
9363
|
+
let titleArmed = false, generation = 0;
|
|
9364
|
+
pi.on('session_start', async () => { generation += 1; titleArmed = !pi.getSessionName?.(); });
|
|
9365
|
+
pi.on('before_agent_start', async (event) => {
|
|
9366
|
+
const prompt = event && typeof event.prompt === 'string' ? event.prompt : '';
|
|
9367
|
+
if (!titleArmed || !prompt.trim()) return;
|
|
9368
|
+
titleArmed = false;
|
|
9369
|
+
const expected = generation;
|
|
9370
|
+
rpc('very-happy/terminal-title-suggest', {prompt}).then(result => {
|
|
9371
|
+
if (result && result.title && expected === generation && !pi.getSessionName?.()) pi.setSessionName(result.title);
|
|
9372
|
+
}).catch(() => {});
|
|
9373
|
+
});
|
|
9374
|
+
}
|
|
9361
9375
|
const available = await rpc('tools/list', {});
|
|
9362
9376
|
for (const tool of available.tools || []) {
|
|
9363
9377
|
pi.registerTool({name:tool.name,label:tool.name,description:tool.description || tool.name,parameters:tool.inputSchema,
|
|
@@ -9517,11 +9531,14 @@ function agentPiece(body) {
|
|
|
9517
9531
|
}
|
|
9518
9532
|
return null;
|
|
9519
9533
|
}
|
|
9520
|
-
function
|
|
9534
|
+
function userPromptText(body) {
|
|
9535
|
+
return isUserPrompt(body) ? body.content.text : null;
|
|
9536
|
+
}
|
|
9537
|
+
function analyzeLatestTurn(entries, options = {}) {
|
|
9521
9538
|
const lastSeq = entries.length > 0 ? entries[entries.length - 1].seq : 0;
|
|
9522
9539
|
let userIndex = -1;
|
|
9523
9540
|
for (let i = entries.length - 1; i >= 0; i--) {
|
|
9524
|
-
if (isUserPrompt(entries[i].body)) {
|
|
9541
|
+
if (isUserPrompt(entries[i].body) && (!options.anchor || options.anchor(entries[i].body))) {
|
|
9525
9542
|
userIndex = i;
|
|
9526
9543
|
break;
|
|
9527
9544
|
}
|
|
@@ -9535,6 +9552,10 @@ function analyzeLatestTurn(entries) {
|
|
|
9535
9552
|
const emitsStarts = entries.some((entry) => sessionEnvelope(entry.body)?.ev?.t === "turn-start");
|
|
9536
9553
|
for (let i = userIndex + 1; i < entries.length; i++) {
|
|
9537
9554
|
const body = entries[i].body;
|
|
9555
|
+
if (options.anchor && isUserPrompt(body)) {
|
|
9556
|
+
if (ended) break;
|
|
9557
|
+
continue;
|
|
9558
|
+
}
|
|
9538
9559
|
const env = sessionEnvelope(body);
|
|
9539
9560
|
if (env?.ev?.t === "turn-start") {
|
|
9540
9561
|
lastMarker = "start";
|
|
@@ -10027,7 +10048,7 @@ function readReceipt(home, id) {
|
|
|
10027
10048
|
throw error;
|
|
10028
10049
|
}
|
|
10029
10050
|
}
|
|
10030
|
-
function writeReceipt(home, receipt) {
|
|
10051
|
+
function writeReceipt$1(home, receipt) {
|
|
10031
10052
|
ensurePrivateDirectorySync(join(home, "teams", "effects"));
|
|
10032
10053
|
const target = receiptPath(home, receipt.operationId);
|
|
10033
10054
|
const tmp = `${target}.${process.pid}.tmp`;
|
|
@@ -10104,7 +10125,7 @@ function createTeamWorker(deps) {
|
|
|
10104
10125
|
const action = async (teamId, requestId, value) => (await http.post(`/v1/teams/${encodeURIComponent(teamId)}/actions`, { requestId, action: value })).data;
|
|
10105
10126
|
async function complete(op, receipt) {
|
|
10106
10127
|
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 } : {} });
|
|
10107
|
-
writeReceipt(home, { ...receipt, phase: "completed" });
|
|
10128
|
+
writeReceipt$1(home, { ...receipt, phase: "completed" });
|
|
10108
10129
|
}
|
|
10109
10130
|
async function execute(op) {
|
|
10110
10131
|
let receipt = readReceipt(home, op.id);
|
|
@@ -10121,7 +10142,7 @@ function createTeamWorker(deps) {
|
|
|
10121
10142
|
op = claimed.operation;
|
|
10122
10143
|
credential = claimed.credential;
|
|
10123
10144
|
receipt = { operationId: op.id, teamId: op.teamId, claimId: op.claimId, phase: "prepared" };
|
|
10124
|
-
writeReceipt(home, receipt);
|
|
10145
|
+
writeReceipt$1(home, receipt);
|
|
10125
10146
|
}
|
|
10126
10147
|
if (receipt.phase === "completed") return;
|
|
10127
10148
|
try {
|
|
@@ -10134,7 +10155,7 @@ function createTeamWorker(deps) {
|
|
|
10134
10155
|
if (!task) throw new Error("Task state unavailable; refusing to start");
|
|
10135
10156
|
if (task.currentAttemptId !== op.attemptId || ["done", "cancelled"].includes(task.status)) {
|
|
10136
10157
|
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" });
|
|
10137
|
-
writeReceipt(home, { ...receipt, phase: "completed" });
|
|
10158
|
+
writeReceipt$1(home, { ...receipt, phase: "completed" });
|
|
10138
10159
|
return;
|
|
10139
10160
|
}
|
|
10140
10161
|
if (!op.directory) throw new Error("No repository selected for worker");
|
|
@@ -10144,17 +10165,17 @@ function createTeamWorker(deps) {
|
|
|
10144
10165
|
const scopeFile = join(scopes, `${op.id}.json`);
|
|
10145
10166
|
writePrivateFileSync(scopeFile, JSON.stringify({ serverUrl: server, scopeToken: credential.token, teamId: op.teamId, botId: op.botId, taskId: op.taskId, attemptId: op.attemptId }));
|
|
10146
10167
|
receipt = { ...receipt, ...resource, scopeFile, phase: "spawning" };
|
|
10147
|
-
writeReceipt(home, receipt);
|
|
10168
|
+
writeReceipt$1(home, receipt);
|
|
10148
10169
|
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 } });
|
|
10149
10170
|
if (spawned.type !== "success") throw new Error(spawned.type === "error" ? spawned.errorMessage : "Directory approval required");
|
|
10150
10171
|
receipt = { ...receipt, sessionId: spawned.sessionId, phase: "spawned" };
|
|
10151
|
-
writeReceipt(home, receipt);
|
|
10172
|
+
writeReceipt$1(home, receipt);
|
|
10152
10173
|
}
|
|
10153
10174
|
if (receipt.phase === "spawning") {
|
|
10154
10175
|
const matches = Object.entries(readPersistedSessions()).filter(([, entry]) => entry.metadata?.teamOperationId === op.id);
|
|
10155
10176
|
if (matches.length !== 1) throw new Error("Spawn outcome unknown; reconcile the worker before retrying");
|
|
10156
10177
|
receipt = { ...receipt, sessionId: matches[0][0], phase: "spawned" };
|
|
10157
|
-
writeReceipt(home, receipt);
|
|
10178
|
+
writeReceipt$1(home, receipt);
|
|
10158
10179
|
}
|
|
10159
10180
|
if (receipt.phase === "spawned") {
|
|
10160
10181
|
const current = (await http.get(`/v1/teams/${encodeURIComponent(op.teamId)}`)).data.team;
|
|
@@ -10169,14 +10190,14 @@ function createTeamWorker(deps) {
|
|
|
10169
10190
|
const key = await waitForSessionKey(receipt.sessionId, 1e4);
|
|
10170
10191
|
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 } : {} });
|
|
10171
10192
|
receipt = { ...receipt, phase: "delivered" };
|
|
10172
|
-
writeReceipt(home, receipt);
|
|
10193
|
+
writeReceipt$1(home, receipt);
|
|
10173
10194
|
}
|
|
10174
10195
|
await complete(op, receipt);
|
|
10175
10196
|
} else {
|
|
10176
10197
|
const team = (await http.get(`/v1/teams/${encodeURIComponent(op.teamId)}`)).data.team;
|
|
10177
10198
|
const latest = team.operations.find((candidate) => candidate.id === op.id);
|
|
10178
10199
|
if (latest?.status === "completed") {
|
|
10179
|
-
writeReceipt(home, { ...receipt, phase: "completed" });
|
|
10200
|
+
writeReceipt$1(home, { ...receipt, phase: "completed" });
|
|
10180
10201
|
return;
|
|
10181
10202
|
}
|
|
10182
10203
|
const bot = team.bots.find((candidate) => candidate.id === op.botId);
|
|
@@ -10306,6 +10327,643 @@ Use the teams tools to inspect current task state before acting. This message is
|
|
|
10306
10327
|
} };
|
|
10307
10328
|
}
|
|
10308
10329
|
|
|
10330
|
+
function resolveSpawnPermissionMode(agent, explicit) {
|
|
10331
|
+
return explicit ?? getAgentCodeDefaults(agent ?? "claude").permissionMode;
|
|
10332
|
+
}
|
|
10333
|
+
function outboundPermissionMode(agent, mode) {
|
|
10334
|
+
const key = normalizeAgentKey(agent ?? "claude");
|
|
10335
|
+
if ((key === "claude" || key === "pi") && mode === "yolo") return "bypassPermissions";
|
|
10336
|
+
return mode;
|
|
10337
|
+
}
|
|
10338
|
+
function resolveFirstMessageMeta(input) {
|
|
10339
|
+
const meta = {
|
|
10340
|
+
permissionMode: outboundPermissionMode(input.agent, input.permissionMode)
|
|
10341
|
+
};
|
|
10342
|
+
if (input.explicitModel !== void 0) {
|
|
10343
|
+
meta.model = input.explicitModel === "default" ? null : input.explicitModel;
|
|
10344
|
+
}
|
|
10345
|
+
if (normalizeAgentKey(input.agent ?? "claude") !== "claude") return meta;
|
|
10346
|
+
if (meta.model === void 0) {
|
|
10347
|
+
const model = getAgentCodeDefaults("claude").modelMode;
|
|
10348
|
+
const runnable = !isClaudeOpus55Model(model) || supportsClaudeOpus55({ capabilities: input.capabilities ?? null });
|
|
10349
|
+
meta.model = runnable && model !== "default" ? model : null;
|
|
10350
|
+
}
|
|
10351
|
+
meta.effort = null;
|
|
10352
|
+
return meta;
|
|
10353
|
+
}
|
|
10354
|
+
|
|
10355
|
+
function payloadJson(payload) {
|
|
10356
|
+
if (!payload) return void 0;
|
|
10357
|
+
try {
|
|
10358
|
+
return JSON.parse(payload);
|
|
10359
|
+
} catch {
|
|
10360
|
+
return void 0;
|
|
10361
|
+
}
|
|
10362
|
+
}
|
|
10363
|
+
function lookupPath(root, path) {
|
|
10364
|
+
let current = root;
|
|
10365
|
+
for (const segment of path) {
|
|
10366
|
+
if (current === null || typeof current !== "object") return void 0;
|
|
10367
|
+
current = current[segment];
|
|
10368
|
+
}
|
|
10369
|
+
return current;
|
|
10370
|
+
}
|
|
10371
|
+
function scalar(value) {
|
|
10372
|
+
if (value === void 0 || value === null) return "";
|
|
10373
|
+
if (typeof value === "string") return value;
|
|
10374
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
10375
|
+
try {
|
|
10376
|
+
return JSON.stringify(value);
|
|
10377
|
+
} catch {
|
|
10378
|
+
return "";
|
|
10379
|
+
}
|
|
10380
|
+
}
|
|
10381
|
+
function formatNow(now, tz) {
|
|
10382
|
+
if (!tz) return new Date(now).toISOString();
|
|
10383
|
+
try {
|
|
10384
|
+
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));
|
|
10385
|
+
const get = (type) => parts.find((p) => p.type === type)?.value ?? "";
|
|
10386
|
+
const offset = get("timeZoneName").replace(/^GMT$/, "+00:00").replace(/^GMT/, "");
|
|
10387
|
+
return `${get("year")}-${get("month")}-${get("day")}T${get("hour")}:${get("minute")}:${get("second")}${offset || "Z"}`;
|
|
10388
|
+
} catch {
|
|
10389
|
+
return new Date(now).toISOString();
|
|
10390
|
+
}
|
|
10391
|
+
}
|
|
10392
|
+
function renderTemplate(text, context) {
|
|
10393
|
+
const json = payloadJson(context.payload);
|
|
10394
|
+
return text.replace(/\{\{\s*([a-zA-Z_][a-zA-Z0-9_.-]*)\s*\}\}/g, (whole, name) => {
|
|
10395
|
+
if (name === "payload") return context.payload ?? "";
|
|
10396
|
+
if (name.startsWith("payload.")) return scalar(lookupPath(json, name.slice("payload.".length).split(".")));
|
|
10397
|
+
if (name === "run.id") return context.runId;
|
|
10398
|
+
if (name === "automation.name") return context.automationName;
|
|
10399
|
+
if (name === "now") return formatNow(context.now ?? Date.now(), context.tz);
|
|
10400
|
+
return whole;
|
|
10401
|
+
});
|
|
10402
|
+
}
|
|
10403
|
+
function renderAction(action, context) {
|
|
10404
|
+
const r = (value) => renderTemplate(value, context);
|
|
10405
|
+
if (action.kind === "spawn") return { ...action, prompt: r(action.prompt) };
|
|
10406
|
+
if (action.kind === "send") return { ...action, prompt: r(action.prompt) };
|
|
10407
|
+
return { ...action, command: action.command.map(r), ...action.env ? { env: Object.fromEntries(Object.entries(action.env).map(([k, v]) => [k, r(v)])) } : {} };
|
|
10408
|
+
}
|
|
10409
|
+
function clipText(text, maxChars, keep = "head") {
|
|
10410
|
+
if (text.length <= maxChars) return text;
|
|
10411
|
+
return keep === "head" ? text.slice(0, maxChars) : text.slice(text.length - maxChars);
|
|
10412
|
+
}
|
|
10413
|
+
const DURATION_UNITS = { ms: 1, s: 1e3, m: 6e4, h: 36e5, d: 864e5 };
|
|
10414
|
+
function parseDuration(raw) {
|
|
10415
|
+
const text = raw.trim();
|
|
10416
|
+
if (/^\d+$/.test(text)) return Number(text);
|
|
10417
|
+
const re = /(\d+(?:\.\d+)?)(ms|s|m|h|d)/gy;
|
|
10418
|
+
let total = 0;
|
|
10419
|
+
let matched = 0;
|
|
10420
|
+
let last = 0;
|
|
10421
|
+
let m;
|
|
10422
|
+
while ((m = re.exec(text)) !== null) {
|
|
10423
|
+
total += Number(m[1]) * DURATION_UNITS[m[2]];
|
|
10424
|
+
matched++;
|
|
10425
|
+
last = re.lastIndex;
|
|
10426
|
+
}
|
|
10427
|
+
if (matched === 0 || last !== text.length) throw new Error(`Invalid duration: ${raw} (use e.g. 90s, 5m, 1h30m, 2d)`);
|
|
10428
|
+
return Math.round(total);
|
|
10429
|
+
}
|
|
10430
|
+
function formatDuration(ms) {
|
|
10431
|
+
if (ms % 864e5 === 0) return `${ms / 864e5}d`;
|
|
10432
|
+
if (ms % 36e5 === 0) return `${ms / 36e5}h`;
|
|
10433
|
+
if (ms % 6e4 === 0) return `${ms / 6e4}m`;
|
|
10434
|
+
if (ms % 1e3 === 0) return `${ms / 1e3}s`;
|
|
10435
|
+
return `${ms}ms`;
|
|
10436
|
+
}
|
|
10437
|
+
function triggerFromFlags(flags, now = Date.now()) {
|
|
10438
|
+
const given = [flags.cron !== void 0, flags.every !== void 0, flags.at !== void 0, flags.manual === true].filter(Boolean).length;
|
|
10439
|
+
if (given === 0) {
|
|
10440
|
+
if (flags.tz !== void 0) throw new Error("--tz only applies with --cron");
|
|
10441
|
+
return void 0;
|
|
10442
|
+
}
|
|
10443
|
+
if (given > 1) throw new Error("Use exactly one of --cron, --every, --at, --manual");
|
|
10444
|
+
if (flags.cron !== void 0) {
|
|
10445
|
+
if (!flags.tz) throw new Error("--cron requires --tz <IANA zone> (e.g. Asia/Singapore)");
|
|
10446
|
+
return { kind: "cron", expr: flags.cron, tz: flags.tz };
|
|
10447
|
+
}
|
|
10448
|
+
if (flags.tz !== void 0) throw new Error("--tz only applies with --cron");
|
|
10449
|
+
if (flags.every !== void 0) return { kind: "interval", everyMs: parseDuration(flags.every), anchorAt: now };
|
|
10450
|
+
if (flags.at !== void 0) {
|
|
10451
|
+
const at = Date.parse(flags.at);
|
|
10452
|
+
if (!Number.isFinite(at)) throw new Error(`--at must be an ISO 8601 timestamp, got: ${flags.at}`);
|
|
10453
|
+
return { kind: "once", at };
|
|
10454
|
+
}
|
|
10455
|
+
return { kind: "manual" };
|
|
10456
|
+
}
|
|
10457
|
+
function describeTrigger(trigger) {
|
|
10458
|
+
switch (trigger.kind) {
|
|
10459
|
+
case "cron":
|
|
10460
|
+
return `cron ${trigger.expr} (${trigger.tz})`;
|
|
10461
|
+
case "interval":
|
|
10462
|
+
return `every ${formatDuration(trigger.everyMs)}`;
|
|
10463
|
+
case "once":
|
|
10464
|
+
return `once at ${new Date(trigger.at).toISOString()}`;
|
|
10465
|
+
default:
|
|
10466
|
+
return "manual";
|
|
10467
|
+
}
|
|
10468
|
+
}
|
|
10469
|
+
function describeAction(action) {
|
|
10470
|
+
if (action.kind === "spawn") return `spawn ${action.agent} in ${action.directory}${action.sticky ? ` (sticky ${action.sticky.key})` : ""}`;
|
|
10471
|
+
if (action.kind === "send") return `send to session ${action.sessionId}`;
|
|
10472
|
+
return `script ${action.command.join(" ")}`;
|
|
10473
|
+
}
|
|
10474
|
+
|
|
10475
|
+
const RECEIPT_RETENTION_MS = 7 * 864e5;
|
|
10476
|
+
const RECEIPT_MAX_ENTRIES = 2e3;
|
|
10477
|
+
function receiptsPath(home) {
|
|
10478
|
+
return join(home, "automation-receipts.json");
|
|
10479
|
+
}
|
|
10480
|
+
function readReceipts(home) {
|
|
10481
|
+
let raw;
|
|
10482
|
+
try {
|
|
10483
|
+
raw = readFileSync(receiptsPath(home), "utf8");
|
|
10484
|
+
} catch (error) {
|
|
10485
|
+
if (error.code === "ENOENT") return {};
|
|
10486
|
+
throw error;
|
|
10487
|
+
}
|
|
10488
|
+
const parsed = JSON.parse(raw);
|
|
10489
|
+
if (!parsed || typeof parsed !== "object" || typeof parsed.receipts !== "object") throw new Error("Invalid automation receipts file");
|
|
10490
|
+
return parsed.receipts;
|
|
10491
|
+
}
|
|
10492
|
+
function writeReceipt(home, receipt, now = Date.now()) {
|
|
10493
|
+
if (!/^[a-zA-Z0-9_-]{1,128}$/.test(receipt.runId)) throw new Error("Invalid automation run id");
|
|
10494
|
+
const all = readReceipts(home);
|
|
10495
|
+
const next = { ...receipt, updatedAt: now };
|
|
10496
|
+
all[receipt.runId] = next;
|
|
10497
|
+
const entries = Object.values(all).sort((a, b) => b.updatedAt - a.updatedAt);
|
|
10498
|
+
const kept = {};
|
|
10499
|
+
let finished = 0;
|
|
10500
|
+
for (const entry of entries) {
|
|
10501
|
+
if (entry.phase === "finished") {
|
|
10502
|
+
if (now - entry.updatedAt > RECEIPT_RETENTION_MS || finished >= RECEIPT_MAX_ENTRIES) continue;
|
|
10503
|
+
finished++;
|
|
10504
|
+
}
|
|
10505
|
+
kept[entry.runId] = entry;
|
|
10506
|
+
}
|
|
10507
|
+
const target = receiptsPath(home);
|
|
10508
|
+
ensurePrivateDirectorySync(dirname(target));
|
|
10509
|
+
const tmp = `${target}.${process.pid}.tmp`;
|
|
10510
|
+
writePrivateFileSync(tmp, JSON.stringify({ version: 1, receipts: kept }));
|
|
10511
|
+
const file = openSync(tmp, "r");
|
|
10512
|
+
try {
|
|
10513
|
+
fsyncSync(file);
|
|
10514
|
+
} finally {
|
|
10515
|
+
closeSync(file);
|
|
10516
|
+
}
|
|
10517
|
+
renameSync(tmp, target);
|
|
10518
|
+
if (process.platform !== "win32") {
|
|
10519
|
+
const directory = openSync(dirname(target), "r");
|
|
10520
|
+
try {
|
|
10521
|
+
fsyncSync(directory);
|
|
10522
|
+
} finally {
|
|
10523
|
+
closeSync(directory);
|
|
10524
|
+
}
|
|
10525
|
+
}
|
|
10526
|
+
return next;
|
|
10527
|
+
}
|
|
10528
|
+
|
|
10529
|
+
const SCRIPT_KILL_GRACE_MS = 1e4;
|
|
10530
|
+
const SCRIPT_DRAIN_MS = 2e3;
|
|
10531
|
+
class TailBuffer {
|
|
10532
|
+
constructor(maxChars) {
|
|
10533
|
+
this.maxChars = maxChars;
|
|
10534
|
+
}
|
|
10535
|
+
text = "";
|
|
10536
|
+
push(chunk) {
|
|
10537
|
+
this.text += chunk;
|
|
10538
|
+
if (this.text.length > this.maxChars * 2) this.text = this.text.slice(this.text.length - this.maxChars);
|
|
10539
|
+
}
|
|
10540
|
+
value() {
|
|
10541
|
+
return this.text.length > this.maxChars ? this.text.slice(this.text.length - this.maxChars) : this.text;
|
|
10542
|
+
}
|
|
10543
|
+
}
|
|
10544
|
+
function killScriptTree(child, signal) {
|
|
10545
|
+
if (child.exitCode !== null || child.signalCode !== null || !child.pid) return;
|
|
10546
|
+
try {
|
|
10547
|
+
if (process.platform !== "win32") process.kill(-child.pid, signal);
|
|
10548
|
+
else child.kill(signal);
|
|
10549
|
+
} catch {
|
|
10550
|
+
try {
|
|
10551
|
+
child.kill(signal);
|
|
10552
|
+
} catch {
|
|
10553
|
+
}
|
|
10554
|
+
}
|
|
10555
|
+
}
|
|
10556
|
+
function terminateScriptTree(child) {
|
|
10557
|
+
killScriptTree(child, "SIGTERM");
|
|
10558
|
+
const killer = setTimeout(() => killScriptTree(child, "SIGKILL"), SCRIPT_KILL_GRACE_MS);
|
|
10559
|
+
killer.unref();
|
|
10560
|
+
child.once("exit", () => clearTimeout(killer));
|
|
10561
|
+
}
|
|
10562
|
+
function runScript(options) {
|
|
10563
|
+
return new Promise((resolve) => {
|
|
10564
|
+
const tail = new TailBuffer(AUTOMATION_SUMMARY_MAX_CHARS);
|
|
10565
|
+
let timedOut = false;
|
|
10566
|
+
let settled = false;
|
|
10567
|
+
let child;
|
|
10568
|
+
const timers = [];
|
|
10569
|
+
const finish = (result) => {
|
|
10570
|
+
if (settled) return;
|
|
10571
|
+
settled = true;
|
|
10572
|
+
for (const t of timers) clearTimeout(t);
|
|
10573
|
+
resolve(result);
|
|
10574
|
+
};
|
|
10575
|
+
const timer = setTimeout(() => {
|
|
10576
|
+
timedOut = true;
|
|
10577
|
+
terminateScriptTree(child);
|
|
10578
|
+
}, options.timeoutMs);
|
|
10579
|
+
timer.unref();
|
|
10580
|
+
timers.push(timer);
|
|
10581
|
+
try {
|
|
10582
|
+
child = spawn$3(options.command[0], options.command.slice(1), {
|
|
10583
|
+
cwd: options.cwd,
|
|
10584
|
+
shell: false,
|
|
10585
|
+
windowsHide: true,
|
|
10586
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
10587
|
+
detached: process.platform !== "win32",
|
|
10588
|
+
env: { ...options.baseEnv ?? process.env, ...options.env ?? {} }
|
|
10589
|
+
});
|
|
10590
|
+
} catch (error) {
|
|
10591
|
+
finish({ exitCode: null, signal: null, timedOut: false, output: "", error: error instanceof Error ? error.message : String(error) });
|
|
10592
|
+
return;
|
|
10593
|
+
}
|
|
10594
|
+
options.onStart?.(child);
|
|
10595
|
+
child.stdout?.setEncoding("utf8").on("data", (chunk) => tail.push(chunk));
|
|
10596
|
+
child.stderr?.setEncoding("utf8").on("data", (chunk) => tail.push(chunk));
|
|
10597
|
+
child.on("error", (error) => finish({ exitCode: null, signal: null, timedOut, output: tail.value(), error: error.message }));
|
|
10598
|
+
child.on("close", (code, signal) => finish({ exitCode: code, signal, timedOut, output: tail.value() }));
|
|
10599
|
+
child.on("exit", (code, signal) => {
|
|
10600
|
+
const drain = setTimeout(() => {
|
|
10601
|
+
killScriptTree(child, "SIGKILL");
|
|
10602
|
+
finish({ exitCode: code, signal, timedOut, output: tail.value() });
|
|
10603
|
+
}, SCRIPT_DRAIN_MS);
|
|
10604
|
+
drain.unref();
|
|
10605
|
+
timers.push(drain);
|
|
10606
|
+
});
|
|
10607
|
+
});
|
|
10608
|
+
}
|
|
10609
|
+
|
|
10610
|
+
const AUTOMATION_TICK_MS = 5e3;
|
|
10611
|
+
const AUTOMATION_LEASE_RENEW_MS = 3e4;
|
|
10612
|
+
const AUTOMATION_LOG_READ_MS = 3e4;
|
|
10613
|
+
const AUTOMATION_UNAVAILABLE_LOG_MS = 10 * 6e4;
|
|
10614
|
+
const AUTOMATION_MAX_CONCURRENT = AUTOMATION_CLAIM_MAX_RUNS;
|
|
10615
|
+
const AUTOMATION_SPAWNED_BY = "automation";
|
|
10616
|
+
function automationPromptHeader(name, runId) {
|
|
10617
|
+
return `[Very Happy automation "${name}" run ${runId}; finish with automation_report (runId ${runId}) or \`very-happy auto report --run ${runId}\`]`;
|
|
10618
|
+
}
|
|
10619
|
+
function automationRunAnchor(runId) {
|
|
10620
|
+
const marker = ` run ${runId};`;
|
|
10621
|
+
return (body) => userPromptText(body)?.startsWith("[Very Happy automation ") === true && userPromptText(body).includes(marker);
|
|
10622
|
+
}
|
|
10623
|
+
async function defaultReadTurn(sessionId, runId) {
|
|
10624
|
+
const persisted = readPersistedSessions()[sessionId];
|
|
10625
|
+
if (!persisted) throw new Error(`No local key for session ${sessionId}`);
|
|
10626
|
+
return analyzeLatestTurn(await readSessionLog(sessionId, persisted, TURN_WINDOW), { anchor: automationRunAnchor(runId) });
|
|
10627
|
+
}
|
|
10628
|
+
async function defaultSend(sessionId, text, meta) {
|
|
10629
|
+
const persisted = await waitForSessionKey(sessionId, 15e3);
|
|
10630
|
+
await sendUserMessage(sessionId, persisted, text, "automation", { ...meta, sentFrom: "automation" });
|
|
10631
|
+
}
|
|
10632
|
+
function expandHome(path) {
|
|
10633
|
+
if (path === "~") return homedir();
|
|
10634
|
+
if (path.startsWith("~/")) return join(homedir(), path.slice(2));
|
|
10635
|
+
return path;
|
|
10636
|
+
}
|
|
10637
|
+
function isSettledByServer(error) {
|
|
10638
|
+
if (!(error instanceof AutomationsRequestError)) return false;
|
|
10639
|
+
if (error.status === 409 && (error.code === "run_finished" || error.code === "claim_mismatch")) return true;
|
|
10640
|
+
return error.status === 404 && error.code === "run_not_found";
|
|
10641
|
+
}
|
|
10642
|
+
function scriptAlive(item) {
|
|
10643
|
+
return !!item.child && item.child.exitCode === null && item.child.signalCode === null;
|
|
10644
|
+
}
|
|
10645
|
+
function createAutomationRunner(deps) {
|
|
10646
|
+
const home = deps.home ?? configuration.happyHomeDir;
|
|
10647
|
+
const client = deps.client ?? createAutomationsClient({ serverUrl: deps.serverUrl ?? configuration.serverUrl, token: deps.token, client: "daemon-auto" });
|
|
10648
|
+
const runScript$1 = deps.runScript ?? runScript;
|
|
10649
|
+
const readTurn = deps.readTurn ?? defaultReadTurn;
|
|
10650
|
+
const send = deps.send ?? defaultSend;
|
|
10651
|
+
const prepareWorktree = deps.prepareWorktree ?? prepareTeamWorktree;
|
|
10652
|
+
const now = deps.now ?? (() => Date.now());
|
|
10653
|
+
const tracked = /* @__PURE__ */ new Map();
|
|
10654
|
+
let running = false;
|
|
10655
|
+
let renewing = false;
|
|
10656
|
+
let closed = false;
|
|
10657
|
+
let disabledUntil = 0;
|
|
10658
|
+
let lastUnavailableLogAt = -Infinity;
|
|
10659
|
+
let recovered = false;
|
|
10660
|
+
function persist(receipt) {
|
|
10661
|
+
writeReceipt(home, receipt, now());
|
|
10662
|
+
}
|
|
10663
|
+
async function finish(item, report2) {
|
|
10664
|
+
try {
|
|
10665
|
+
await client.report(item.runId, report2);
|
|
10666
|
+
} catch (error) {
|
|
10667
|
+
if (!isSettledByServer(error)) {
|
|
10668
|
+
item.pendingFinal = report2;
|
|
10669
|
+
deps.log(`Automation run ${item.runId} final report deferred`);
|
|
10670
|
+
return;
|
|
10671
|
+
}
|
|
10672
|
+
}
|
|
10673
|
+
persist({ runId: item.runId, automationId: item.automationId, kind: item.kind, phase: "finished", claimId: item.claimId, sessionId: item.sessionId });
|
|
10674
|
+
tracked.delete(item.runId);
|
|
10675
|
+
}
|
|
10676
|
+
function track(claimed, kind, sessionId, child) {
|
|
10677
|
+
const at = now();
|
|
10678
|
+
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 };
|
|
10679
|
+
tracked.set(item.runId, item);
|
|
10680
|
+
return item;
|
|
10681
|
+
}
|
|
10682
|
+
async function failNow(claimed, kind, error, attentionReason, sessionId) {
|
|
10683
|
+
const item = track(claimed, kind, sessionId);
|
|
10684
|
+
await finish(item, { claimId: item.claimId, status: "failed", error: clipText(error, AUTOMATION_SUMMARY_MAX_CHARS), ...sessionId ? { sessionId } : {}, ...attentionReason ? { needsAttention: true, attentionReason } : {} });
|
|
10685
|
+
}
|
|
10686
|
+
async function reportRunning(item, sessionId, stickyKey) {
|
|
10687
|
+
const body = { claimId: item.claimId, status: "running", ...sessionId ? { sessionId } : {}, ...stickyKey ? { stickyKey } : {} };
|
|
10688
|
+
try {
|
|
10689
|
+
await client.report(item.runId, body);
|
|
10690
|
+
} catch (error) {
|
|
10691
|
+
if (isSettledByServer(error)) {
|
|
10692
|
+
settleExternally(item);
|
|
10693
|
+
return;
|
|
10694
|
+
}
|
|
10695
|
+
if (sessionId) item.pendingRunning = { sessionId, ...stickyKey ? { stickyKey } : {} };
|
|
10696
|
+
deps.log(`Automation run ${item.runId} running report deferred`);
|
|
10697
|
+
}
|
|
10698
|
+
}
|
|
10699
|
+
async function execute(claimed) {
|
|
10700
|
+
const { run, automation } = claimed;
|
|
10701
|
+
const existing = readReceipts(home)[run.id];
|
|
10702
|
+
if (existing) {
|
|
10703
|
+
if (existing.phase === "finished" || tracked.has(run.id)) return;
|
|
10704
|
+
if (existing.phase === "starting") {
|
|
10705
|
+
await failNow(claimed, existing.kind, "Local execution outcome unknown after restart; verify before re-running", "unknown_outcome");
|
|
10706
|
+
return;
|
|
10707
|
+
}
|
|
10708
|
+
if (existing.kind === "script") {
|
|
10709
|
+
await failNow(claimed, "script", "Daemon restarted while the script was running", "daemon_restarted");
|
|
10710
|
+
return;
|
|
10711
|
+
}
|
|
10712
|
+
track(claimed, existing.kind, existing.sessionId).sawTurn = true;
|
|
10713
|
+
return;
|
|
10714
|
+
}
|
|
10715
|
+
const context = { payload: run.payload, runId: run.id, automationName: automation.name, ...automation.trigger.kind === "cron" ? { tz: automation.trigger.tz } : {}, now: now() };
|
|
10716
|
+
const action = renderAction(automation.action, context);
|
|
10717
|
+
const env = { VH_AUTOMATION_RUN_ID: run.id, VH_AUTOMATION_NAME: automation.name };
|
|
10718
|
+
const header = automationPromptHeader(automation.name, run.id);
|
|
10719
|
+
if (action.kind === "script") {
|
|
10720
|
+
const timeoutMs = action.timeoutMs ?? automation.maxRuntimeMs;
|
|
10721
|
+
persist({ runId: run.id, automationId: automation.id, kind: "script", phase: "starting", claimId: run.claimId });
|
|
10722
|
+
const item2 = track(claimed, "script");
|
|
10723
|
+
const finished = runScript$1({
|
|
10724
|
+
command: action.command,
|
|
10725
|
+
cwd: action.cwd ? expandHome(action.cwd) : void 0,
|
|
10726
|
+
timeoutMs,
|
|
10727
|
+
env: { ...action.env ?? {}, ...env, ...run.payload ? { VH_AUTOMATION_PAYLOAD: run.payload } : {} },
|
|
10728
|
+
onStart: (child) => {
|
|
10729
|
+
item2.child = child;
|
|
10730
|
+
}
|
|
10731
|
+
});
|
|
10732
|
+
persist({ runId: run.id, automationId: automation.id, kind: "script", phase: "running", claimId: run.claimId });
|
|
10733
|
+
await reportRunning(item2);
|
|
10734
|
+
void finished.then(async (result) => {
|
|
10735
|
+
if (!tracked.has(run.id)) return;
|
|
10736
|
+
const ok = result.exitCode === 0 && !result.timedOut && !result.error;
|
|
10737
|
+
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;
|
|
10738
|
+
await finish(item2, { claimId: run.claimId, status: ok ? "done" : "failed", summary: clipText(result.output, AUTOMATION_SUMMARY_MAX_CHARS, "tail"), exitCode: result.exitCode, ...error ? { error } : {} });
|
|
10739
|
+
});
|
|
10740
|
+
return;
|
|
10741
|
+
}
|
|
10742
|
+
if (action.kind === "send") {
|
|
10743
|
+
if (!deps.live(action.sessionId)) {
|
|
10744
|
+
await failNow(claimed, "send", `Session ${action.sessionId} is not running on this machine`, "session_not_live", action.sessionId);
|
|
10745
|
+
return;
|
|
10746
|
+
}
|
|
10747
|
+
persist({ runId: run.id, automationId: automation.id, kind: "send", phase: "running", claimId: run.claimId, sessionId: action.sessionId });
|
|
10748
|
+
try {
|
|
10749
|
+
await send(action.sessionId, `${header}
|
|
10750
|
+
${action.prompt}`, { localId: `automation-${run.id}` });
|
|
10751
|
+
} catch (error) {
|
|
10752
|
+
await failNow(claimed, "send", `Message delivery failed: ${error instanceof Error ? error.message : String(error)}`, "send_failed", action.sessionId);
|
|
10753
|
+
return;
|
|
10754
|
+
}
|
|
10755
|
+
await reportRunning(track(claimed, "send", action.sessionId), action.sessionId);
|
|
10756
|
+
return;
|
|
10757
|
+
}
|
|
10758
|
+
const agent = action.agent;
|
|
10759
|
+
if (!isSpawnAgent(agent)) {
|
|
10760
|
+
await failNow(claimed, "spawn", `Unknown agent "${agent}"`, "invalid_action");
|
|
10761
|
+
return;
|
|
10762
|
+
}
|
|
10763
|
+
if (action.permissionMode && sanitizeSpawnPermissionMode(action.permissionMode) === null) {
|
|
10764
|
+
await failNow(claimed, "spawn", `Invalid permission mode "${action.permissionMode}"`, "invalid_action");
|
|
10765
|
+
return;
|
|
10766
|
+
}
|
|
10767
|
+
const permissionMode = resolveSpawnPermissionMode(agent, action.permissionMode ?? void 0);
|
|
10768
|
+
const stickyKey = action.sticky ? renderTemplate(action.sticky.key, context) : void 0;
|
|
10769
|
+
if (stickyKey) {
|
|
10770
|
+
const bound = claimed.stickies.find((sticky) => sticky.key === stickyKey);
|
|
10771
|
+
if (bound && deps.live(bound.sessionId)) {
|
|
10772
|
+
persist({ runId: run.id, automationId: automation.id, kind: "send", phase: "running", claimId: run.claimId, sessionId: bound.sessionId, stickyKey });
|
|
10773
|
+
try {
|
|
10774
|
+
await send(bound.sessionId, `${header}
|
|
10775
|
+
${action.prompt}`, { localId: `automation-${run.id}` });
|
|
10776
|
+
} catch (error) {
|
|
10777
|
+
await failNow(claimed, "send", `Sticky session delivery failed: ${error instanceof Error ? error.message : String(error)}`, "send_failed", bound.sessionId);
|
|
10778
|
+
return;
|
|
10779
|
+
}
|
|
10780
|
+
await reportRunning(track(claimed, "send", bound.sessionId), bound.sessionId, stickyKey);
|
|
10781
|
+
return;
|
|
10782
|
+
}
|
|
10783
|
+
if (bound) await client.deleteSticky(automation.id, stickyKey).catch(() => deps.log(`Automation ${automation.name}: stale sticky ${stickyKey} not cleared`));
|
|
10784
|
+
}
|
|
10785
|
+
let directory = expandHome(action.directory);
|
|
10786
|
+
if (!isAbsolute(directory)) {
|
|
10787
|
+
await failNow(claimed, "spawn", `Directory must be absolute: ${action.directory}`, "invalid_action");
|
|
10788
|
+
return;
|
|
10789
|
+
}
|
|
10790
|
+
if (action.worktree) {
|
|
10791
|
+
try {
|
|
10792
|
+
directory = (await prepareWorktree(home, directory, `auto-${run.id}`)).directory;
|
|
10793
|
+
} catch (error) {
|
|
10794
|
+
await failNow(claimed, "spawn", `Worktree preparation failed: ${error instanceof Error ? error.message : String(error)}`, "worktree_failed");
|
|
10795
|
+
return;
|
|
10796
|
+
}
|
|
10797
|
+
}
|
|
10798
|
+
persist({ runId: run.id, automationId: automation.id, kind: "spawn", phase: "starting", claimId: run.claimId, stickyKey, ...action.worktree ? { directory } : {} });
|
|
10799
|
+
const spawned = await deps.spawn({ directory, agent, permissionMode, spawnedBy: AUTOMATION_SPAWNED_BY, environmentVariables: env });
|
|
10800
|
+
if (spawned.type !== "success") {
|
|
10801
|
+
await failNow(claimed, "spawn", spawned.type === "error" ? spawned.errorMessage : "Directory approval required", "spawn_failed");
|
|
10802
|
+
return;
|
|
10803
|
+
}
|
|
10804
|
+
persist({ runId: run.id, automationId: automation.id, kind: "spawn", phase: "running", claimId: run.claimId, sessionId: spawned.sessionId, stickyKey, ...action.worktree ? { directory } : {} });
|
|
10805
|
+
const item = track(claimed, "spawn", spawned.sessionId);
|
|
10806
|
+
try {
|
|
10807
|
+
const capabilities = readPersistedSessions()[spawned.sessionId]?.metadata?.capabilities ?? null;
|
|
10808
|
+
const meta = resolveFirstMessageMeta({ agent, permissionMode, explicitModel: action.model ?? void 0, capabilities });
|
|
10809
|
+
await send(spawned.sessionId, `${header}
|
|
10810
|
+
${action.prompt}`, { localId: `automation-${run.id}`, ...meta });
|
|
10811
|
+
} catch (error) {
|
|
10812
|
+
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" });
|
|
10813
|
+
return;
|
|
10814
|
+
}
|
|
10815
|
+
await reportRunning(item, spawned.sessionId, stickyKey);
|
|
10816
|
+
}
|
|
10817
|
+
async function monitor() {
|
|
10818
|
+
for (const item of [...tracked.values()]) {
|
|
10819
|
+
if (closed) return;
|
|
10820
|
+
if (item.pendingFinal) {
|
|
10821
|
+
await finish(item, item.pendingFinal);
|
|
10822
|
+
continue;
|
|
10823
|
+
}
|
|
10824
|
+
if (item.kind === "script") continue;
|
|
10825
|
+
const sessionId = item.sessionId;
|
|
10826
|
+
const active = deps.turnActive(sessionId);
|
|
10827
|
+
if (active) item.sawTurn = true;
|
|
10828
|
+
const alive = deps.live(sessionId) && !item.exited;
|
|
10829
|
+
if (!alive) {
|
|
10830
|
+
const turn2 = await readTurn(sessionId, item.runId).catch(() => null);
|
|
10831
|
+
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) });
|
|
10832
|
+
else await finish(item, { claimId: item.claimId, status: "failed", sessionId, error: turn2?.error ?? "Session exited before the turn ended", needsAttention: true, attentionReason: "session_exited" });
|
|
10833
|
+
continue;
|
|
10834
|
+
}
|
|
10835
|
+
const edgeFell = item.sawTurn && !active;
|
|
10836
|
+
if (!edgeFell && now() < item.nextLogReadAt) continue;
|
|
10837
|
+
item.nextLogReadAt = now() + AUTOMATION_LOG_READ_MS;
|
|
10838
|
+
if (edgeFell) item.sawTurn = false;
|
|
10839
|
+
const turn = await readTurn(sessionId, item.runId).catch(() => null);
|
|
10840
|
+
if (!turn?.ended) continue;
|
|
10841
|
+
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}` });
|
|
10842
|
+
else await finish(item, { claimId: item.claimId, status: "done", sessionId, summary: clipText(turn.answer, AUTOMATION_SUMMARY_MAX_CHARS) });
|
|
10843
|
+
}
|
|
10844
|
+
}
|
|
10845
|
+
async function renewAll() {
|
|
10846
|
+
if (renewing || closed) return;
|
|
10847
|
+
renewing = true;
|
|
10848
|
+
try {
|
|
10849
|
+
const at = now();
|
|
10850
|
+
for (const item of [...tracked.values()]) {
|
|
10851
|
+
if (closed) return;
|
|
10852
|
+
if (at - item.lastLeaseAt < AUTOMATION_LEASE_RENEW_MS) continue;
|
|
10853
|
+
item.lastLeaseAt = at;
|
|
10854
|
+
const body = { claimId: item.claimId, leaseMs: AUTOMATION_DEFAULT_LEASE_MS, ...item.pendingRunning ? { status: "running", ...item.pendingRunning } : {} };
|
|
10855
|
+
try {
|
|
10856
|
+
const run = await client.report(item.runId, body);
|
|
10857
|
+
item.pendingRunning = void 0;
|
|
10858
|
+
if (isAutomationRunTerminal(run.status)) settleExternally(item);
|
|
10859
|
+
} catch (error) {
|
|
10860
|
+
if (isSettledByServer(error)) settleExternally(item);
|
|
10861
|
+
else deps.log(`Automation run ${item.runId} lease renewal failed`);
|
|
10862
|
+
}
|
|
10863
|
+
}
|
|
10864
|
+
} finally {
|
|
10865
|
+
renewing = false;
|
|
10866
|
+
}
|
|
10867
|
+
}
|
|
10868
|
+
function settleExternally(item) {
|
|
10869
|
+
if (item.child && scriptAlive(item)) terminateScriptTree(item.child);
|
|
10870
|
+
persist({ runId: item.runId, automationId: item.automationId, kind: item.kind, phase: "finished", claimId: item.claimId, sessionId: item.sessionId });
|
|
10871
|
+
tracked.delete(item.runId);
|
|
10872
|
+
}
|
|
10873
|
+
async function recover() {
|
|
10874
|
+
recovered = true;
|
|
10875
|
+
let receipts;
|
|
10876
|
+
try {
|
|
10877
|
+
receipts = readReceipts(home);
|
|
10878
|
+
} catch {
|
|
10879
|
+
deps.log("Automation receipts unreadable; refusing to execute until repaired");
|
|
10880
|
+
closed = true;
|
|
10881
|
+
return;
|
|
10882
|
+
}
|
|
10883
|
+
for (const receipt of Object.values(receipts)) {
|
|
10884
|
+
if (receipt.phase === "finished") continue;
|
|
10885
|
+
const item = track({ run: { id: receipt.runId, claimId: receipt.claimId }, automation: { id: receipt.automationId } }, receipt.kind, receipt.sessionId);
|
|
10886
|
+
item.sawTurn = true;
|
|
10887
|
+
item.lastLeaseAt = 0;
|
|
10888
|
+
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" });
|
|
10889
|
+
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" });
|
|
10890
|
+
}
|
|
10891
|
+
}
|
|
10892
|
+
async function tick() {
|
|
10893
|
+
if (running || closed) return;
|
|
10894
|
+
running = true;
|
|
10895
|
+
try {
|
|
10896
|
+
if (!recovered) await recover();
|
|
10897
|
+
if (closed) return;
|
|
10898
|
+
await monitor();
|
|
10899
|
+
const capacity = AUTOMATION_MAX_CONCURRENT - tracked.size;
|
|
10900
|
+
if (now() >= disabledUntil && capacity > 0) {
|
|
10901
|
+
try {
|
|
10902
|
+
const claimed = await client.claim({ machineId: deps.machineId, limit: capacity, leaseMs: AUTOMATION_DEFAULT_LEASE_MS });
|
|
10903
|
+
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)}`))));
|
|
10904
|
+
} catch (error) {
|
|
10905
|
+
if (isAutomationsUnavailable(error)) {
|
|
10906
|
+
disabledUntil = now() + 6e4;
|
|
10907
|
+
if (now() - lastUnavailableLogAt >= AUTOMATION_UNAVAILABLE_LOG_MS) {
|
|
10908
|
+
lastUnavailableLogAt = now();
|
|
10909
|
+
deps.log("Automations unavailable on this server (404); claim loop idle");
|
|
10910
|
+
}
|
|
10911
|
+
} else {
|
|
10912
|
+
disabledUntil = now() + 1e4;
|
|
10913
|
+
deps.log(`Automation claim failed${error instanceof AutomationsRequestError ? ` (${error.status})` : ""}`);
|
|
10914
|
+
}
|
|
10915
|
+
}
|
|
10916
|
+
}
|
|
10917
|
+
} finally {
|
|
10918
|
+
running = false;
|
|
10919
|
+
}
|
|
10920
|
+
}
|
|
10921
|
+
function report(sessionId, event) {
|
|
10922
|
+
for (const item of tracked.values()) {
|
|
10923
|
+
if (item.sessionId !== sessionId) continue;
|
|
10924
|
+
if (event === "exited") item.exited = true;
|
|
10925
|
+
if (event === "blocked" && !item.attentionReported) {
|
|
10926
|
+
item.attentionReported = true;
|
|
10927
|
+
void client.report(item.runId, { claimId: item.claimId, needsAttention: true, attentionReason: "needs_input" }).catch(() => deps.log(`Automation run ${item.runId} attention report failed`));
|
|
10928
|
+
}
|
|
10929
|
+
}
|
|
10930
|
+
}
|
|
10931
|
+
const tickMs = deps.tickMs ?? AUTOMATION_TICK_MS;
|
|
10932
|
+
const timer = setInterval(() => {
|
|
10933
|
+
void tick();
|
|
10934
|
+
}, tickMs);
|
|
10935
|
+
timer.unref();
|
|
10936
|
+
const leaseTimer = setInterval(() => {
|
|
10937
|
+
void renewAll();
|
|
10938
|
+
}, tickMs);
|
|
10939
|
+
leaseTimer.unref();
|
|
10940
|
+
void tick();
|
|
10941
|
+
return {
|
|
10942
|
+
tick,
|
|
10943
|
+
renewAll,
|
|
10944
|
+
report,
|
|
10945
|
+
hasSession: (id) => [...tracked.values()].some((item) => item.sessionId === id),
|
|
10946
|
+
trackedRunIds: () => [...tracked.keys()],
|
|
10947
|
+
/** Executing a claim, or a script child is still alive — handover and auto-update wait for this. */
|
|
10948
|
+
get busy() {
|
|
10949
|
+
return running || [...tracked.values()].some(scriptAlive);
|
|
10950
|
+
},
|
|
10951
|
+
/**
|
|
10952
|
+
* Stop the loops. `killScripts` (real shutdown) terminates live script
|
|
10953
|
+
* trees; the next daemon's recovery reports them failed + attention from
|
|
10954
|
+
* the receipt, never re-running them. A handover waits on `busy` instead.
|
|
10955
|
+
*/
|
|
10956
|
+
stop: (options = {}) => {
|
|
10957
|
+
closed = true;
|
|
10958
|
+
clearInterval(timer);
|
|
10959
|
+
clearInterval(leaseTimer);
|
|
10960
|
+
if (options.killScripts) {
|
|
10961
|
+
for (const item of tracked.values()) if (item.child && scriptAlive(item)) terminateScriptTree(item.child);
|
|
10962
|
+
}
|
|
10963
|
+
}
|
|
10964
|
+
};
|
|
10965
|
+
}
|
|
10966
|
+
|
|
10309
10967
|
function daemonExitCode(source) {
|
|
10310
10968
|
return source === "exception" ? 1 : 0;
|
|
10311
10969
|
}
|
|
@@ -10466,6 +11124,7 @@ async function startDaemon() {
|
|
|
10466
11124
|
const turnActivity = new TurnActivityTracker();
|
|
10467
11125
|
const sessionIdToFinishedSession = /* @__PURE__ */ new Map();
|
|
10468
11126
|
let teamWorker;
|
|
11127
|
+
let automationRunner;
|
|
10469
11128
|
const persisted = readPersistedSessions();
|
|
10470
11129
|
const liveHappyProcesses = await findAllHappyProcesses();
|
|
10471
11130
|
for (const [id, s] of Object.entries(persisted)) {
|
|
@@ -11320,6 +11979,7 @@ async function startDaemon() {
|
|
|
11320
11979
|
const onChildExited = (pid) => {
|
|
11321
11980
|
const session = pidToTrackedSession.get(pid);
|
|
11322
11981
|
if (session?.happySessionId && (session.spawnedBy === "teams" || teamWorker?.hasSession(session.happySessionId))) teamWorker?.report(session.happySessionId, "exited");
|
|
11982
|
+
if (session?.happySessionId && automationRunner?.hasSession(session.happySessionId)) automationRunner.report(session.happySessionId, "exited");
|
|
11323
11983
|
if (session?.happySessionId) turnActivity.forget(session.happySessionId);
|
|
11324
11984
|
if (session?.happySessionId && session.encryption) {
|
|
11325
11985
|
sessionIdToFinishedSession.set(session.happySessionId, session);
|
|
@@ -11339,6 +11999,7 @@ async function startDaemon() {
|
|
|
11339
11999
|
}
|
|
11340
12000
|
};
|
|
11341
12001
|
const onSessionStateEvent = (sessionId, event, spawnedByFromSession) => {
|
|
12002
|
+
if (automationRunner?.hasSession(sessionId)) automationRunner.report(sessionId, event === "completed" ? "idle" : "blocked");
|
|
11342
12003
|
if (spawnedByFromSession === "teams" || teamWorker?.hasSession(sessionId) || [...pidToTrackedSession.values()].some((s) => s.happySessionId === sessionId && s.spawnedBy === "teams")) {
|
|
11343
12004
|
teamWorker?.report(sessionId, event === "completed" ? "idle" : "blocked");
|
|
11344
12005
|
return;
|
|
@@ -11577,7 +12238,7 @@ async function startDaemon() {
|
|
|
11577
12238
|
} catch {
|
|
11578
12239
|
}
|
|
11579
12240
|
}
|
|
11580
|
-
if (bundleReplaced && !teamWorker?.busy && !updateController.isRunning() && !turnActivity.hasActiveTurn()) {
|
|
12241
|
+
if (bundleReplaced && !teamWorker?.busy && !automationRunner?.busy && !updateController.isRunning() && !turnActivity.hasActiveTurn()) {
|
|
11581
12242
|
const preflight = await preflightNewBundle(bundlePath);
|
|
11582
12243
|
if (preflight.action === "hold") {
|
|
11583
12244
|
if (lastHandoverHold !== preflight.reason) {
|
|
@@ -11587,10 +12248,11 @@ async function startDaemon() {
|
|
|
11587
12248
|
updateController.holdHandover(preflight.reason);
|
|
11588
12249
|
return;
|
|
11589
12250
|
}
|
|
11590
|
-
if (teamWorker?.busy || updateController.isRunning() || turnActivity.hasActiveTurn()) return;
|
|
12251
|
+
if (teamWorker?.busy || automationRunner?.busy || updateController.isRunning() || turnActivity.hasActiveTurn()) return;
|
|
11591
12252
|
lastHandoverHold = null;
|
|
11592
12253
|
logger.debug("[DAEMON RUN] Daemon bundle replaced on disk and verified, handing off to new daemon");
|
|
11593
12254
|
teamWorker?.stop();
|
|
12255
|
+
automationRunner?.stop();
|
|
11594
12256
|
clearInterval(restartOnStaleVersionAndHeartbeat);
|
|
11595
12257
|
clearInterval(cliUpdateInterval);
|
|
11596
12258
|
stopAgentVersionChecks();
|
|
@@ -11642,6 +12304,14 @@ async function startDaemon() {
|
|
|
11642
12304
|
live: (id) => [...pidToTrackedSession.values()].some((s) => s.happySessionId === id && isPidAlive(s.pid)),
|
|
11643
12305
|
log: (message) => logger.debug(`[DAEMON RUN] ${message}`)
|
|
11644
12306
|
});
|
|
12307
|
+
automationRunner = createAutomationRunner({
|
|
12308
|
+
machineId,
|
|
12309
|
+
token: credentials.token,
|
|
12310
|
+
spawn: spawnSession,
|
|
12311
|
+
live: (id) => [...pidToTrackedSession.values()].some((s) => s.happySessionId === id && isPidAlive(s.pid)),
|
|
12312
|
+
turnActive: (id) => turnActivity.activeSessions().includes(id),
|
|
12313
|
+
log: (message) => logger.debug(`[DAEMON RUN] ${message}`)
|
|
12314
|
+
});
|
|
11645
12315
|
void apiMachine.updateMachineMetadata((current) => ({
|
|
11646
12316
|
...current ?? initialMachineMetadata,
|
|
11647
12317
|
teamsVersion: 1,
|
|
@@ -11650,6 +12320,7 @@ async function startDaemon() {
|
|
|
11650
12320
|
const cleanupAndShutdown = async (source, errorMessage) => {
|
|
11651
12321
|
logger.debug(`[DAEMON RUN] Starting proper cleanup (source: ${source}, errorMessage: ${errorMessage})...`);
|
|
11652
12322
|
teamWorker?.stop();
|
|
12323
|
+
automationRunner?.stop({ killScripts: true });
|
|
11653
12324
|
if (restartOnStaleVersionAndHeartbeat) {
|
|
11654
12325
|
clearInterval(restartOnStaleVersionAndHeartbeat);
|
|
11655
12326
|
logger.debug("[DAEMON RUN] Health check interval cleared");
|
|
@@ -12154,6 +12825,7 @@ function createMcpServer(handlers, options, sessionId) {
|
|
|
12154
12825
|
version: "1.0.0"
|
|
12155
12826
|
});
|
|
12156
12827
|
registerTeamsTools(mcp, sessionId);
|
|
12828
|
+
registerAutomationTools(mcp);
|
|
12157
12829
|
mcp.registerTool("change_title", {
|
|
12158
12830
|
description: "Change the title of the current chat session",
|
|
12159
12831
|
title: "Change Chat Title",
|
|
@@ -12396,6 +13068,7 @@ async function startHappyServer(client, options) {
|
|
|
12396
13068
|
PREVIEW_TOOL_NAME,
|
|
12397
13069
|
REPORT_PROGRESS_TOOL_NAME,
|
|
12398
13070
|
...TEAM_TOOL_NAMES,
|
|
13071
|
+
...AUTOMATION_TOOL_NAMES,
|
|
12399
13072
|
...options?.assistant && !process.env.VH_TEAM_SCOPE_FILE ? ASSISTANT_TOOL_NAMES : []
|
|
12400
13073
|
],
|
|
12401
13074
|
stop: () => {
|
|
@@ -12732,7 +13405,7 @@ function registerSideQuestionHandler(rpc, deps) {
|
|
|
12732
13405
|
const retainMs = deps.retainMs ?? SIDE_QUESTION_RETAIN_MS;
|
|
12733
13406
|
const maxRunMs = deps.maxRunMs ?? SIDE_QUESTION_MAX_RUN_MS;
|
|
12734
13407
|
const fork = deps.run ?? (async (input) => {
|
|
12735
|
-
const { query } = await import('./index-
|
|
13408
|
+
const { query } = await import('./index-BXA9fxbK.mjs');
|
|
12736
13409
|
return runSideQuestion(query, input);
|
|
12737
13410
|
});
|
|
12738
13411
|
const run = async (input, live) => {
|
|
@@ -12909,7 +13582,12 @@ function runClaudeOneShot(binary, prompt) {
|
|
|
12909
13582
|
try {
|
|
12910
13583
|
child = spawn$3(binary, ["-p", "--model", "haiku", prompt], {
|
|
12911
13584
|
stdio: ["ignore", "pipe", "ignore"],
|
|
12912
|
-
|
|
13585
|
+
// HAPPY_MANAGED=1: this claude is run BY happy. The user-wide
|
|
13586
|
+
// terminal-mirror SessionStart hook exits on it, so a one-shot
|
|
13587
|
+
// spawned from inside a vh web terminal (VH_TERMINAL_ID
|
|
13588
|
+
// inherited — B-500's pi terminal bridge, or `very-happy` typed
|
|
13589
|
+
// in a terminal) never binds a mirror session to itself.
|
|
13590
|
+
env: { ...process.env, HAPPY_MANAGED: "1" }
|
|
12913
13591
|
});
|
|
12914
13592
|
} catch (error) {
|
|
12915
13593
|
logger.debug("[titleGenerator] spawn failed", { error: String(error) });
|
|
@@ -12944,6 +13622,18 @@ function runClaudeOneShot(binary, prompt) {
|
|
|
12944
13622
|
});
|
|
12945
13623
|
});
|
|
12946
13624
|
}
|
|
13625
|
+
async function suggestTitleForPrompt(prompt) {
|
|
13626
|
+
const binary = resolveClaudeBinary();
|
|
13627
|
+
if (!binary) {
|
|
13628
|
+
logger.debug("[titleGenerator] no claude binary; skipping auto-title");
|
|
13629
|
+
return null;
|
|
13630
|
+
}
|
|
13631
|
+
const stdout = await runClaudeOneShot(binary, buildPrompt(prompt));
|
|
13632
|
+
if (stdout == null) return null;
|
|
13633
|
+
const title = sanitizeTitle(stdout);
|
|
13634
|
+
if (!title) logger.debug("[titleGenerator] empty/unusable title output; skipping");
|
|
13635
|
+
return title;
|
|
13636
|
+
}
|
|
12947
13637
|
class TitleGenerator {
|
|
12948
13638
|
constructor(session) {
|
|
12949
13639
|
this.session = session;
|
|
@@ -12967,23 +13657,12 @@ class TitleGenerator {
|
|
|
12967
13657
|
});
|
|
12968
13658
|
}
|
|
12969
13659
|
async generate(firstUserMessage) {
|
|
12970
|
-
const binary = resolveClaudeBinary();
|
|
12971
|
-
if (!binary) {
|
|
12972
|
-
logger.debug("[titleGenerator] no claude binary; skipping auto-title");
|
|
12973
|
-
return;
|
|
12974
|
-
}
|
|
12975
13660
|
if (this.session.getMetadata()?.summary?.text) {
|
|
12976
13661
|
logger.debug("[titleGenerator] summary appeared before generation; skipping");
|
|
12977
13662
|
return;
|
|
12978
13663
|
}
|
|
12979
|
-
const
|
|
12980
|
-
|
|
12981
|
-
if (stdout == null) return;
|
|
12982
|
-
const title = sanitizeTitle(stdout);
|
|
12983
|
-
if (!title) {
|
|
12984
|
-
logger.debug("[titleGenerator] empty/unusable title output; skipping");
|
|
12985
|
-
return;
|
|
12986
|
-
}
|
|
13664
|
+
const title = await suggestTitleForPrompt(firstUserMessage);
|
|
13665
|
+
if (!title) return;
|
|
12987
13666
|
if (this.session.getMetadata()?.summary?.text) {
|
|
12988
13667
|
logger.debug("[titleGenerator] summary set during generation; not overwriting");
|
|
12989
13668
|
return;
|
|
@@ -18011,7 +18690,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
18011
18690
|
process.exit(0);
|
|
18012
18691
|
} else if (subcommand === "install-pi-tools") {
|
|
18013
18692
|
try {
|
|
18014
|
-
const { handleInstallPiTools } = await import('./installPiTools-
|
|
18693
|
+
const { handleInstallPiTools } = await import('./installPiTools-Cj7yU9Fc.mjs');
|
|
18015
18694
|
await handleInstallPiTools(args.slice(1));
|
|
18016
18695
|
} catch (error) {
|
|
18017
18696
|
console.error(error instanceof Error ? error.message : "Pi tools installation failed");
|
|
@@ -18020,7 +18699,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
18020
18699
|
return;
|
|
18021
18700
|
} else if (subcommand === "install-terminal-hooks") {
|
|
18022
18701
|
try {
|
|
18023
|
-
const { installTerminalHooks, parseTerminalHooksArgs, TERMINAL_HOOKS_HELP } = await import('./installTerminalHooks-
|
|
18702
|
+
const { installTerminalHooks, parseTerminalHooksArgs, TERMINAL_HOOKS_HELP } = await import('./installTerminalHooks-Bpl32K75.mjs');
|
|
18024
18703
|
const command = parseTerminalHooksArgs(args.slice(1));
|
|
18025
18704
|
if (command.action === "help") {
|
|
18026
18705
|
console.log(TERMINAL_HOOKS_HELP);
|
|
@@ -18037,7 +18716,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
18037
18716
|
}
|
|
18038
18717
|
} else if (subcommand === "agent-home") {
|
|
18039
18718
|
try {
|
|
18040
|
-
const { parseAgentHomeArgs, runAgentHomeCommand } = await import('./agentHome-
|
|
18719
|
+
const { parseAgentHomeArgs, runAgentHomeCommand } = await import('./agentHome-D68Xh2ae.mjs');
|
|
18041
18720
|
await runAgentHomeCommand(parseAgentHomeArgs(args.slice(1)));
|
|
18042
18721
|
process.exit(0);
|
|
18043
18722
|
} catch (error) {
|
|
@@ -18046,7 +18725,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
18046
18725
|
}
|
|
18047
18726
|
} else if (subcommand === "spawn") {
|
|
18048
18727
|
try {
|
|
18049
|
-
const { handleSpawnCommand } = await import('./spawn-
|
|
18728
|
+
const { handleSpawnCommand } = await import('./spawn-C6fRkedx.mjs');
|
|
18050
18729
|
await handleSpawnCommand(args.slice(1));
|
|
18051
18730
|
} catch (error) {
|
|
18052
18731
|
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|
|
@@ -18058,7 +18737,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
18058
18737
|
return;
|
|
18059
18738
|
} else if (subcommand === "sessions") {
|
|
18060
18739
|
try {
|
|
18061
|
-
const { handleSessionsCommand } = await import('./sessions-
|
|
18740
|
+
const { handleSessionsCommand } = await import('./sessions-iBnYzVUK.mjs');
|
|
18062
18741
|
await handleSessionsCommand(args.slice(1));
|
|
18063
18742
|
} catch (error) {
|
|
18064
18743
|
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|
|
@@ -18070,7 +18749,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
18070
18749
|
return;
|
|
18071
18750
|
} else if (subcommand === "send") {
|
|
18072
18751
|
try {
|
|
18073
|
-
const { handleSendCommand } = await import('./send-
|
|
18752
|
+
const { handleSendCommand } = await import('./send-Cs-RwqNf.mjs');
|
|
18074
18753
|
await handleSendCommand(args.slice(1));
|
|
18075
18754
|
} catch (error) {
|
|
18076
18755
|
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|
|
@@ -18176,9 +18855,9 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
18176
18855
|
if (geminiSubcommand === "project" && args[2] === "set" && args[3]) {
|
|
18177
18856
|
const projectId = args[3];
|
|
18178
18857
|
try {
|
|
18179
|
-
const { saveGoogleCloudProjectToConfig } = await import('./config-
|
|
18180
|
-
const { readCredentialsForConfiguredRelay: readCredentialsForConfiguredRelay2 } = await import('./limits-
|
|
18181
|
-
const { ApiClient: ApiClient2 } = await import('./types-
|
|
18858
|
+
const { saveGoogleCloudProjectToConfig } = await import('./config-CHJ189UJ.mjs');
|
|
18859
|
+
const { readCredentialsForConfiguredRelay: readCredentialsForConfiguredRelay2 } = await import('./limits-BtAuKRl2.mjs').then(function (n) { return n.al; });
|
|
18860
|
+
const { ApiClient: ApiClient2 } = await import('./types-BvqOVUOW.mjs').then(function (n) { return n.ah; });
|
|
18182
18861
|
let userEmail = void 0;
|
|
18183
18862
|
try {
|
|
18184
18863
|
const credentials = await readCredentialsForConfiguredRelay2();
|
|
@@ -18209,7 +18888,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
18209
18888
|
}
|
|
18210
18889
|
if (geminiSubcommand === "project" && args[2] === "get") {
|
|
18211
18890
|
try {
|
|
18212
|
-
const { readGeminiLocalConfig } = await import('./config-
|
|
18891
|
+
const { readGeminiLocalConfig } = await import('./config-CHJ189UJ.mjs');
|
|
18213
18892
|
const config = readGeminiLocalConfig();
|
|
18214
18893
|
if (config.googleCloudProject) {
|
|
18215
18894
|
console.log(`Current Google Cloud Project: ${config.googleCloudProject}`);
|
|
@@ -18249,7 +18928,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
18249
18928
|
process.exit(0);
|
|
18250
18929
|
}
|
|
18251
18930
|
try {
|
|
18252
|
-
const { runGemini } = await import('./runGemini-
|
|
18931
|
+
const { runGemini } = await import('./runGemini-YqH7a-7O.mjs');
|
|
18253
18932
|
let startedBy = void 0;
|
|
18254
18933
|
for (let i = 1; i < args.length; i++) {
|
|
18255
18934
|
if (args[i] === "--started-by") {
|
|
@@ -18272,7 +18951,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
18272
18951
|
} else if (subcommand === "pi") {
|
|
18273
18952
|
if (args[1] === "--terminal") {
|
|
18274
18953
|
try {
|
|
18275
|
-
const { runPiTerminal } = await import('./piTerminal-
|
|
18954
|
+
const { runPiTerminal } = await import('./piTerminal-BoHvPEZ_.mjs');
|
|
18276
18955
|
process.exitCode = await runPiTerminal(args.slice(2));
|
|
18277
18956
|
} catch (error) {
|
|
18278
18957
|
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|
|
@@ -18289,7 +18968,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
18289
18968
|
}
|
|
18290
18969
|
};
|
|
18291
18970
|
try {
|
|
18292
|
-
const { runAcp, resolveAcpAgentConfig } = await import('./index-
|
|
18971
|
+
const { runAcp, resolveAcpAgentConfig } = await import('./index-BgYZGODP.mjs');
|
|
18293
18972
|
const parsed = parsePiRunnerArgs(args.slice(1));
|
|
18294
18973
|
const resolved = resolveAcpAgentConfig(["pi", ...parsed.passthrough]);
|
|
18295
18974
|
const { credentials } = await authAndSetupMachineIfNeeded();
|
|
@@ -18317,7 +18996,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
18317
18996
|
return;
|
|
18318
18997
|
} else if (subcommand === "acp") {
|
|
18319
18998
|
try {
|
|
18320
|
-
const { runAcp, resolveAcpAgentConfig } = await import('./index-
|
|
18999
|
+
const { runAcp, resolveAcpAgentConfig } = await import('./index-BgYZGODP.mjs');
|
|
18321
19000
|
let startedBy = void 0;
|
|
18322
19001
|
let verbose = false;
|
|
18323
19002
|
const acpArgs = [];
|
|
@@ -18357,7 +19036,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
18357
19036
|
return;
|
|
18358
19037
|
} else if (subcommand === "openclaw") {
|
|
18359
19038
|
try {
|
|
18360
|
-
const { runOpenClaw } = await import('./runOpenClaw-
|
|
19039
|
+
const { runOpenClaw } = await import('./runOpenClaw-DSSyFQBb.mjs');
|
|
18361
19040
|
let startedBy = void 0;
|
|
18362
19041
|
let verbose = false;
|
|
18363
19042
|
let gatewayUrl;
|
|
@@ -18408,7 +19087,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
18408
19087
|
return;
|
|
18409
19088
|
} else if (subcommand === "todo") {
|
|
18410
19089
|
try {
|
|
18411
|
-
const { handleTodoCommand } = await import('./todo-
|
|
19090
|
+
const { handleTodoCommand } = await import('./todo-aCZmSt27.mjs');
|
|
18412
19091
|
await handleTodoCommand(args.slice(1));
|
|
18413
19092
|
} catch (error) {
|
|
18414
19093
|
console.error(JSON.stringify({ error: error instanceof Error ? error.message : "Todo command failed" }));
|
|
@@ -18417,16 +19096,28 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
18417
19096
|
return;
|
|
18418
19097
|
} else if (subcommand === "teams") {
|
|
18419
19098
|
try {
|
|
18420
|
-
const { handleTeamsCommand } = await import('./teams-
|
|
19099
|
+
const { handleTeamsCommand } = await import('./teams-CkrWQ4uM.mjs');
|
|
18421
19100
|
await handleTeamsCommand(args.slice(1));
|
|
18422
19101
|
} catch (error) {
|
|
18423
19102
|
console.error(error instanceof Error ? error.message : "Teams command failed");
|
|
18424
19103
|
process.exitCode = 1;
|
|
18425
19104
|
}
|
|
18426
19105
|
return;
|
|
19106
|
+
} else if (subcommand === "auto") {
|
|
19107
|
+
try {
|
|
19108
|
+
const { handleAutoCommand } = await import('./auto-CcXxfqkC.mjs');
|
|
19109
|
+
await handleAutoCommand(args.slice(1));
|
|
19110
|
+
} catch (error) {
|
|
19111
|
+
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Automation command failed");
|
|
19112
|
+
if (process.env.DEBUG) {
|
|
19113
|
+
console.error(error);
|
|
19114
|
+
}
|
|
19115
|
+
process.exitCode = 1;
|
|
19116
|
+
}
|
|
19117
|
+
return;
|
|
18427
19118
|
} else if (subcommand === "mcp" && (args.length === 1 || args.length === 2 && args[1] === "--terminal-tools")) {
|
|
18428
19119
|
try {
|
|
18429
|
-
const { handleMcpCommand } = await import('./mcp-
|
|
19120
|
+
const { handleMcpCommand } = await import('./mcp-DQkmk4VV.mjs');
|
|
18430
19121
|
await handleMcpCommand(args[1] === "--terminal-tools");
|
|
18431
19122
|
} catch (error) {
|
|
18432
19123
|
process.stderr.write(`[very-happy mcp] Fatal: ${error instanceof Error ? error.message : String(error)}
|
|
@@ -18660,6 +19351,8 @@ ${chalk.bold("Usage:")}
|
|
|
18660
19351
|
conversations on this machine (agent-home --help)
|
|
18661
19352
|
very-happy connect Connect AI vendor API keys
|
|
18662
19353
|
very-happy todo Built-in Todos and official agent skill (todo --help)
|
|
19354
|
+
very-happy auto Scheduled / triggered automations: spawn, sticky
|
|
19355
|
+
sessions or scripts with tracked runs (auto --help)
|
|
18663
19356
|
very-happy sandbox Configure and manage OS-level sandboxing
|
|
18664
19357
|
very-happy notify Send push notification
|
|
18665
19358
|
very-happy mcp Stdio MCP server exposing copy_to_clipboard for a
|
|
@@ -18794,4 +19487,4 @@ ${chalk.bold("Examples:")}
|
|
|
18794
19487
|
}
|
|
18795
19488
|
}
|
|
18796
19489
|
|
|
18797
|
-
export { ALLOWED_SPAWN_PERMISSION_MODES as A,
|
|
19490
|
+
export { MessageBuffer as $, ALLOWED_SPAWN_PERMISSION_MODES as A, initialMachineMetadata as B, createSessionMetadata as C, DEFAULT_RECENT_LIMIT as D, setupOfflineReconnection as E, StreamRelay as F, GEMINI_MODEL_ENV as G, MessageQueue2 as H, hashObject as I, startHappyServer as J, registerAgentAttachmentDownloads as K, TitleGenerator as L, MAX_READ_LIMIT as M, registerKillSessionHandler as N, CLAUDE_ATTACHMENT_KINDS as O, PI_TEAMS_EXTENSION as P, stageClaudeAttachments as Q, appendStagedAttachmentsToPrompt as R, SPAWN_AGENTS as S, TurnWaitTimeoutError as T, BasePermissionHandler as U, writeSessionModeFile as V, normalizeAcpPermissionMode as W, removeSessionModeFile as X, GEMINI_API_KEY_ENV as Y, GOOGLE_API_KEY_ENV as Z, BaseReasoningProcessor as _, listSessions as a, CHANGE_TITLE_INSTRUCTION as a0, 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, suggestTitleForPrompt as y, TEAM_SKILL as z };
|