very-happy-cli 0.2.154 → 0.2.156
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{AcpBackend-B14xNj5L.cjs → AcpBackend-BHe1_F1h.cjs} +3 -3
- package/dist/{AcpBackend-Ckyadhf4.mjs → AcpBackend-C3nc1u-s.mjs} +2 -2
- package/dist/{AcpSessionManager-CCHmPdrR.cjs → AcpSessionManager-BVxkQnNf.cjs} +1 -1
- package/dist/{AcpSessionManager-H8t4P-IW.mjs → AcpSessionManager-dN3k1LbI.mjs} +1 -1
- package/dist/agentGuidance-C-CmV03f.cjs +1665 -0
- package/dist/agentGuidance-C604n0Zq.mjs +1613 -0
- package/dist/{agentHome-BUsLTZnz.cjs → agentHome-CmIJNV7f.cjs} +5 -5
- package/dist/{agentHome-B20RWdgE.mjs → agentHome-D2XGsoqs.mjs} +5 -5
- package/dist/{auto-BQnncDcs.mjs → auto-B1I6G_Nf.mjs} +21 -21
- package/dist/{auto-IeldARBS.cjs → auto-CMDq4oIu.cjs} +24 -24
- package/dist/codex/happyMcpStdioBridge.cjs +6 -3
- package/dist/codex/happyMcpStdioBridge.mjs +6 -3
- package/dist/{config-DM_xI9M6.cjs → config-BYHSRY2E.cjs} +8 -8
- package/dist/{config-B-ocQh5V.mjs → config-BkTu25bH.mjs} +8 -8
- package/dist/{index-ZKuBjvHi.mjs → index-B5QbzEga.mjs} +21 -13
- package/dist/{index-BUxBxSxJ.cjs → index-B_-7NpGZ.cjs} +21 -13
- package/dist/{index-eCjkm8Y2.cjs → index-BbtR-Qto.cjs} +606 -982
- package/dist/{index-okLyUzeD.cjs → index-CB8QnjvS.cjs} +7 -7
- package/dist/{index-BELV05ko.mjs → index-DAAreiRF.mjs} +548 -911
- package/dist/{index-CIx3248o.mjs → index-PYO8Mdz4.mjs} +7 -7
- package/dist/index.cjs +6 -6
- package/dist/index.mjs +6 -6
- package/dist/{install-CoLN3whR.mjs → install-CNtXzwT_.mjs} +1 -1
- package/dist/{install-BZOdxfPd.cjs → install-CO1ps6Wt.cjs} +1 -1
- package/dist/{installTerminalHooks-CA4OcAS7.cjs → installTerminalHooks-BOAnQ1Qe.cjs} +5 -5
- package/dist/{installTerminalHooks-Dde_8t36.mjs → installTerminalHooks-CMpOV3RU.mjs} +5 -5
- package/dist/lib.cjs +2 -2
- package/dist/lib.d.cts +24 -8
- package/dist/lib.d.mts +24 -8
- package/dist/lib.mjs +2 -2
- package/dist/{limits-Dt7_unH4.mjs → limits-DmMdchTz.mjs} +309 -13
- package/dist/{limits-CmzISka2.cjs → limits-o29KXBgj.cjs} +329 -12
- package/dist/{mcp-9Ui4nQZL.mjs → mcp-C_uejotl.mjs} +17 -17
- package/dist/{mcp-7iDHlyCI.cjs → mcp-CwK1nI7Y.cjs} +23 -23
- package/dist/{piRuntime-C4E4pgye.mjs → piRuntime-Cu2-8nOT.mjs} +2 -2
- package/dist/{piRuntime-BesGGPME.cjs → piRuntime-Dj5WnqzJ.cjs} +2 -2
- package/dist/{piTerminal-fUogwxYr.cjs → piTerminal-D-BIbov9.cjs} +6 -6
- package/dist/{piTerminal-CCEGCQOY.mjs → piTerminal-DanZeBrO.mjs} +6 -6
- package/dist/{runGemini-CNPGSsSK.cjs → runGemini--QqK-Ap5.cjs} +7 -7
- package/dist/{runGemini-BW-pEklU.mjs → runGemini-CZ_lhMWY.mjs} +6 -6
- package/dist/{runOpenClaw-Bsodkg_s.cjs → runOpenClaw-C0xLhTvk.cjs} +6 -6
- package/dist/{runOpenClaw-NRB6-_jU.mjs → runOpenClaw-CxgLpm38.mjs} +5 -5
- package/dist/{send-DYeSFg8s.mjs → send-BaOkDgH0.mjs} +5 -39
- package/dist/{send-Bd00QcHC.cjs → send-CTcGMVuG.cjs} +8 -42
- package/dist/{sessions-DlPOYKLU.mjs → sessions-Bn304xF1.mjs} +88 -45
- package/dist/{sessions-RIFUoy_g.cjs → sessions-OmvyJb0n.cjs} +99 -56
- package/dist/{spawn-D1bhyHbH.mjs → spawn-BKNabutV.mjs} +22 -22
- package/dist/{spawn-DxrfHTF4.cjs → spawn-CC6V2KiA.cjs} +30 -30
- package/dist/{teams-pfKHpkgL.mjs → teams-CLbS2_4Q.mjs} +8 -8
- package/dist/{teams-Cfy0VMCo.cjs → teams-XtrH3_h3.cjs} +8 -8
- package/dist/{todo-B2CjDg_V.mjs → todo-BcPbNKt2.mjs} +8 -6
- package/dist/{todo-B4cBFzHB.cjs → todo-CEDvKNXK.cjs} +8 -6
- package/dist/{types-D5RWyBE5.mjs → types-BEVzYSoV.mjs} +52 -291
- package/dist/{types-Bd_TGr0F.cjs → types-CphWjqHc.cjs} +64 -320
- package/package.json +7 -7
- package/dist/agentGuidance-DUBXppv-.cjs +0 -554
- package/dist/agentGuidance-TLYQxsm2.mjs +0 -530
|
@@ -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 { g as getProjectPath, k as initializeSandbox, w as wrapCommand, j as contentLogMetadata, I as InvalidateSync, R as RawJSONLinesSchema, l as AsyncLock, M as MAX_CHAT_ATTACHMENT_SOURCE_BYTES, m as commandOnPath, n as deriveLocalCliUpdateSummary, o as isClaudeSessionId, q as getLocalHappyAgentCredentialPath, s as readLocalHappyAgentCredentials, t as parseExactVersion, u as compareExactVersions, v as extractCompleteLines, x as decideBackfill, y as parseTerminalHookPayload, z as tmuxArgs, B as scrubTmuxClientEnv, D as decideMirrorBinding, E as MIRROR_BACKFILL_LINES_DEFAULT, G as mirrorLineKey, H as mirrorLocalId, J as detectCLIAvailability, K as detectResumeSupport, L as refreshAgentHomes, d as describeAgentHome, T as TurnActivityTracker, N as summarizeSpawnSessionForLog, O as locateClaudeConversation, P as agentHomeSpawnEnv, Q as shellescape, A as ApiClient, S as fetchCliUpdateState, U as resolveCliUpdateCheckInterval, V as DEFAULT_CLI_UPDATE_CHECK_INTERVAL_MS, W as listVhTerminals, X as readVhTerminal, Y as sendToVhTerminal, Z as TITLE_PROMPT_PREFIX, _ as BOARD_ANALYZER_PROMPT_PREFIX, h as connectionState, $ as startOfflineReconnection, C as CodexAppServerClient } from './types-BEVzYSoV.mjs';
|
|
5
|
+
import { l as logger, a7 as notifyDaemonSessionEvent, x as BUILTIN_TODO_DISCOVERY, g as delay, a8 as STREAM_DELTA_MAX_CHARS, a9 as ensurePrivateDirectory, aa as hardenPrivateFile, c as configuration, ab as notifyDaemonClaudeAuthFailed, F as packageJson, r as readSettings, ac as readCredentials, Z as credentialRelayProblem, k as checkIfDaemonRunningAndCleanupStaleState, ad as readDaemonState, ae as AGENT_VERSION_SOURCES, w as encodeBase64, af as readSettingsOutcome, u as updateSettings, ag as encodeBase64Url, M as decodeBase64, ah as writeCredentialsLegacy, ai as writeCredentialsDataKey, d as readPersistedSessions, E as writePrivateFileSync, aj as decryptWithDataKey, ak as decryptLegacy, al as persistSession, z as ensurePrivateDirectorySync, am as getAgentCodeDefaults, an as normalizeAgentKey, ao as isClaudeOpus55Model, ap as supportsClaudeOpus55, aq as AUTOMATION_SUMMARY_MAX_CHARS, ar as AUTOMATION_CLAIM_MAX_RUNS, as as AUTOMATION_DEFAULT_LEASE_MS, f as isAutomationRunTerminal, at as isDaemonRunningCurrentlyInstalledHappyVersion, au as stopDaemon, av as acquireDaemonLock, aw as deletePersistedSessions, N as decrypt, ax as writeDaemonState, ay as writeSettings, az as cleanupDaemonState, aA as releaseDaemonLock, i as isValidSessionId, m as spawnDaemonSession, a1 as isValidTerminalId, aB as ContextUsageSchema, C as CLIPBOARD_TOOL_NAME, a as CLIPBOARD_TOOL_TITLE, b as CLIPBOARD_TOOL_DESCRIPTION, o as CLIPBOARD_MAX_BYTES, v as notifyDaemonSessionStarted, y as reportSessionEditToDaemon, aC as clearCredentials, aD as clearMachineId, h as readCredentialsForConfiguredRelay, aE as SandboxConfigSchema, e as createEnvelope, O as listDaemonSessions, n as stopDaemonSession, aF as getLatestDaemonLog } from './limits-DmMdchTz.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 './terminalTitleSuggest-DKgX3osW.mjs';
|
|
11
|
-
import {
|
|
11
|
+
import { E as trimIdent, F as AGENT_GUIDANCE, G as formatEditConflictNotice, z as sendUserMessage, S as SESSION_PEER_SENT_FROM, x as readSessionMetadata, y as waitForSessionKey, k as archiveSession, H as createAutomationsClient, t as isAutomationsUnavailable, I as AutomationsRequestError, J as analyzeLatestTurn, K as readSessionLog$1, L as TURN_WINDOW, N as userPromptText, v as teamScopePath, O as TeamConnectionPreflightError, C as createTeamsClient, e as listSessions, h as readSessionTranscript, n as deliverToSession, o as sessionWebUrl, i as stopSession, P as PREVIEW_TOOL_NAME, R as REPORT_PROGRESS_TOOL_NAME, Q as TEAM_TOOL_NAMES, U as AUTOMATION_TOOL_NAMES, V as SESSION_PEER_TOOL_NAMES, r as registerTeamsTools, c as registerAutomationTools, d as registerSessionPeerTools, W as createSessionPeerToolExecutor, a as PREVIEW_TOOL_TITLE, b as PREVIEW_TOOL_DESCRIPTION, X as REPORT_PROGRESS_TOOL_TITLE, Y as REPORT_PROGRESS_TOOL_DESCRIPTION } from './agentGuidance-C604n0Zq.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';
|
|
@@ -16,7 +16,7 @@ import React, { useState, useRef, useEffect, useCallback } from 'react';
|
|
|
16
16
|
import { query as query$1, AbortError } from '@anthropic-ai/claude-agent-sdk';
|
|
17
17
|
import axios, { AxiosError } from 'axios';
|
|
18
18
|
import 'node:events';
|
|
19
|
-
import
|
|
19
|
+
import 'socket.io-client';
|
|
20
20
|
import tweetnacl from 'tweetnacl';
|
|
21
21
|
import os$1, { homedir as homedir$1 } from 'os';
|
|
22
22
|
import { spawn as spawn$2, execSync as execSync$1, exec as exec$1 } from 'child_process';
|
|
@@ -878,7 +878,7 @@ async function createSessionScanner(opts) {
|
|
|
878
878
|
let processedMessageKeys = /* @__PURE__ */ new Set();
|
|
879
879
|
let deadSessions = /* @__PURE__ */ new Set();
|
|
880
880
|
if (opts.sessionId) {
|
|
881
|
-
let messages = await readSessionLog
|
|
881
|
+
let messages = await readSessionLog(projectDir, opts.sessionId);
|
|
882
882
|
logger.debug(`[SESSION_SCANNER] Marking ${messages.length} existing messages as processed from session ${opts.sessionId}`);
|
|
883
883
|
for (let m of messages) {
|
|
884
884
|
processedMessageKeys.add(messageKey(m));
|
|
@@ -901,7 +901,7 @@ async function createSessionScanner(opts) {
|
|
|
901
901
|
}
|
|
902
902
|
}
|
|
903
903
|
for (let session of sessions) {
|
|
904
|
-
const sessionMessages = await readSessionLog
|
|
904
|
+
const sessionMessages = await readSessionLog(projectDir, session);
|
|
905
905
|
let skipped = 0;
|
|
906
906
|
let sent = 0;
|
|
907
907
|
for (let file of sessionMessages) {
|
|
@@ -978,7 +978,7 @@ async function createSessionScanner(opts) {
|
|
|
978
978
|
return;
|
|
979
979
|
}
|
|
980
980
|
if (options?.treatExistingAsProcessed) {
|
|
981
|
-
const existing = await readSessionLog
|
|
981
|
+
const existing = await readSessionLog(projectDir, sessionId);
|
|
982
982
|
logger.debug(`[SESSION_SCANNER] Pre-marking ${existing.length} existing messages as processed for new session ${sessionId}`);
|
|
983
983
|
for (const m of existing) {
|
|
984
984
|
processedMessageKeys.add(messageKey(m));
|
|
@@ -1006,7 +1006,7 @@ function messageKey(message) {
|
|
|
1006
1006
|
throw Error();
|
|
1007
1007
|
}
|
|
1008
1008
|
}
|
|
1009
|
-
async function readSessionLog
|
|
1009
|
+
async function readSessionLog(projectDir, sessionId) {
|
|
1010
1010
|
const expectedSessionFile = join(projectDir, `${sessionId}.jsonl`);
|
|
1011
1011
|
logger.debug(`[SESSION_SCANNER] Reading session file: ${expectedSessionFile}`);
|
|
1012
1012
|
let file;
|
|
@@ -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-D2XGsoqs.mjs');
|
|
5384
|
+
const { describeAgentHome } = await import('./types-BEVzYSoV.mjs').then(function (n) { return n.a0; });
|
|
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)}`));
|
|
@@ -6063,59 +6063,6 @@ function formatAssistantReportMessage(title, sessionId, event) {
|
|
|
6063
6063
|
return `[\u7CFB\u7EDF\u901A\u62A5] \u4F1A\u8BDD\u300C${title}\u300D${state}\uFF08${sessionId}\uFF09\u3002\u8BF7\u7528 session_read \u6838\u5B9E\u7ED3\u679C\uFF0C\u5E76\u5411\u7528\u6237\u53E3\u5934\u6C47\u62A5\u4E00\u53E5\u7ED3\u8BBA\uFF1B\u591A\u6761\u901A\u62A5\u63A5\u8FD1\u65F6\u5408\u5E76\u6C47\u62A5\u3002`;
|
|
6064
6064
|
}
|
|
6065
6065
|
|
|
6066
|
-
function sessionWebUrl(sessionId) {
|
|
6067
|
-
return `${configuration.webappUrl.replace(/\/+$/, "")}/session/${sessionId}`;
|
|
6068
|
-
}
|
|
6069
|
-
async function waitForSessionKey(sessionId, timeoutMs) {
|
|
6070
|
-
const deadline = Date.now() + timeoutMs;
|
|
6071
|
-
while (true) {
|
|
6072
|
-
const entry = readPersistedSessions()[sessionId];
|
|
6073
|
-
if (entry) return entry;
|
|
6074
|
-
if (Date.now() >= deadline) {
|
|
6075
|
-
throw new Error(
|
|
6076
|
-
`Session ${sessionId} has no encryption key in ${configuration.sessionsFile}. Either it was not spawned by this machine's daemon, or the daemon is too old to persist session keys.`
|
|
6077
|
-
);
|
|
6078
|
-
}
|
|
6079
|
-
await delay(200);
|
|
6080
|
-
}
|
|
6081
|
-
}
|
|
6082
|
-
async function sendUserMessage(sessionId, persisted, text, client, options = {}) {
|
|
6083
|
-
const credentials = await readCredentialsForConfiguredRelay();
|
|
6084
|
-
if (!credentials) {
|
|
6085
|
-
throw new Error("Not authenticated. Run `very-happy auth login` first.");
|
|
6086
|
-
}
|
|
6087
|
-
const envelope = {
|
|
6088
|
-
role: "user",
|
|
6089
|
-
content: {
|
|
6090
|
-
type: "text",
|
|
6091
|
-
text
|
|
6092
|
-
},
|
|
6093
|
-
meta: {
|
|
6094
|
-
sentFrom: options.sentFrom ?? "cli",
|
|
6095
|
-
...options.model !== void 0 ? { model: options.model } : {},
|
|
6096
|
-
...options.permissionMode !== void 0 ? { permissionMode: options.permissionMode } : {},
|
|
6097
|
-
...options.effort !== void 0 ? { effort: options.effort } : {}
|
|
6098
|
-
}
|
|
6099
|
-
};
|
|
6100
|
-
const encrypted = encodeBase64(encrypt(
|
|
6101
|
-
decodeBase64(persisted.encryptionKey),
|
|
6102
|
-
persisted.encryptionVariant,
|
|
6103
|
-
envelope
|
|
6104
|
-
));
|
|
6105
|
-
await axios.post(
|
|
6106
|
-
`${configuration.serverUrl}/v3/sessions/${encodeURIComponent(sessionId)}/messages`,
|
|
6107
|
-
{ messages: [{ content: encrypted, localId: options.localId ?? randomUUID() }] },
|
|
6108
|
-
{
|
|
6109
|
-
headers: {
|
|
6110
|
-
"Authorization": `Bearer ${credentials.token}`,
|
|
6111
|
-
"Content-Type": "application/json",
|
|
6112
|
-
"X-Happy-Client": `${client}/${configuration.currentCliVersion}`
|
|
6113
|
-
},
|
|
6114
|
-
timeout: 3e4
|
|
6115
|
-
}
|
|
6116
|
-
);
|
|
6117
|
-
}
|
|
6118
|
-
|
|
6119
6066
|
const ALLOWED_SPAWN_PERMISSION_MODES = [
|
|
6120
6067
|
"default",
|
|
6121
6068
|
"acceptEdits",
|
|
@@ -6271,7 +6218,10 @@ function startDaemonControlServer({
|
|
|
6271
6218
|
pushClipboard,
|
|
6272
6219
|
pushFilePreview,
|
|
6273
6220
|
onTerminalHook,
|
|
6274
|
-
setTerminalTitle
|
|
6221
|
+
setTerminalTitle,
|
|
6222
|
+
onSessionEdit,
|
|
6223
|
+
listPeers,
|
|
6224
|
+
isTurnActive
|
|
6275
6225
|
}) {
|
|
6276
6226
|
return new Promise((resolve) => {
|
|
6277
6227
|
const app = fastify({
|
|
@@ -6355,7 +6305,8 @@ function startDaemonControlServer({
|
|
|
6355
6305
|
children: z.array(z.object({
|
|
6356
6306
|
startedBy: z.string(),
|
|
6357
6307
|
happySessionId: z.string(),
|
|
6358
|
-
pid: z.number()
|
|
6308
|
+
pid: z.number(),
|
|
6309
|
+
turnActive: z.boolean()
|
|
6359
6310
|
}))
|
|
6360
6311
|
})
|
|
6361
6312
|
}
|
|
@@ -6367,7 +6318,8 @@ function startDaemonControlServer({
|
|
|
6367
6318
|
children: children.filter((child) => child.happySessionId !== void 0).map((child) => ({
|
|
6368
6319
|
startedBy: child.startedBy,
|
|
6369
6320
|
happySessionId: child.happySessionId,
|
|
6370
|
-
pid: child.pid
|
|
6321
|
+
pid: child.pid,
|
|
6322
|
+
turnActive: isTurnActive ? isTurnActive(child.happySessionId) : false
|
|
6371
6323
|
}))
|
|
6372
6324
|
};
|
|
6373
6325
|
});
|
|
@@ -6594,6 +6546,45 @@ function startDaemonControlServer({
|
|
|
6594
6546
|
}
|
|
6595
6547
|
return { status: "ok" };
|
|
6596
6548
|
});
|
|
6549
|
+
typed.post("/session-edit", {
|
|
6550
|
+
schema: {
|
|
6551
|
+
body: z.object({
|
|
6552
|
+
sessionId: z.string().regex(/^[a-zA-Z0-9_-]{1,128}$/),
|
|
6553
|
+
path: z.string().min(1).max(4096),
|
|
6554
|
+
tool: z.string().min(1).max(64),
|
|
6555
|
+
cwd: z.string().max(4096).optional(),
|
|
6556
|
+
/** When the runner saw the call (ms). Clamped to now by the daemon;
|
|
6557
|
+
* older than the conflict window = ignored (replayed history). */
|
|
6558
|
+
at: z.number().int().nonnegative().optional()
|
|
6559
|
+
}),
|
|
6560
|
+
response: { 200: z.object({ status: z.literal("ok") }) }
|
|
6561
|
+
}
|
|
6562
|
+
}, async (request) => {
|
|
6563
|
+
try {
|
|
6564
|
+
onSessionEdit?.(request.body);
|
|
6565
|
+
} catch (error) {
|
|
6566
|
+
logger.debug("[CONTROL SERVER] session-edit handler failed:", error);
|
|
6567
|
+
}
|
|
6568
|
+
return { status: "ok" };
|
|
6569
|
+
});
|
|
6570
|
+
typed.post("/peers", {
|
|
6571
|
+
schema: {
|
|
6572
|
+
response: {
|
|
6573
|
+
200: z.object({
|
|
6574
|
+
sessions: z.array(z.object({
|
|
6575
|
+
sessionId: z.string(),
|
|
6576
|
+
kind: z.enum(["managed", "mirror"]),
|
|
6577
|
+
pid: z.number().optional(),
|
|
6578
|
+
cwd: z.string().optional(),
|
|
6579
|
+
flavor: z.string().optional(),
|
|
6580
|
+
title: z.string().optional(),
|
|
6581
|
+
variant: z.string().optional(),
|
|
6582
|
+
edits: z.array(z.object({ path: z.string(), tool: z.string(), at: z.number() }))
|
|
6583
|
+
}))
|
|
6584
|
+
})
|
|
6585
|
+
}
|
|
6586
|
+
}
|
|
6587
|
+
}, async () => ({ sessions: listPeers ? listPeers() : [] }));
|
|
6597
6588
|
typed.post("/stop", {
|
|
6598
6589
|
schema: {
|
|
6599
6590
|
response: {
|
|
@@ -6629,6 +6620,273 @@ function startDaemonControlServer({
|
|
|
6629
6620
|
});
|
|
6630
6621
|
}
|
|
6631
6622
|
|
|
6623
|
+
const EDIT_CONFLICT_WINDOW_MS = 30 * 6e4;
|
|
6624
|
+
function pairKey$1(a, b, path) {
|
|
6625
|
+
return a < b ? `${a}\0${b}\0${path}` : `${b}\0${a}\0${path}`;
|
|
6626
|
+
}
|
|
6627
|
+
class EditConflictTracker {
|
|
6628
|
+
windowMs;
|
|
6629
|
+
maxPaths;
|
|
6630
|
+
/** real path → sessionId → latest edit */
|
|
6631
|
+
byPath = /* @__PURE__ */ new Map();
|
|
6632
|
+
/** (sorted pair, path) → when the notice went out */
|
|
6633
|
+
notified = /* @__PURE__ */ new Map();
|
|
6634
|
+
constructor(options = {}) {
|
|
6635
|
+
this.windowMs = options.windowMs ?? EDIT_CONFLICT_WINDOW_MS;
|
|
6636
|
+
this.maxPaths = options.maxPaths ?? 5e3;
|
|
6637
|
+
}
|
|
6638
|
+
/**
|
|
6639
|
+
* Remember an edit and return the peers to notify about it. `isLive`
|
|
6640
|
+
* decides whether another session still counts (dead ones are dropped
|
|
6641
|
+
* from the table on the spot).
|
|
6642
|
+
*/
|
|
6643
|
+
record(edit, isLive) {
|
|
6644
|
+
this.prune(edit.at);
|
|
6645
|
+
let sessions = this.byPath.get(edit.path);
|
|
6646
|
+
if (!sessions) {
|
|
6647
|
+
sessions = /* @__PURE__ */ new Map();
|
|
6648
|
+
this.byPath.set(edit.path, sessions);
|
|
6649
|
+
}
|
|
6650
|
+
const conflicts = [];
|
|
6651
|
+
for (const [sessionId, peer] of sessions) {
|
|
6652
|
+
if (sessionId === edit.sessionId) continue;
|
|
6653
|
+
if (!isLive(sessionId)) {
|
|
6654
|
+
sessions.delete(sessionId);
|
|
6655
|
+
continue;
|
|
6656
|
+
}
|
|
6657
|
+
if (edit.at - peer.at > this.windowMs) continue;
|
|
6658
|
+
const key = pairKey$1(edit.sessionId, sessionId, edit.path);
|
|
6659
|
+
const notifiedAt = this.notified.get(key);
|
|
6660
|
+
if (notifiedAt !== void 0 && edit.at - notifiedAt <= this.windowMs) continue;
|
|
6661
|
+
this.notified.set(key, edit.at);
|
|
6662
|
+
conflicts.push({ editor: edit, peer });
|
|
6663
|
+
}
|
|
6664
|
+
sessions.set(edit.sessionId, edit);
|
|
6665
|
+
return conflicts;
|
|
6666
|
+
}
|
|
6667
|
+
/** Paths this session edited inside the window, newest first. */
|
|
6668
|
+
editsOf(sessionId, now, limit = 20) {
|
|
6669
|
+
const out = [];
|
|
6670
|
+
for (const [path, sessions] of this.byPath) {
|
|
6671
|
+
const edit = sessions.get(sessionId);
|
|
6672
|
+
if (edit && now - edit.at <= this.windowMs) out.push({ path, tool: edit.tool, at: edit.at });
|
|
6673
|
+
}
|
|
6674
|
+
return out.sort((a, b) => b.at - a.at).slice(0, limit);
|
|
6675
|
+
}
|
|
6676
|
+
/** Session ended: nothing it did counts any more. */
|
|
6677
|
+
forget(sessionId) {
|
|
6678
|
+
for (const [path, sessions] of this.byPath) {
|
|
6679
|
+
sessions.delete(sessionId);
|
|
6680
|
+
if (sessions.size === 0) this.byPath.delete(path);
|
|
6681
|
+
}
|
|
6682
|
+
for (const key of this.notified.keys()) {
|
|
6683
|
+
const [a, b] = key.split("\0");
|
|
6684
|
+
if (a === sessionId || b === sessionId) this.notified.delete(key);
|
|
6685
|
+
}
|
|
6686
|
+
}
|
|
6687
|
+
/** Drop everything older than the window (and cap the table size). */
|
|
6688
|
+
prune(now) {
|
|
6689
|
+
for (const [path, sessions] of this.byPath) {
|
|
6690
|
+
for (const [sessionId, edit] of sessions) if (now - edit.at > this.windowMs) sessions.delete(sessionId);
|
|
6691
|
+
if (sessions.size === 0) this.byPath.delete(path);
|
|
6692
|
+
}
|
|
6693
|
+
for (const [key, at] of this.notified) if (now - at > this.windowMs) this.notified.delete(key);
|
|
6694
|
+
if (this.byPath.size > this.maxPaths) {
|
|
6695
|
+
const oldest = [...this.byPath.entries()].map(([path, sessions]) => ({ path, at: Math.max(...[...sessions.values()].map((e) => e.at)) })).sort((a, b) => a.at - b.at);
|
|
6696
|
+
for (const entry of oldest.slice(0, this.byPath.size - this.maxPaths)) this.byPath.delete(entry.path);
|
|
6697
|
+
}
|
|
6698
|
+
}
|
|
6699
|
+
/** Test / diagnostics helper. */
|
|
6700
|
+
size() {
|
|
6701
|
+
return this.byPath.size;
|
|
6702
|
+
}
|
|
6703
|
+
}
|
|
6704
|
+
function normalizeEditPath(path, cwd, realpath = realpathSync.native ?? realpathSync) {
|
|
6705
|
+
const absolute = isAbsolute(path) ? resolve(path) : resolve(cwd, path);
|
|
6706
|
+
let existing = absolute;
|
|
6707
|
+
const tail = [];
|
|
6708
|
+
for (let depth = 0; depth < 64; depth++) {
|
|
6709
|
+
try {
|
|
6710
|
+
const real = realpath(existing);
|
|
6711
|
+
return tail.length ? join(real, ...tail) : real;
|
|
6712
|
+
} catch {
|
|
6713
|
+
const parent = dirname(existing);
|
|
6714
|
+
if (parent === existing) break;
|
|
6715
|
+
tail.unshift(existing.slice(parent.length).replace(new RegExp(`^\\${sep}`), ""));
|
|
6716
|
+
existing = parent;
|
|
6717
|
+
}
|
|
6718
|
+
}
|
|
6719
|
+
return absolute;
|
|
6720
|
+
}
|
|
6721
|
+
|
|
6722
|
+
const CONFLICT_COALESCE_MS = 1e4;
|
|
6723
|
+
const MAX_NOTICES_PER_PAIR = 5;
|
|
6724
|
+
function pairKey(a, b) {
|
|
6725
|
+
return a < b ? `${a}\0${b}` : `${b}\0${a}`;
|
|
6726
|
+
}
|
|
6727
|
+
function senderOf(info, sessionId) {
|
|
6728
|
+
return { sessionId, title: info?.title, flavor: info?.flavor, cwd: info?.cwd };
|
|
6729
|
+
}
|
|
6730
|
+
function createPeerCoordinator(deps) {
|
|
6731
|
+
const now = deps.now ?? (() => Date.now());
|
|
6732
|
+
const windowMs = deps.windowMs ?? EDIT_CONFLICT_WINDOW_MS;
|
|
6733
|
+
const coalesceMs = deps.coalesceMs ?? CONFLICT_COALESCE_MS;
|
|
6734
|
+
const maxNoticesPerPair = deps.maxNoticesPerPair ?? MAX_NOTICES_PER_PAIR;
|
|
6735
|
+
const tracker = new EditConflictTracker({ windowMs });
|
|
6736
|
+
const readPersisted = deps.readPersisted ?? readPersistedSessions;
|
|
6737
|
+
const send = deps.send ?? sendUserMessage;
|
|
6738
|
+
const normalize = deps.normalize ?? normalizeEditPath;
|
|
6739
|
+
const liveTitle = deps.liveTitle ?? (async (sessionId, persisted) => (await readSessionMetadata(sessionId, persisted))?.summary?.text);
|
|
6740
|
+
const schedule = deps.schedule ?? ((fn, ms) => {
|
|
6741
|
+
const t = setTimeout(fn, ms);
|
|
6742
|
+
t.unref?.();
|
|
6743
|
+
return t;
|
|
6744
|
+
});
|
|
6745
|
+
let getMirrors = deps.getMirrors ?? (() => []);
|
|
6746
|
+
const pending = /* @__PURE__ */ new Map();
|
|
6747
|
+
const noticesSent = /* @__PURE__ */ new Map();
|
|
6748
|
+
let inflight = Promise.resolve();
|
|
6749
|
+
const snapshot = () => {
|
|
6750
|
+
const at = now();
|
|
6751
|
+
const out = /* @__PURE__ */ new Map();
|
|
6752
|
+
for (const child of deps.getChildren()) {
|
|
6753
|
+
const id = child.happySessionId;
|
|
6754
|
+
if (!id || out.has(id)) continue;
|
|
6755
|
+
const meta = child.happySessionMetadataFromLocalWebhook;
|
|
6756
|
+
out.set(id, {
|
|
6757
|
+
sessionId: id,
|
|
6758
|
+
kind: "managed",
|
|
6759
|
+
pid: child.pid,
|
|
6760
|
+
...meta?.path ? { cwd: meta.path } : {},
|
|
6761
|
+
...meta?.flavor ? { flavor: meta.flavor } : {},
|
|
6762
|
+
...meta?.summary?.text ? { title: meta.summary.text } : {},
|
|
6763
|
+
...meta?.variant ?? child.variant ? { variant: meta?.variant ?? child.variant } : {},
|
|
6764
|
+
edits: tracker.editsOf(id, at)
|
|
6765
|
+
});
|
|
6766
|
+
}
|
|
6767
|
+
for (const mirror of getMirrors()) {
|
|
6768
|
+
if (out.has(mirror.sessionId)) continue;
|
|
6769
|
+
out.set(mirror.sessionId, {
|
|
6770
|
+
sessionId: mirror.sessionId,
|
|
6771
|
+
kind: "mirror",
|
|
6772
|
+
flavor: "terminal-mirror",
|
|
6773
|
+
...mirror.cwd ? { cwd: mirror.cwd } : {},
|
|
6774
|
+
...mirror.title ? { title: mirror.title } : {},
|
|
6775
|
+
edits: tracker.editsOf(mirror.sessionId, at)
|
|
6776
|
+
});
|
|
6777
|
+
}
|
|
6778
|
+
return out;
|
|
6779
|
+
};
|
|
6780
|
+
const deliver = async (recipient, peer, peerId, paths, peerEditedAgoMs, noticeId) => {
|
|
6781
|
+
if (recipient.kind === "mirror") return;
|
|
6782
|
+
const persisted = readPersisted()[recipient.sessionId];
|
|
6783
|
+
if (!persisted) {
|
|
6784
|
+
logger.debug(`[PEERS] no key for ${recipient.sessionId}; conflict notice ${noticeId} not delivered`);
|
|
6785
|
+
return;
|
|
6786
|
+
}
|
|
6787
|
+
const peerSender = senderOf(peer, peerId);
|
|
6788
|
+
if (!peerSender.title && peer?.kind === "managed") {
|
|
6789
|
+
const peerKey = readPersisted()[peer.sessionId];
|
|
6790
|
+
if (peerKey) {
|
|
6791
|
+
try {
|
|
6792
|
+
peerSender.title = await liveTitle(peer.sessionId, peerKey);
|
|
6793
|
+
} catch {
|
|
6794
|
+
}
|
|
6795
|
+
}
|
|
6796
|
+
}
|
|
6797
|
+
const text = formatEditConflictNotice({ id: noticeId, path: paths[0], paths, peer: peerSender, peerEditedAgoMs, windowMs });
|
|
6798
|
+
try {
|
|
6799
|
+
await send(recipient.sessionId, persisted, text, "edit-conflict", {
|
|
6800
|
+
sentFrom: SESSION_PEER_SENT_FROM,
|
|
6801
|
+
delivery: "steer",
|
|
6802
|
+
localId: `edit-conflict-${noticeId}-${recipient.sessionId}`
|
|
6803
|
+
});
|
|
6804
|
+
logger.debug(`[PEERS] conflict notice ${noticeId} \u2192 ${recipient.sessionId} (${paths.length} file(s), first ${paths[0]}, peer ${peerId})`);
|
|
6805
|
+
} catch (error) {
|
|
6806
|
+
logger.debug(`[PEERS] conflict notice ${noticeId} \u2192 ${recipient.sessionId} failed:`, error);
|
|
6807
|
+
}
|
|
6808
|
+
};
|
|
6809
|
+
const flushPair = async (key) => {
|
|
6810
|
+
const pair = pending.get(key);
|
|
6811
|
+
if (!pair) return;
|
|
6812
|
+
pending.delete(key);
|
|
6813
|
+
const at = now();
|
|
6814
|
+
const sent = (noticesSent.get(key) ?? []).filter((t) => at - t <= windowMs);
|
|
6815
|
+
if (sent.length >= maxNoticesPerPair) {
|
|
6816
|
+
noticesSent.set(key, sent);
|
|
6817
|
+
logger.debug(`[PEERS] pair ${pair.a}/${pair.b}: ${maxNoticesPerPair} notices already sent this window; ${pair.paths.size} conflicting path(s) not announced`);
|
|
6818
|
+
return;
|
|
6819
|
+
}
|
|
6820
|
+
sent.push(at);
|
|
6821
|
+
noticesSent.set(key, sent);
|
|
6822
|
+
const sessions = snapshot();
|
|
6823
|
+
const a = sessions.get(pair.a);
|
|
6824
|
+
const b = sessions.get(pair.b);
|
|
6825
|
+
const paths = [...pair.paths.keys()];
|
|
6826
|
+
const noticeId = randomUUID().slice(0, 8);
|
|
6827
|
+
const latestBy = (sessionId) => Math.max(...[...pair.paths.values()].map((c) => c.editor.sessionId === sessionId ? c.editor.at : c.peer.at));
|
|
6828
|
+
const agoFor = (recipientId, otherId) => Math.max(0, latestBy(recipientId) - latestBy(otherId));
|
|
6829
|
+
if (a) await deliver(a, b, pair.b, paths, agoFor(pair.a, pair.b), noticeId);
|
|
6830
|
+
if (b) await deliver(b, a, pair.a, paths, agoFor(pair.b, pair.a), noticeId);
|
|
6831
|
+
};
|
|
6832
|
+
const enqueue = (conflict) => {
|
|
6833
|
+
const key = pairKey(conflict.editor.sessionId, conflict.peer.sessionId);
|
|
6834
|
+
let pair = pending.get(key);
|
|
6835
|
+
if (!pair) {
|
|
6836
|
+
pair = { a: conflict.editor.sessionId, b: conflict.peer.sessionId, paths: /* @__PURE__ */ new Map(), timer: null };
|
|
6837
|
+
pair.timer = schedule(() => {
|
|
6838
|
+
inflight = inflight.then(() => flushPair(key)).catch(() => {
|
|
6839
|
+
});
|
|
6840
|
+
}, coalesceMs);
|
|
6841
|
+
pending.set(key, pair);
|
|
6842
|
+
}
|
|
6843
|
+
pair.paths.set(conflict.editor.path, conflict);
|
|
6844
|
+
};
|
|
6845
|
+
return {
|
|
6846
|
+
list: () => [...snapshot().values()],
|
|
6847
|
+
onEdit(edit) {
|
|
6848
|
+
const sessions = snapshot();
|
|
6849
|
+
const self = sessions.get(edit.sessionId);
|
|
6850
|
+
if (!self) {
|
|
6851
|
+
logger.debug(`[PEERS] edit from unknown/dead session ${edit.sessionId} ignored`);
|
|
6852
|
+
return [];
|
|
6853
|
+
}
|
|
6854
|
+
const current = now();
|
|
6855
|
+
const at = typeof edit.at === "number" && Number.isFinite(edit.at) ? Math.min(edit.at, current) : current;
|
|
6856
|
+
if (current - at > windowMs) {
|
|
6857
|
+
logger.debug(`[PEERS] edit from ${edit.sessionId} on ${edit.path} is ${Math.round((current - at) / 6e4)}m old (replayed history); ignored`);
|
|
6858
|
+
return [];
|
|
6859
|
+
}
|
|
6860
|
+
const cwd = edit.cwd ?? self.cwd ?? process.cwd();
|
|
6861
|
+
const path = normalize(edit.path, cwd);
|
|
6862
|
+
const conflicts = tracker.record({ sessionId: edit.sessionId, path, tool: edit.tool, at }, (id) => sessions.has(id));
|
|
6863
|
+
for (const conflict of conflicts) enqueue(conflict);
|
|
6864
|
+
return conflicts;
|
|
6865
|
+
},
|
|
6866
|
+
async flush() {
|
|
6867
|
+
for (const key of [...pending.keys()]) {
|
|
6868
|
+
const pair = pending.get(key);
|
|
6869
|
+
if (pair?.timer && typeof pair.timer === "object") clearTimeout(pair.timer);
|
|
6870
|
+
inflight = inflight.then(() => flushPair(key)).catch(() => {
|
|
6871
|
+
});
|
|
6872
|
+
}
|
|
6873
|
+
await inflight;
|
|
6874
|
+
},
|
|
6875
|
+
forget(sessionId) {
|
|
6876
|
+
tracker.forget(sessionId);
|
|
6877
|
+
for (const [key, pair] of pending) {
|
|
6878
|
+
if (pair.a === sessionId || pair.b === sessionId) {
|
|
6879
|
+
if (pair.timer && typeof pair.timer === "object") clearTimeout(pair.timer);
|
|
6880
|
+
pending.delete(key);
|
|
6881
|
+
}
|
|
6882
|
+
}
|
|
6883
|
+
},
|
|
6884
|
+
setMirrorSource(source) {
|
|
6885
|
+
getMirrors = source;
|
|
6886
|
+
}
|
|
6887
|
+
};
|
|
6888
|
+
}
|
|
6889
|
+
|
|
6632
6890
|
const ASSISTANT_CLAUDE_MD = `# very-happy \u8C03\u5EA6\u4E2D\u5FC3\uFF08\u8BED\u97F3\u52A9\u624B\uFF09
|
|
6633
6891
|
|
|
6634
6892
|
\u4F60\u662F very-happy \u7684\u8C03\u5EA6\u4E2D\u5FC3\uFF0C\u8DD1\u5728\u7528\u6237\u81EA\u5DF1\u7684\u673A\u5668\u4E0A\u3002\u7528\u6237\u4E3B\u8981\u901A\u8FC7\u8BED\u97F3\u8DDF\u4F60\u8BF4\u8BDD\uFF0C
|
|
@@ -8548,6 +8806,73 @@ class ClaudeAuthService {
|
|
|
8548
8806
|
}
|
|
8549
8807
|
}
|
|
8550
8808
|
|
|
8809
|
+
const CLAUDE_EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
|
|
8810
|
+
function pathOf(input) {
|
|
8811
|
+
if (!input || typeof input !== "object") return null;
|
|
8812
|
+
const record = input;
|
|
8813
|
+
const candidate = record.file_path ?? record.notebook_path;
|
|
8814
|
+
return typeof candidate === "string" && candidate.trim() !== "" ? candidate : null;
|
|
8815
|
+
}
|
|
8816
|
+
function extractClaudeEditPaths(body) {
|
|
8817
|
+
if (!body || typeof body !== "object") return [];
|
|
8818
|
+
const line = body;
|
|
8819
|
+
if (line.type !== "assistant") return [];
|
|
8820
|
+
const content = line.message?.content;
|
|
8821
|
+
if (!Array.isArray(content)) return [];
|
|
8822
|
+
const stamp = line.timestamp;
|
|
8823
|
+
const at = typeof stamp === "string" ? Date.parse(stamp) : typeof stamp === "number" ? stamp : NaN;
|
|
8824
|
+
const edits = [];
|
|
8825
|
+
for (const block of content) {
|
|
8826
|
+
if (!block || typeof block !== "object") continue;
|
|
8827
|
+
const b = block;
|
|
8828
|
+
if (b.type !== "tool_use" || typeof b.name !== "string" || !CLAUDE_EDIT_TOOLS.has(b.name)) continue;
|
|
8829
|
+
const path = pathOf(b.input);
|
|
8830
|
+
if (path) edits.push({ path, tool: b.name, ...Number.isFinite(at) ? { at } : {} });
|
|
8831
|
+
}
|
|
8832
|
+
return edits;
|
|
8833
|
+
}
|
|
8834
|
+
function extractCodexPatchPaths(changes) {
|
|
8835
|
+
if (!changes || typeof changes !== "object" || Array.isArray(changes)) return [];
|
|
8836
|
+
return Object.keys(changes).filter((path) => path.trim() !== "").map((path) => ({ path, tool: "CodexPatch" }));
|
|
8837
|
+
}
|
|
8838
|
+
function extractAcpEditPaths(toolKind, args) {
|
|
8839
|
+
if (!args || typeof args !== "object") return [];
|
|
8840
|
+
const a = args;
|
|
8841
|
+
if (a.piTool === "write" || a.piTool === "edit") {
|
|
8842
|
+
const raw = a.rawInput;
|
|
8843
|
+
const path = raw && typeof raw.path === "string" && raw.path.trim() !== "" ? raw.path : null;
|
|
8844
|
+
return path ? [{ path, tool: a.piTool }] : [];
|
|
8845
|
+
}
|
|
8846
|
+
if (typeof a.piTool === "string") return [];
|
|
8847
|
+
if (toolKind !== "edit" || !Array.isArray(a.locations)) return [];
|
|
8848
|
+
const edits = [];
|
|
8849
|
+
for (const location of a.locations) {
|
|
8850
|
+
const path = location && typeof location === "object" ? location.path : void 0;
|
|
8851
|
+
if (typeof path === "string" && path.trim() !== "" && !edits.some((e) => e.path === path)) edits.push({ path, tool: "edit" });
|
|
8852
|
+
}
|
|
8853
|
+
return edits;
|
|
8854
|
+
}
|
|
8855
|
+
class EditReportThrottle {
|
|
8856
|
+
constructor(intervalMs = 5e3) {
|
|
8857
|
+
this.intervalMs = intervalMs;
|
|
8858
|
+
}
|
|
8859
|
+
lastReportedAt = /* @__PURE__ */ new Map();
|
|
8860
|
+
/** Returns the edits that should be reported now and remembers them. */
|
|
8861
|
+
take(edits, now = Date.now()) {
|
|
8862
|
+
const due = [];
|
|
8863
|
+
for (const edit of edits) {
|
|
8864
|
+
const last = this.lastReportedAt.get(edit.path);
|
|
8865
|
+
if (last !== void 0 && now - last < this.intervalMs) continue;
|
|
8866
|
+
this.lastReportedAt.set(edit.path, now);
|
|
8867
|
+
due.push(edit);
|
|
8868
|
+
}
|
|
8869
|
+
if (this.lastReportedAt.size > 512) {
|
|
8870
|
+
for (const [path, at] of this.lastReportedAt) if (now - at > this.intervalMs * 10) this.lastReportedAt.delete(path);
|
|
8871
|
+
}
|
|
8872
|
+
return due;
|
|
8873
|
+
}
|
|
8874
|
+
}
|
|
8875
|
+
|
|
8551
8876
|
const INTERNAL_CLAUDE_EVENT_TYPES = /* @__PURE__ */ new Set(["file-history-snapshot", "change", "queue-operation"]);
|
|
8552
8877
|
function parseLines(lines) {
|
|
8553
8878
|
const out = [];
|
|
@@ -8584,12 +8909,15 @@ function createMirrorScanner(opts) {
|
|
|
8584
8909
|
}
|
|
8585
8910
|
tail.offset = 0;
|
|
8586
8911
|
}
|
|
8912
|
+
let replaced = false;
|
|
8587
8913
|
if (tail.offset > size) {
|
|
8588
8914
|
logger.debug(`[MIRROR SCANNER] ${tail.path}: offset ${tail.offset} > size ${size} \u2014 file replaced, resetting (localId idempotency absorbs replays)`);
|
|
8589
8915
|
tail.offset = 0;
|
|
8916
|
+
replaced = true;
|
|
8590
8917
|
}
|
|
8591
8918
|
if (tail.offset === size) return;
|
|
8592
8919
|
const isFirstBackfillRead = tail.mode === "backfill-tail" && tail.offset === 0;
|
|
8920
|
+
const replay = isFirstBackfillRead || replaced;
|
|
8593
8921
|
const handle = await open$1(tail.path, "r");
|
|
8594
8922
|
let buf;
|
|
8595
8923
|
try {
|
|
@@ -8609,7 +8937,7 @@ function createMirrorScanner(opts) {
|
|
|
8609
8937
|
if (decision.truncated) opts.events.onBackfillTruncated?.();
|
|
8610
8938
|
tail.mode = "from-eof";
|
|
8611
8939
|
}
|
|
8612
|
-
if (messages.length > 0) opts.events.onMessages(messages);
|
|
8940
|
+
if (messages.length > 0) opts.events.onMessages(messages, { replay });
|
|
8613
8941
|
};
|
|
8614
8942
|
const sync = new InvalidateSync(async () => {
|
|
8615
8943
|
for (const tail of files.values()) {
|
|
@@ -8732,8 +9060,17 @@ function createMirrorManager(deps) {
|
|
|
8732
9060
|
binding.client.updateMetadata((current) => ({ ...current, ...patch }));
|
|
8733
9061
|
persistBinding(binding);
|
|
8734
9062
|
};
|
|
8735
|
-
const sendMessages = (binding, messages) => {
|
|
9063
|
+
const sendMessages = (binding, messages, context = { replay: false }) => {
|
|
8736
9064
|
for (const message of messages) {
|
|
9065
|
+
if (deps.onEdit && binding.status === "active" && !context.replay) {
|
|
9066
|
+
for (const edit of extractClaudeEditPaths(message)) {
|
|
9067
|
+
try {
|
|
9068
|
+
deps.onEdit({ sessionId: binding.happySessionId, path: edit.path, tool: edit.tool, cwd: binding.metadata.path, ...edit.at !== void 0 ? { at: edit.at } : {} });
|
|
9069
|
+
} catch (error) {
|
|
9070
|
+
logger.debug(`[MIRROR] edit tap failed for terminal ${binding.terminalId}:`, error);
|
|
9071
|
+
}
|
|
9072
|
+
}
|
|
9073
|
+
}
|
|
8737
9074
|
const key = mirrorLineKey(message);
|
|
8738
9075
|
binding.client.sendClaudeSessionMessage(message, key ? { localIdFor: (envelopeIndex) => mirrorLocalId(key, envelopeIndex) } : void 0);
|
|
8739
9076
|
}
|
|
@@ -8742,7 +9079,7 @@ function createMirrorManager(deps) {
|
|
|
8742
9079
|
const scanner = createMirrorScanner({
|
|
8743
9080
|
backfillLines,
|
|
8744
9081
|
events: {
|
|
8745
|
-
onMessages: (messages) => sendMessages(binding, messages),
|
|
9082
|
+
onMessages: (messages, context) => sendMessages(binding, messages, context),
|
|
8746
9083
|
onBackfillTruncated: opts.withTruncationNotice ? () => binding.client.sendSessionEvent({
|
|
8747
9084
|
type: "message",
|
|
8748
9085
|
message: `\u53EA\u8BFB\u955C\u50CF\u53EA\u56DE\u704C\u4E86\u6700\u8FD1 ${backfillLines} \u6761\uFF0C\u66F4\u65E9\u5185\u5BB9\u8BF7\u5728\u7EC8\u7AEF\u91CC\u56DE\u770B`
|
|
@@ -8765,6 +9102,10 @@ function createMirrorManager(deps) {
|
|
|
8765
9102
|
await binding.scanner?.cleanup();
|
|
8766
9103
|
binding.scanner = null;
|
|
8767
9104
|
binding.client.closeClaudeSessionTurn("completed");
|
|
9105
|
+
try {
|
|
9106
|
+
deps.onSessionEnded?.(binding.happySessionId);
|
|
9107
|
+
} catch {
|
|
9108
|
+
}
|
|
8768
9109
|
updateBindingMetadata(binding, {
|
|
8769
9110
|
lifecycleState: "archived",
|
|
8770
9111
|
lifecycleStateSince: Date.now(),
|
|
@@ -8772,9 +9113,9 @@ function createMirrorManager(deps) {
|
|
|
8772
9113
|
archiveReason: reason
|
|
8773
9114
|
});
|
|
8774
9115
|
try {
|
|
8775
|
-
await deps.api.
|
|
9116
|
+
await deps.api.archiveSession(binding.happySessionId);
|
|
8776
9117
|
} catch (error) {
|
|
8777
|
-
logger.debug(`[MIRROR]
|
|
9118
|
+
logger.debug(`[MIRROR] archiveSession failed for ${binding.happySessionId}:`, error);
|
|
8778
9119
|
}
|
|
8779
9120
|
deps.onBindingsChanged?.();
|
|
8780
9121
|
};
|
|
@@ -8977,6 +9318,13 @@ function createMirrorManager(deps) {
|
|
|
8977
9318
|
}
|
|
8978
9319
|
};
|
|
8979
9320
|
return {
|
|
9321
|
+
listActiveMirrors() {
|
|
9322
|
+
return [...bindings.values()].filter((binding) => binding.status === "active").map((binding) => ({
|
|
9323
|
+
sessionId: binding.happySessionId,
|
|
9324
|
+
...binding.metadata.path ? { cwd: binding.metadata.path } : {},
|
|
9325
|
+
...binding.metadata.summary?.text ? { title: binding.metadata.summary.text } : {}
|
|
9326
|
+
}));
|
|
9327
|
+
},
|
|
8980
9328
|
handleHookPayload(body) {
|
|
8981
9329
|
const event = parseTerminalHookPayload(body);
|
|
8982
9330
|
if (!event) {
|
|
@@ -9443,683 +9791,6 @@ Delegate from this working directory unless a different repository was explicitl
|
|
|
9443
9791
|
${goal}`;
|
|
9444
9792
|
}
|
|
9445
9793
|
|
|
9446
|
-
const MAX_LINE_CHARS = 500;
|
|
9447
|
-
const MAX_ARGS_CHARS = 160;
|
|
9448
|
-
const DEFAULT_LIMITS = { line: MAX_LINE_CHARS, args: MAX_ARGS_CHARS };
|
|
9449
|
-
const NO_LIMITS = { line: Number.POSITIVE_INFINITY, args: Number.POSITIVE_INFINITY };
|
|
9450
|
-
function truncateText(text, max = MAX_LINE_CHARS) {
|
|
9451
|
-
const chars = Array.from(text);
|
|
9452
|
-
if (chars.length <= max) return text;
|
|
9453
|
-
return chars.slice(0, max).join("") + ` \u2026[+${chars.length - max} chars]`;
|
|
9454
|
-
}
|
|
9455
|
-
function squash(text) {
|
|
9456
|
-
return text.replace(/\s+/g, " ").trim();
|
|
9457
|
-
}
|
|
9458
|
-
function safeJson(value, max) {
|
|
9459
|
-
try {
|
|
9460
|
-
return truncateText(JSON.stringify(value) ?? "", max);
|
|
9461
|
-
} catch {
|
|
9462
|
-
return "[unserializable]";
|
|
9463
|
-
}
|
|
9464
|
-
}
|
|
9465
|
-
function formatMessageBody(body, limits = DEFAULT_LIMITS) {
|
|
9466
|
-
if (!body || typeof body !== "object") return null;
|
|
9467
|
-
const b = body;
|
|
9468
|
-
if (b.role === "user" && b.content?.type === "text" && typeof b.content.text === "string") {
|
|
9469
|
-
const text = squash(b.content.text);
|
|
9470
|
-
return text.length > 0 ? `user: ${truncateText(text, limits.line)}` : null;
|
|
9471
|
-
}
|
|
9472
|
-
if (b.role === "session" && b.content && typeof b.content === "object") {
|
|
9473
|
-
const env = b.content;
|
|
9474
|
-
const ev = env.ev;
|
|
9475
|
-
if (!ev || typeof ev.t !== "string") return null;
|
|
9476
|
-
switch (ev.t) {
|
|
9477
|
-
case "text": {
|
|
9478
|
-
if (ev.thinking) return null;
|
|
9479
|
-
const text = squash(String(ev.text ?? ""));
|
|
9480
|
-
if (text.length === 0) return null;
|
|
9481
|
-
const role = env.role === "user" ? "user" : "assistant";
|
|
9482
|
-
return `${role}: ${truncateText(text, limits.line)}`;
|
|
9483
|
-
}
|
|
9484
|
-
case "tool-call-start": {
|
|
9485
|
-
const name = String(ev.name ?? "tool");
|
|
9486
|
-
const title = squash(String(ev.title ?? ""));
|
|
9487
|
-
const args = ev.args ? safeJson(ev.args, limits.args) : "";
|
|
9488
|
-
const detail = title || args;
|
|
9489
|
-
return `[tool] ${name}${detail ? `: ${truncateText(detail, limits.args)}` : ""}`;
|
|
9490
|
-
}
|
|
9491
|
-
case "service": {
|
|
9492
|
-
const text = squash(String(ev.text ?? ""));
|
|
9493
|
-
return text.length > 0 ? `[service] ${truncateText(text, limits.args)}` : null;
|
|
9494
|
-
}
|
|
9495
|
-
case "turn-end": {
|
|
9496
|
-
if (ev.status && ev.status !== "completed") return `[turn ${ev.status}]`;
|
|
9497
|
-
return null;
|
|
9498
|
-
}
|
|
9499
|
-
default:
|
|
9500
|
-
return null;
|
|
9501
|
-
}
|
|
9502
|
-
}
|
|
9503
|
-
if (b.role === "agent" && b.content?.type === "acp" && b.content.data && typeof b.content.data === "object") {
|
|
9504
|
-
const d = b.content.data;
|
|
9505
|
-
switch (d.type) {
|
|
9506
|
-
case "message": {
|
|
9507
|
-
const text = squash(String(d.message ?? ""));
|
|
9508
|
-
return text.length > 0 ? `assistant: ${truncateText(text, limits.line)}` : null;
|
|
9509
|
-
}
|
|
9510
|
-
case "tool-call":
|
|
9511
|
-
return `[tool] ${String(d.name ?? "tool")}: ${safeJson(d.input, limits.args)}`;
|
|
9512
|
-
case "permission-request":
|
|
9513
|
-
return `[permission] ${String(d.toolName ?? "")}: ${truncateText(squash(String(d.description ?? "")), limits.args)}`;
|
|
9514
|
-
default:
|
|
9515
|
-
return null;
|
|
9516
|
-
}
|
|
9517
|
-
}
|
|
9518
|
-
if (b.role === "agent" && b.content?.type === "output") {
|
|
9519
|
-
const msg = b.content.data?.message;
|
|
9520
|
-
if (msg && Array.isArray(msg.content)) {
|
|
9521
|
-
const texts = msg.content.filter((c) => c?.type === "text" && typeof c.text === "string").map((c) => squash(c.text)).filter((t) => t.length > 0);
|
|
9522
|
-
if (texts.length > 0) {
|
|
9523
|
-
const role = msg.role === "user" ? "user" : "assistant";
|
|
9524
|
-
return `${role}: ${truncateText(texts.join(" "), limits.line)}`;
|
|
9525
|
-
}
|
|
9526
|
-
}
|
|
9527
|
-
return null;
|
|
9528
|
-
}
|
|
9529
|
-
return null;
|
|
9530
|
-
}
|
|
9531
|
-
function formatTranscript(bodies, options = {}) {
|
|
9532
|
-
const limits = options.full ? NO_LIMITS : DEFAULT_LIMITS;
|
|
9533
|
-
const lines = [];
|
|
9534
|
-
for (const body of bodies) {
|
|
9535
|
-
const line = formatMessageBody(body, limits);
|
|
9536
|
-
if (line !== null) lines.push(line);
|
|
9537
|
-
}
|
|
9538
|
-
return lines.join("\n");
|
|
9539
|
-
}
|
|
9540
|
-
|
|
9541
|
-
function asRecord(value) {
|
|
9542
|
-
return value && typeof value === "object" ? value : null;
|
|
9543
|
-
}
|
|
9544
|
-
function sessionEnvelope(body) {
|
|
9545
|
-
const b = asRecord(body);
|
|
9546
|
-
if (!b || b.role !== "session") return null;
|
|
9547
|
-
const env = asRecord(b.content);
|
|
9548
|
-
if (!env) return null;
|
|
9549
|
-
const ev = asRecord(env.ev);
|
|
9550
|
-
return ev && typeof ev.t === "string" ? { role: env.role, ev } : null;
|
|
9551
|
-
}
|
|
9552
|
-
function isUserPrompt(body) {
|
|
9553
|
-
const b = asRecord(body);
|
|
9554
|
-
return !!b && b.role === "user" && b.content?.type === "text" && typeof b.content.text === "string";
|
|
9555
|
-
}
|
|
9556
|
-
function agentPiece(body) {
|
|
9557
|
-
const env = sessionEnvelope(body);
|
|
9558
|
-
if (env && env.role !== "user") {
|
|
9559
|
-
if (env.ev.t === "text" && !env.ev.thinking) {
|
|
9560
|
-
const text = String(env.ev.text ?? "");
|
|
9561
|
-
return text.trim().length > 0 ? { kind: "text", text } : null;
|
|
9562
|
-
}
|
|
9563
|
-
if (env.ev.t === "tool-call-start") return { kind: "tool" };
|
|
9564
|
-
return null;
|
|
9565
|
-
}
|
|
9566
|
-
const b = asRecord(body);
|
|
9567
|
-
if (b?.role === "agent" && b.content?.type === "acp") {
|
|
9568
|
-
const d = asRecord(b.content.data);
|
|
9569
|
-
if (d?.type === "message") {
|
|
9570
|
-
const text = String(d.message ?? "");
|
|
9571
|
-
return text.trim().length > 0 ? { kind: "text", text } : null;
|
|
9572
|
-
}
|
|
9573
|
-
if (d?.type === "tool-call") return { kind: "tool" };
|
|
9574
|
-
}
|
|
9575
|
-
return null;
|
|
9576
|
-
}
|
|
9577
|
-
function userPromptText(body) {
|
|
9578
|
-
return isUserPrompt(body) ? body.content.text : null;
|
|
9579
|
-
}
|
|
9580
|
-
function analyzeLatestTurn(entries, options = {}) {
|
|
9581
|
-
const lastSeq = entries.length > 0 ? entries[entries.length - 1].seq : 0;
|
|
9582
|
-
let userIndex = -1;
|
|
9583
|
-
for (let i = entries.length - 1; i >= 0; i--) {
|
|
9584
|
-
if (isUserPrompt(entries[i].body) && (!options.anchor || options.anchor(entries[i].body))) {
|
|
9585
|
-
userIndex = i;
|
|
9586
|
-
break;
|
|
9587
|
-
}
|
|
9588
|
-
}
|
|
9589
|
-
let ended = false;
|
|
9590
|
-
let status = null;
|
|
9591
|
-
let error = null;
|
|
9592
|
-
let texts = [];
|
|
9593
|
-
let lastMarker = null;
|
|
9594
|
-
let startedAfterPrompt = false;
|
|
9595
|
-
const emitsStarts = entries.some((entry) => sessionEnvelope(entry.body)?.ev?.t === "turn-start");
|
|
9596
|
-
for (let i = userIndex + 1; i < entries.length; i++) {
|
|
9597
|
-
const body = entries[i].body;
|
|
9598
|
-
if (options.anchor && isUserPrompt(body)) {
|
|
9599
|
-
if (ended) break;
|
|
9600
|
-
continue;
|
|
9601
|
-
}
|
|
9602
|
-
const env = sessionEnvelope(body);
|
|
9603
|
-
if (env?.ev?.t === "turn-start") {
|
|
9604
|
-
lastMarker = "start";
|
|
9605
|
-
startedAfterPrompt = true;
|
|
9606
|
-
ended = false;
|
|
9607
|
-
status = null;
|
|
9608
|
-
error = null;
|
|
9609
|
-
texts = [];
|
|
9610
|
-
continue;
|
|
9611
|
-
}
|
|
9612
|
-
if (env?.ev?.t === "turn-end") {
|
|
9613
|
-
lastMarker = "end";
|
|
9614
|
-
if (userIndex >= 0 && emitsStarts && !startedAfterPrompt) continue;
|
|
9615
|
-
ended = true;
|
|
9616
|
-
status = typeof env.ev.status === "string" ? env.ev.status : null;
|
|
9617
|
-
error = typeof env.ev.error === "string" ? env.ev.error : null;
|
|
9618
|
-
continue;
|
|
9619
|
-
}
|
|
9620
|
-
const piece = agentPiece(body);
|
|
9621
|
-
if (!piece) continue;
|
|
9622
|
-
if (piece.kind === "tool") texts = [];
|
|
9623
|
-
else texts.push(piece.text);
|
|
9624
|
-
}
|
|
9625
|
-
if (userIndex < 0) ended = lastMarker === "end";
|
|
9626
|
-
return {
|
|
9627
|
-
userSeq: userIndex >= 0 ? entries[userIndex].seq : null,
|
|
9628
|
-
ended,
|
|
9629
|
-
status: ended ? status : null,
|
|
9630
|
-
error: ended ? error : null,
|
|
9631
|
-
answer: texts.join("\n\n").trim(),
|
|
9632
|
-
lastSeq
|
|
9633
|
-
};
|
|
9634
|
-
}
|
|
9635
|
-
|
|
9636
|
-
const USER_SOCKET_CONNECT_TIMEOUT_MS = 15e3;
|
|
9637
|
-
async function openUserScopedSocket(token, options = {}) {
|
|
9638
|
-
const socket = io(configuration.serverUrl, {
|
|
9639
|
-
auth: {
|
|
9640
|
-
token,
|
|
9641
|
-
clientType: "user-scoped",
|
|
9642
|
-
happyClient: `cli-session-ops/${configuration.currentCliVersion}`
|
|
9643
|
-
},
|
|
9644
|
-
path: "/v1/updates",
|
|
9645
|
-
transports: ["websocket"],
|
|
9646
|
-
reconnection: false,
|
|
9647
|
-
autoConnect: false
|
|
9648
|
-
});
|
|
9649
|
-
await new Promise((resolve, reject) => {
|
|
9650
|
-
const timer = setTimeout(() => {
|
|
9651
|
-
socket.close();
|
|
9652
|
-
reject(new Error(`Timed out connecting to ${configuration.serverUrl}`));
|
|
9653
|
-
}, options.connectTimeoutMs ?? USER_SOCKET_CONNECT_TIMEOUT_MS);
|
|
9654
|
-
socket.once("connect", () => {
|
|
9655
|
-
clearTimeout(timer);
|
|
9656
|
-
resolve();
|
|
9657
|
-
});
|
|
9658
|
-
socket.once("connect_error", (error) => {
|
|
9659
|
-
clearTimeout(timer);
|
|
9660
|
-
socket.close();
|
|
9661
|
-
reject(new Error(`Could not connect to ${configuration.serverUrl}: ${error.message}`));
|
|
9662
|
-
});
|
|
9663
|
-
socket.connect();
|
|
9664
|
-
});
|
|
9665
|
-
return {
|
|
9666
|
-
async rpcCall(payload, timeoutMs) {
|
|
9667
|
-
const ack = await socket.timeout(timeoutMs).emitWithAck("rpc-call", payload);
|
|
9668
|
-
return ack;
|
|
9669
|
-
},
|
|
9670
|
-
close() {
|
|
9671
|
-
socket.close();
|
|
9672
|
-
}
|
|
9673
|
-
};
|
|
9674
|
-
}
|
|
9675
|
-
|
|
9676
|
-
const RPC_RATE_LIMIT_ERROR = "RPC rate limit reached";
|
|
9677
|
-
const RPC_ACCOUNT_RATE_LIMIT_ERROR = "RPC account rate limit reached";
|
|
9678
|
-
const RPC_RATE_RETRY_MIN_DELAY_MS = 2e3;
|
|
9679
|
-
const RPC_RATE_RETRY_MAX_DELAY_MS = 6e4;
|
|
9680
|
-
const RPC_RATE_MAX_RETRIES = 2;
|
|
9681
|
-
function isRpcRateLimitAck(ack) {
|
|
9682
|
-
if (!ack || typeof ack !== "object") return false;
|
|
9683
|
-
const a = ack;
|
|
9684
|
-
if (a.ok !== false) return false;
|
|
9685
|
-
if (a.code === "rpc_rate_limited" || a.code === "rpc_account_rate_limited") return true;
|
|
9686
|
-
return a.error === RPC_RATE_LIMIT_ERROR || a.error === RPC_ACCOUNT_RATE_LIMIT_ERROR;
|
|
9687
|
-
}
|
|
9688
|
-
function rpcRateRetryDelayMs(attempt, retryAfterMs, random = Math.random()) {
|
|
9689
|
-
if (typeof retryAfterMs === "number" && Number.isFinite(retryAfterMs) && retryAfterMs > 0) {
|
|
9690
|
-
return Math.min(RPC_RATE_RETRY_MAX_DELAY_MS, Math.round(retryAfterMs * (1 + random * 0.5)));
|
|
9691
|
-
}
|
|
9692
|
-
const exponent = Math.max(0, Math.min(attempt, 32) - 1);
|
|
9693
|
-
const ceiling = Math.min(RPC_RATE_RETRY_MIN_DELAY_MS * 2 ** exponent, RPC_RATE_RETRY_MAX_DELAY_MS);
|
|
9694
|
-
return Math.round(RPC_RATE_RETRY_MIN_DELAY_MS + random * (ceiling - RPC_RATE_RETRY_MIN_DELAY_MS));
|
|
9695
|
-
}
|
|
9696
|
-
|
|
9697
|
-
const PERMISSION_OPS_CLIENT = "session-ops";
|
|
9698
|
-
const PERMISSION_RPC_TIMEOUT_MS = 3e4;
|
|
9699
|
-
const SETTLE_TIMEOUT_MS = 5e3;
|
|
9700
|
-
const SETTLE_POLL_MS = 500;
|
|
9701
|
-
function buildPermissionRpcPayload(requestId, verdict) {
|
|
9702
|
-
if (verdict.kind === "approve") {
|
|
9703
|
-
return {
|
|
9704
|
-
id: requestId,
|
|
9705
|
-
approved: true,
|
|
9706
|
-
decision: verdict.forSession ? "approved_for_session" : "approved"
|
|
9707
|
-
};
|
|
9708
|
-
}
|
|
9709
|
-
return {
|
|
9710
|
-
id: requestId,
|
|
9711
|
-
approved: false,
|
|
9712
|
-
decision: "denied",
|
|
9713
|
-
...verdict.reason ? { reason: verdict.reason } : {}
|
|
9714
|
-
};
|
|
9715
|
-
}
|
|
9716
|
-
function interpretPermissionAck(ack, decryptResult) {
|
|
9717
|
-
if (!ack.ok) {
|
|
9718
|
-
const message = ack.error ?? "RPC call failed";
|
|
9719
|
-
if (message === "RPC method not available") {
|
|
9720
|
-
return { status: "offline", message: "no running wrapper has registered the permission RPC for this session" };
|
|
9721
|
-
}
|
|
9722
|
-
if (message === "RPC target disconnected") {
|
|
9723
|
-
return { status: "offline", message: "the wrapper disconnected while the permission RPC was in flight" };
|
|
9724
|
-
}
|
|
9725
|
-
if (/timed? ?out/i.test(message)) {
|
|
9726
|
-
return { status: "timeout", message };
|
|
9727
|
-
}
|
|
9728
|
-
if (isRpcRateLimitAck(ack)) {
|
|
9729
|
-
return { status: "rate-limited", message, retryAfterMs: rpcRateRetryDelayMs(1, ack.retryAfterMs, 0) };
|
|
9730
|
-
}
|
|
9731
|
-
return { status: "rejected", message };
|
|
9732
|
-
}
|
|
9733
|
-
let body = null;
|
|
9734
|
-
if (typeof ack.result === "string" && ack.result.length > 0) {
|
|
9735
|
-
try {
|
|
9736
|
-
body = decryptResult(ack.result);
|
|
9737
|
-
} catch {
|
|
9738
|
-
body = null;
|
|
9739
|
-
}
|
|
9740
|
-
}
|
|
9741
|
-
if (body && typeof body === "object" && typeof body.error === "string") {
|
|
9742
|
-
return { status: "handler-error", message: body.error };
|
|
9743
|
-
}
|
|
9744
|
-
return { status: "acknowledged" };
|
|
9745
|
-
}
|
|
9746
|
-
function pendingRequestsOf(agentState, now) {
|
|
9747
|
-
const requests = agentState?.requests;
|
|
9748
|
-
if (!requests || typeof requests !== "object") return [];
|
|
9749
|
-
return Object.entries(requests).map(([id, request]) => ({
|
|
9750
|
-
id,
|
|
9751
|
-
tool: typeof request?.tool === "string" ? request.tool : "unknown",
|
|
9752
|
-
...request?.kind ? { kind: request.kind } : {},
|
|
9753
|
-
...typeof request?.createdAt === "number" ? { createdAt: request.createdAt, waitingMs: Math.max(0, now - request.createdAt) } : {}
|
|
9754
|
-
})).sort((a, b) => (a.createdAt ?? Number.MAX_SAFE_INTEGER) - (b.createdAt ?? Number.MAX_SAFE_INTEGER));
|
|
9755
|
-
}
|
|
9756
|
-
const RPC_DEADLINE_GRACE_MS = 2e3;
|
|
9757
|
-
function withDeadline(promise, ms, message) {
|
|
9758
|
-
return new Promise((resolve, reject) => {
|
|
9759
|
-
const timer = setTimeout(() => reject(new Error(`${message} (timed out)`)), ms);
|
|
9760
|
-
promise.then(
|
|
9761
|
-
(value) => {
|
|
9762
|
-
clearTimeout(timer);
|
|
9763
|
-
resolve(value);
|
|
9764
|
-
},
|
|
9765
|
-
(error) => {
|
|
9766
|
-
clearTimeout(timer);
|
|
9767
|
-
reject(error);
|
|
9768
|
-
}
|
|
9769
|
-
);
|
|
9770
|
-
});
|
|
9771
|
-
}
|
|
9772
|
-
async function resolvePermissionRequest(sessionId, requestId, verdict, deps = {}) {
|
|
9773
|
-
const now = deps.now ?? Date.now;
|
|
9774
|
-
const persisted = (deps.readPersisted ?? readPersistedSessions)()[sessionId];
|
|
9775
|
-
if (!persisted) {
|
|
9776
|
-
throw new Error(
|
|
9777
|
-
`No local key for session ${sessionId} \u2014 approve/deny needs the session key from ~/.happy/sessions.json (the RPC payload is encrypted with it), so only sessions this machine's daemon spawned within 14 days can be answered from here.`
|
|
9778
|
-
);
|
|
9779
|
-
}
|
|
9780
|
-
const token = await (deps.bearerToken ?? bearerToken$1)();
|
|
9781
|
-
const fetchState = deps.fetchAgentState ?? fetchDecryptedAgentState;
|
|
9782
|
-
const before = pendingRequestsOf(await fetchState(sessionId, persisted, token), now());
|
|
9783
|
-
if (!before.some((request) => request.id === requestId)) {
|
|
9784
|
-
const hint = before.length > 0 ? `Pending request ids: ${before.map((request) => `${request.id} (${request.tool})`).join(", ")}` : "The session has no pending requests.";
|
|
9785
|
-
throw new Error(`Request ${requestId} is not pending on session ${sessionId}. ${hint}`);
|
|
9786
|
-
}
|
|
9787
|
-
const payload = buildPermissionRpcPayload(requestId, verdict);
|
|
9788
|
-
const key = decodeBase64(persisted.encryptionKey);
|
|
9789
|
-
const params = encodeBase64(encrypt(key, persisted.encryptionVariant, payload));
|
|
9790
|
-
const transport = await (deps.openTransport ?? openUserScopedSocket)(token);
|
|
9791
|
-
const rpcTimeoutMs = deps.rpcTimeoutMs ?? PERMISSION_RPC_TIMEOUT_MS;
|
|
9792
|
-
const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
9793
|
-
let ack;
|
|
9794
|
-
try {
|
|
9795
|
-
for (let attempt = 1; ; attempt++) {
|
|
9796
|
-
try {
|
|
9797
|
-
ack = await withDeadline(
|
|
9798
|
-
transport.rpcCall({ method: `${sessionId}:permission`, params }, rpcTimeoutMs),
|
|
9799
|
-
rpcTimeoutMs + RPC_DEADLINE_GRACE_MS,
|
|
9800
|
-
`permission RPC did not settle within ${rpcTimeoutMs}ms`
|
|
9801
|
-
);
|
|
9802
|
-
} catch (error) {
|
|
9803
|
-
ack = { ok: false, error: error instanceof Error ? error.message : "RPC call failed" };
|
|
9804
|
-
}
|
|
9805
|
-
if (!isRpcRateLimitAck(ack) || attempt > RPC_RATE_MAX_RETRIES) break;
|
|
9806
|
-
const waitMs = rpcRateRetryDelayMs(attempt, ack.retryAfterMs, (deps.random ?? Math.random)());
|
|
9807
|
-
deps.onRateLimited?.({ attempt, waitMs, serverError: ack.error ?? "RPC rate limit reached" });
|
|
9808
|
-
await sleep(waitMs);
|
|
9809
|
-
}
|
|
9810
|
-
} finally {
|
|
9811
|
-
transport.close();
|
|
9812
|
-
}
|
|
9813
|
-
const outcome = interpretPermissionAck(ack, (encrypted) => decrypt(key, persisted.encryptionVariant, decodeBase64(encrypted)));
|
|
9814
|
-
if (outcome.status !== "acknowledged") {
|
|
9815
|
-
return { sessionId, requestId, payload, outcome };
|
|
9816
|
-
}
|
|
9817
|
-
const deadline = now() + (deps.settleTimeoutMs ?? SETTLE_TIMEOUT_MS);
|
|
9818
|
-
let settled = false;
|
|
9819
|
-
for (; ; ) {
|
|
9820
|
-
const pending = pendingRequestsOf(await fetchState(sessionId, persisted, token), now());
|
|
9821
|
-
if (!pending.some((request) => request.id === requestId)) {
|
|
9822
|
-
settled = true;
|
|
9823
|
-
break;
|
|
9824
|
-
}
|
|
9825
|
-
if (now() >= deadline) break;
|
|
9826
|
-
await sleep(SETTLE_POLL_MS);
|
|
9827
|
-
}
|
|
9828
|
-
return { sessionId, requestId, payload, outcome, settled };
|
|
9829
|
-
}
|
|
9830
|
-
async function bearerToken$1() {
|
|
9831
|
-
const credentials = await readCredentialsForConfiguredRelay();
|
|
9832
|
-
if (!credentials) throw new Error("CLI is not authenticated (no ~/.happy/access.key)");
|
|
9833
|
-
return credentials.token;
|
|
9834
|
-
}
|
|
9835
|
-
async function fetchDecryptedAgentState(sessionId, persisted, token) {
|
|
9836
|
-
const response = await axios.get(
|
|
9837
|
-
`${configuration.serverUrl}/v1/sessions/${encodeURIComponent(sessionId)}`,
|
|
9838
|
-
{
|
|
9839
|
-
headers: {
|
|
9840
|
-
"Authorization": `Bearer ${token}`,
|
|
9841
|
-
"X-Happy-Client": `${PERMISSION_OPS_CLIENT}/${configuration.currentCliVersion}`
|
|
9842
|
-
},
|
|
9843
|
-
timeout: 15e3,
|
|
9844
|
-
// 404 is a real answer ("not yours / gone"), not a transport fault.
|
|
9845
|
-
validateStatus: (status) => status >= 200 && status < 300 || status === 404
|
|
9846
|
-
}
|
|
9847
|
-
);
|
|
9848
|
-
if (response.status === 404) {
|
|
9849
|
-
throw new Error(`Session ${sessionId} was not found on this account (it may have been deleted, or the local key belongs to a session on another relay).`);
|
|
9850
|
-
}
|
|
9851
|
-
const raw = response.data?.session?.agentState;
|
|
9852
|
-
if (typeof raw !== "string" || raw.length === 0) return null;
|
|
9853
|
-
return decrypt(decodeBase64(persisted.encryptionKey), persisted.encryptionVariant, decodeBase64(raw));
|
|
9854
|
-
}
|
|
9855
|
-
|
|
9856
|
-
const SESSION_OPS_CLIENT = "session-ops";
|
|
9857
|
-
const DEFAULT_RECENT_LIMIT = 15;
|
|
9858
|
-
const MAX_READ_LIMIT = 100;
|
|
9859
|
-
function mergeSessionSummaries(live, persisted, options = {}) {
|
|
9860
|
-
const recentLimit = Math.max(0, options.recentLimit ?? DEFAULT_RECENT_LIMIT);
|
|
9861
|
-
const seen = /* @__PURE__ */ new Set();
|
|
9862
|
-
const summaries = [];
|
|
9863
|
-
for (const child of live) {
|
|
9864
|
-
const id = typeof child.happySessionId === "string" ? child.happySessionId : void 0;
|
|
9865
|
-
if (!id || seen.has(id)) continue;
|
|
9866
|
-
seen.add(id);
|
|
9867
|
-
summaries.push(toSummary(id, persisted[id], livenessOf(child)));
|
|
9868
|
-
}
|
|
9869
|
-
const rest = Object.entries(persisted).filter(([id]) => !seen.has(id)).filter(([, entry]) => entry.metadata?.flavor !== "terminal-mirror").sort((a, b) => b[1].savedAt - a[1].savedAt).slice(0, recentLimit);
|
|
9870
|
-
for (const [id, entry] of rest) {
|
|
9871
|
-
summaries.push(toSummary(id, entry, { live: false }));
|
|
9872
|
-
}
|
|
9873
|
-
if (!options.tag) return summaries;
|
|
9874
|
-
const wanted = options.tag;
|
|
9875
|
-
return summaries.filter((summary) => summary.tags?.includes(wanted) === true);
|
|
9876
|
-
}
|
|
9877
|
-
function livenessOf(child) {
|
|
9878
|
-
return { live: true, pid: typeof child.pid === "number" ? child.pid : void 0 };
|
|
9879
|
-
}
|
|
9880
|
-
function sessionLiveness(live, sessionId) {
|
|
9881
|
-
const child = live.find((entry) => entry.happySessionId === sessionId);
|
|
9882
|
-
return child ? livenessOf(child) : { live: false };
|
|
9883
|
-
}
|
|
9884
|
-
function toSummary(id, persisted, extra) {
|
|
9885
|
-
const meta = persisted?.metadata;
|
|
9886
|
-
return {
|
|
9887
|
-
id,
|
|
9888
|
-
live: extra.live,
|
|
9889
|
-
...extra.pid !== void 0 ? { pid: extra.pid } : {},
|
|
9890
|
-
...meta?.summary?.text ? { title: meta.summary.text } : {},
|
|
9891
|
-
...meta?.path ? { cwd: meta.path } : {},
|
|
9892
|
-
...meta?.flavor ? { flavor: meta.flavor } : {},
|
|
9893
|
-
...meta?.tags?.length ? { tags: [...meta.tags] } : {},
|
|
9894
|
-
...meta?.variant ? { variant: meta.variant } : {},
|
|
9895
|
-
url: sessionWebUrl(id),
|
|
9896
|
-
...persisted?.savedAt !== void 0 ? { savedAt: persisted.savedAt } : {}
|
|
9897
|
-
};
|
|
9898
|
-
}
|
|
9899
|
-
async function listSessions(options = {}) {
|
|
9900
|
-
const live = await listDaemonSessions();
|
|
9901
|
-
return mergeSessionSummaries(live, readPersistedSessions(), options);
|
|
9902
|
-
}
|
|
9903
|
-
function summarizeAccountSession(row, persisted, liveIds, now) {
|
|
9904
|
-
const base = {
|
|
9905
|
-
id: row.id,
|
|
9906
|
-
live: liveIds.has(row.id),
|
|
9907
|
-
url: sessionWebUrl(row.id),
|
|
9908
|
-
decryptable: false,
|
|
9909
|
-
active: row.active === true,
|
|
9910
|
-
archived: typeof row.archivedAt === "number",
|
|
9911
|
-
...typeof row.activeAt === "number" ? { activeAt: row.activeAt } : {},
|
|
9912
|
-
...typeof row.updatedAt === "number" ? { updatedAt: row.updatedAt } : {},
|
|
9913
|
-
attention: false
|
|
9914
|
-
};
|
|
9915
|
-
if (!persisted) return base;
|
|
9916
|
-
const key = decodeBase64(persisted.encryptionKey);
|
|
9917
|
-
let metadata = null;
|
|
9918
|
-
let agentState = null;
|
|
9919
|
-
try {
|
|
9920
|
-
metadata = row.metadata ? decrypt(key, persisted.encryptionVariant, decodeBase64(row.metadata)) : null;
|
|
9921
|
-
agentState = row.agentState ? decrypt(key, persisted.encryptionVariant, decodeBase64(row.agentState)) : null;
|
|
9922
|
-
} catch {
|
|
9923
|
-
return base;
|
|
9924
|
-
}
|
|
9925
|
-
if (!metadata) return base;
|
|
9926
|
-
const meta = metadata;
|
|
9927
|
-
const pending = pendingRequestsOf(agentState, now);
|
|
9928
|
-
return {
|
|
9929
|
-
...base,
|
|
9930
|
-
decryptable: true,
|
|
9931
|
-
...meta.summary?.text ? { title: meta.summary.text } : {},
|
|
9932
|
-
...meta.path ? { cwd: meta.path } : {},
|
|
9933
|
-
...meta.flavor ? { flavor: meta.flavor } : {},
|
|
9934
|
-
...meta.tags?.length ? { tags: [...meta.tags] } : {},
|
|
9935
|
-
...meta.variant ? { variant: meta.variant } : {},
|
|
9936
|
-
...meta.machineId ? { machineId: meta.machineId } : {},
|
|
9937
|
-
...persisted.savedAt !== void 0 ? { savedAt: persisted.savedAt } : {},
|
|
9938
|
-
pending,
|
|
9939
|
-
attention: pending.length > 0
|
|
9940
|
-
};
|
|
9941
|
-
}
|
|
9942
|
-
function orderAccountSessions(summaries, options = {}) {
|
|
9943
|
-
const oldestWait = (summary) => Math.max(0, ...(summary.pending ?? []).map((request) => request.waitingMs ?? 0));
|
|
9944
|
-
const rank = (summary) => summary.attention ? 0 : summary.live ? 1 : 2;
|
|
9945
|
-
let rows = summaries.filter((summary) => summary.flavor !== "terminal-mirror").slice().sort((a, b) => {
|
|
9946
|
-
const byRank = rank(a) - rank(b);
|
|
9947
|
-
if (byRank !== 0) return byRank;
|
|
9948
|
-
if (a.attention && b.attention) return oldestWait(b) - oldestWait(a);
|
|
9949
|
-
return (b.updatedAt ?? 0) - (a.updatedAt ?? 0);
|
|
9950
|
-
});
|
|
9951
|
-
if (options.tag) {
|
|
9952
|
-
const wanted = options.tag;
|
|
9953
|
-
rows = rows.filter((summary) => summary.tags?.includes(wanted) === true);
|
|
9954
|
-
}
|
|
9955
|
-
const recentLimit = Math.max(0, options.recentLimit ?? DEFAULT_RECENT_LIMIT);
|
|
9956
|
-
let idleKept = 0;
|
|
9957
|
-
return rows.filter((summary) => rank(summary) < 2 || idleKept++ < recentLimit);
|
|
9958
|
-
}
|
|
9959
|
-
async function listAccountSessions(options = {}) {
|
|
9960
|
-
const token = await bearerToken();
|
|
9961
|
-
const response = await axios.get(`${configuration.serverUrl}/v1/sessions`, {
|
|
9962
|
-
headers: {
|
|
9963
|
-
"Authorization": `Bearer ${token}`,
|
|
9964
|
-
"X-Happy-Client": `${SESSION_OPS_CLIENT}/${configuration.currentCliVersion}`
|
|
9965
|
-
},
|
|
9966
|
-
timeout: 15e3
|
|
9967
|
-
});
|
|
9968
|
-
const rows = Array.isArray(response.data?.sessions) ? response.data.sessions : [];
|
|
9969
|
-
const persisted = readPersistedSessions();
|
|
9970
|
-
const live = await listDaemonSessions();
|
|
9971
|
-
const liveIds = new Set(live.map((child) => child.happySessionId).filter((id) => typeof id === "string"));
|
|
9972
|
-
const now = Date.now();
|
|
9973
|
-
const summaries = rows.filter((row) => typeof row?.id === "string").map((row) => summarizeAccountSession(row, persisted[row.id], liveIds, now)).filter((summary) => options.includeArchived || !summary.archived);
|
|
9974
|
-
return orderAccountSessions(summaries, options);
|
|
9975
|
-
}
|
|
9976
|
-
async function bearerToken() {
|
|
9977
|
-
const credentials = await readCredentialsForConfiguredRelay();
|
|
9978
|
-
if (!credentials) throw new Error("CLI is not authenticated (no ~/.happy/access.key)");
|
|
9979
|
-
return credentials.token;
|
|
9980
|
-
}
|
|
9981
|
-
function requirePersisted(sessionId) {
|
|
9982
|
-
const persisted = readPersistedSessions()[sessionId];
|
|
9983
|
-
if (!persisted) {
|
|
9984
|
-
throw new Error(
|
|
9985
|
-
`No local key for session ${sessionId} \u2014 it was not spawned by this machine's daemon (or is older than 14 days).`
|
|
9986
|
-
);
|
|
9987
|
-
}
|
|
9988
|
-
return persisted;
|
|
9989
|
-
}
|
|
9990
|
-
async function readSessionLog(sessionId, persisted, limit) {
|
|
9991
|
-
const token = await bearerToken();
|
|
9992
|
-
const response = await axios.get(
|
|
9993
|
-
`${configuration.serverUrl}/v3/sessions/${encodeURIComponent(sessionId)}/messages`,
|
|
9994
|
-
{
|
|
9995
|
-
params: { before_seq: 2147483647, limit },
|
|
9996
|
-
headers: {
|
|
9997
|
-
"Authorization": `Bearer ${token}`,
|
|
9998
|
-
"X-Happy-Client": `${SESSION_OPS_CLIENT}/${configuration.currentCliVersion}`
|
|
9999
|
-
},
|
|
10000
|
-
timeout: 15e3
|
|
10001
|
-
}
|
|
10002
|
-
);
|
|
10003
|
-
const messages = Array.isArray(response.data?.messages) ? response.data.messages : [];
|
|
10004
|
-
messages.reverse();
|
|
10005
|
-
const key = decodeBase64(persisted.encryptionKey);
|
|
10006
|
-
return messages.map((message) => {
|
|
10007
|
-
if (message.content?.t !== "encrypted") return { seq: message.seq, body: null };
|
|
10008
|
-
try {
|
|
10009
|
-
return { seq: message.seq, body: decrypt(key, persisted.encryptionVariant, decodeBase64(message.content.c)) };
|
|
10010
|
-
} catch {
|
|
10011
|
-
return { seq: message.seq, body: null };
|
|
10012
|
-
}
|
|
10013
|
-
});
|
|
10014
|
-
}
|
|
10015
|
-
const TURN_WINDOW = 500;
|
|
10016
|
-
async function readSessionTranscript(sessionId, limit, options = {}) {
|
|
10017
|
-
const persisted = requirePersisted(sessionId);
|
|
10018
|
-
const bounded = Math.max(1, Math.min(MAX_READ_LIMIT, Math.floor(limit)));
|
|
10019
|
-
const entries = await readSessionLog(sessionId, persisted, Math.max(bounded, TURN_WINDOW));
|
|
10020
|
-
const shown = entries.slice(-bounded);
|
|
10021
|
-
const live = await listDaemonSessions();
|
|
10022
|
-
return {
|
|
10023
|
-
summary: toSummary(sessionId, persisted, sessionLiveness(live, sessionId)),
|
|
10024
|
-
messageCount: shown.length,
|
|
10025
|
-
transcript: formatTranscript(shown.map((entry) => entry.body), { full: options.full }),
|
|
10026
|
-
turn: analyzeLatestTurn(entries)
|
|
10027
|
-
};
|
|
10028
|
-
}
|
|
10029
|
-
async function readSessionMetadata(sessionId, persisted) {
|
|
10030
|
-
const token = await bearerToken();
|
|
10031
|
-
const response = await axios.get(`${configuration.serverUrl}/v1/sessions/${encodeURIComponent(sessionId)}`, {
|
|
10032
|
-
headers: {
|
|
10033
|
-
"Authorization": `Bearer ${token}`,
|
|
10034
|
-
"X-Happy-Client": `${SESSION_OPS_CLIENT}/${configuration.currentCliVersion}`
|
|
10035
|
-
},
|
|
10036
|
-
timeout: 15e3
|
|
10037
|
-
});
|
|
10038
|
-
const ciphertext = response.data?.session?.metadata;
|
|
10039
|
-
if (typeof ciphertext !== "string" || ciphertext.length === 0) return null;
|
|
10040
|
-
return decrypt(decodeBase64(persisted.encryptionKey), persisted.encryptionVariant, decodeBase64(ciphertext));
|
|
10041
|
-
}
|
|
10042
|
-
async function readSessionState(sessionId, persisted) {
|
|
10043
|
-
const token = await bearerToken();
|
|
10044
|
-
const response = await axios.get(`${configuration.serverUrl}/v1/sessions/${encodeURIComponent(sessionId)}`, {
|
|
10045
|
-
headers: {
|
|
10046
|
-
"Authorization": `Bearer ${token}`,
|
|
10047
|
-
"X-Happy-Client": `${SESSION_OPS_CLIENT}/${configuration.currentCliVersion}`
|
|
10048
|
-
},
|
|
10049
|
-
timeout: 15e3,
|
|
10050
|
-
validateStatus: (status) => status >= 200 && status < 300 || status === 404
|
|
10051
|
-
});
|
|
10052
|
-
if (response.status === 404) return { found: false, active: false };
|
|
10053
|
-
const row = response.data?.session;
|
|
10054
|
-
if (!row || typeof row.id !== "string") throw new Error(`Server returned no session row for ${sessionId}`);
|
|
10055
|
-
const state = {
|
|
10056
|
-
found: true,
|
|
10057
|
-
active: row.active === true,
|
|
10058
|
-
...typeof row.activeAt === "number" ? { activeAt: row.activeAt } : {},
|
|
10059
|
-
archivedAt: typeof row.archivedAt === "number" ? row.archivedAt : null,
|
|
10060
|
-
...typeof row.updatedAt === "number" ? { updatedAt: row.updatedAt } : {}
|
|
10061
|
-
};
|
|
10062
|
-
if (persisted && typeof row.metadata === "string" && row.metadata.length > 0) {
|
|
10063
|
-
try {
|
|
10064
|
-
const metadata = decrypt(decodeBase64(persisted.encryptionKey), persisted.encryptionVariant, decodeBase64(row.metadata));
|
|
10065
|
-
if (metadata?.machineId) state.machineId = metadata.machineId;
|
|
10066
|
-
} catch {
|
|
10067
|
-
}
|
|
10068
|
-
}
|
|
10069
|
-
return state;
|
|
10070
|
-
}
|
|
10071
|
-
class TurnWaitTimeoutError extends Error {
|
|
10072
|
-
constructor(sessionId, timeoutMs, turn) {
|
|
10073
|
-
super(`Session ${sessionId}: the latest turn did not end within ${Math.round(timeoutMs / 1e3)}s`);
|
|
10074
|
-
this.sessionId = sessionId;
|
|
10075
|
-
this.timeoutMs = timeoutMs;
|
|
10076
|
-
this.turn = turn;
|
|
10077
|
-
}
|
|
10078
|
-
}
|
|
10079
|
-
async function waitForTurnEnd(sessionId, options) {
|
|
10080
|
-
const persisted = requirePersisted(sessionId);
|
|
10081
|
-
const pollMs = options.pollMs ?? 3e3;
|
|
10082
|
-
const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
10083
|
-
const deadline = Date.now() + options.timeoutMs;
|
|
10084
|
-
while (true) {
|
|
10085
|
-
const turn = analyzeLatestTurn(await readSessionLog(sessionId, persisted, TURN_WINDOW));
|
|
10086
|
-
if (turn.ended) return turn;
|
|
10087
|
-
if (Date.now() + pollMs > deadline) throw new TurnWaitTimeoutError(sessionId, options.timeoutMs, turn);
|
|
10088
|
-
await sleep(pollMs);
|
|
10089
|
-
}
|
|
10090
|
-
}
|
|
10091
|
-
async function stopSession(sessionId) {
|
|
10092
|
-
return await stopDaemonSession(sessionId);
|
|
10093
|
-
}
|
|
10094
|
-
async function archiveSession(sessionId) {
|
|
10095
|
-
const token = await bearerToken();
|
|
10096
|
-
await axios.post(
|
|
10097
|
-
`${configuration.serverUrl}/v1/sessions/${encodeURIComponent(sessionId)}/archive`,
|
|
10098
|
-
{},
|
|
10099
|
-
{
|
|
10100
|
-
headers: {
|
|
10101
|
-
"Authorization": `Bearer ${token}`,
|
|
10102
|
-
"X-Happy-Client": `${SESSION_OPS_CLIENT}/${configuration.currentCliVersion}`
|
|
10103
|
-
},
|
|
10104
|
-
timeout: 1e4
|
|
10105
|
-
}
|
|
10106
|
-
);
|
|
10107
|
-
}
|
|
10108
|
-
async function unarchiveSession(sessionId) {
|
|
10109
|
-
const token = await bearerToken();
|
|
10110
|
-
await axios.post(
|
|
10111
|
-
`${configuration.serverUrl}/v1/sessions/${encodeURIComponent(sessionId)}/unarchive`,
|
|
10112
|
-
{},
|
|
10113
|
-
{
|
|
10114
|
-
headers: {
|
|
10115
|
-
"Authorization": `Bearer ${token}`,
|
|
10116
|
-
"X-Happy-Client": `${SESSION_OPS_CLIENT}/${configuration.currentCliVersion}`
|
|
10117
|
-
},
|
|
10118
|
-
timeout: 1e4
|
|
10119
|
-
}
|
|
10120
|
-
);
|
|
10121
|
-
}
|
|
10122
|
-
|
|
10123
9794
|
function receiptPath(home, operationId) {
|
|
10124
9795
|
if (!/^[a-zA-Z0-9_-]{1,128}$/.test(operationId)) throw new Error("Invalid team operation id");
|
|
10125
9796
|
return join(home, "teams", "effects", `${operationId}.json`);
|
|
@@ -10709,7 +10380,7 @@ function automationRunAnchor(runId) {
|
|
|
10709
10380
|
async function defaultReadTurn(sessionId, runId) {
|
|
10710
10381
|
const persisted = readPersistedSessions()[sessionId];
|
|
10711
10382
|
if (!persisted) throw new Error(`No local key for session ${sessionId}`);
|
|
10712
|
-
return analyzeLatestTurn(await readSessionLog(sessionId, persisted, TURN_WINDOW), { anchor: automationRunAnchor(runId) });
|
|
10383
|
+
return analyzeLatestTurn(await readSessionLog$1(sessionId, persisted, TURN_WINDOW), { anchor: automationRunAnchor(runId) });
|
|
10713
10384
|
}
|
|
10714
10385
|
async function defaultSend(sessionId, text, meta) {
|
|
10715
10386
|
const persisted = await waitForSessionKey(sessionId, 15e3);
|
|
@@ -11053,6 +10724,16 @@ ${action.prompt}`, { localId: `automation-${run.id}`, ...meta });
|
|
|
11053
10724
|
function daemonExitCode(source) {
|
|
11054
10725
|
return source === "exception" ? 1 : 0;
|
|
11055
10726
|
}
|
|
10727
|
+
const HANDOVER_EXIT_CODE = 75;
|
|
10728
|
+
|
|
10729
|
+
function detectDaemonSupervisor(env, platform = process.platform) {
|
|
10730
|
+
const explicit = env.HAPPY_DAEMON_SUPERVISOR?.trim().toLowerCase();
|
|
10731
|
+
if (explicit === "systemd") return "systemd";
|
|
10732
|
+
if (explicit === "none") return "none";
|
|
10733
|
+
if (platform === "linux" && typeof env.INVOCATION_ID === "string" && env.INVOCATION_ID.trim() !== "") return "systemd";
|
|
10734
|
+
return "none";
|
|
10735
|
+
}
|
|
10736
|
+
const SYSTEMD_RESTART_HINT = "systemctl --user restart very-happy-daemon";
|
|
11056
10737
|
|
|
11057
10738
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
11058
10739
|
function sanitizeImportTitle(value) {
|
|
@@ -11208,6 +10889,7 @@ async function startDaemon() {
|
|
|
11208
10889
|
logger.debug("[DAEMON RUN] Auth and machine setup complete");
|
|
11209
10890
|
const pidToTrackedSession = /* @__PURE__ */ new Map();
|
|
11210
10891
|
const turnActivity = new TurnActivityTracker();
|
|
10892
|
+
const peerCoordinator = createPeerCoordinator({ getChildren: () => Array.from(pidToTrackedSession.values()) });
|
|
11211
10893
|
const sessionIdToFinishedSession = /* @__PURE__ */ new Map();
|
|
11212
10894
|
let teamWorker;
|
|
11213
10895
|
let automationRunner;
|
|
@@ -12067,6 +11749,7 @@ async function startDaemon() {
|
|
|
12067
11749
|
if (session?.happySessionId && (session.spawnedBy === "teams" || teamWorker?.hasSession(session.happySessionId))) teamWorker?.report(session.happySessionId, "exited");
|
|
12068
11750
|
if (session?.happySessionId && automationRunner?.hasSession(session.happySessionId)) automationRunner.report(session.happySessionId, "exited");
|
|
12069
11751
|
if (session?.happySessionId) turnActivity.forget(session.happySessionId);
|
|
11752
|
+
if (session?.happySessionId) peerCoordinator.forget(session.happySessionId);
|
|
12070
11753
|
if (session?.happySessionId && session.encryption) {
|
|
12071
11754
|
sessionIdToFinishedSession.set(session.happySessionId, session);
|
|
12072
11755
|
logger.debug(`[DAEMON RUN] Process PID ${pid} exited, preserved session ${session.happySessionId} for resume`);
|
|
@@ -12152,6 +11835,13 @@ async function startDaemon() {
|
|
|
12152
11835
|
onSessionStateEvent,
|
|
12153
11836
|
onClaudeAuthFailed: (sessionId) => claudeAuthServiceRef?.signalAuthFailed(sessionId),
|
|
12154
11837
|
onSessionTurnEvent: (sessionId, event) => turnActivity.apply(sessionId, event),
|
|
11838
|
+
onSessionEdit: (edit) => {
|
|
11839
|
+
peerCoordinator.onEdit(edit);
|
|
11840
|
+
},
|
|
11841
|
+
listPeers: () => peerCoordinator.list(),
|
|
11842
|
+
// B-505: `daemon list` shows which sessions have a turn in flight, so an
|
|
11843
|
+
// operator can check the WHOLE machine before a supervised restart.
|
|
11844
|
+
isTurnActive: (sessionId) => turnActivity.activeSessions().includes(sessionId),
|
|
12155
11845
|
pushClipboard: (text, terminalId) => {
|
|
12156
11846
|
if (!apiMachineRef) {
|
|
12157
11847
|
return { delivered: false, truncated: false, totalBytes: 0, error: "daemon is still starting up" };
|
|
@@ -12171,6 +11861,8 @@ async function startDaemon() {
|
|
|
12171
11861
|
}
|
|
12172
11862
|
});
|
|
12173
11863
|
const daemonClaudeCredentials = resolveClaudeCredentialReadiness();
|
|
11864
|
+
const daemonSupervisor = detectDaemonSupervisor(process.env);
|
|
11865
|
+
logger.debug(`[DAEMON RUN] Supervisor: ${daemonSupervisor}`);
|
|
12174
11866
|
let fileState = {
|
|
12175
11867
|
pid: process.pid,
|
|
12176
11868
|
httpPort: controlPort,
|
|
@@ -12180,7 +11872,8 @@ async function startDaemon() {
|
|
|
12180
11872
|
serverUrl: configuration.serverUrl,
|
|
12181
11873
|
webappUrl: configuration.webappUrl,
|
|
12182
11874
|
daemonLogPath: logger.logFilePath,
|
|
12183
|
-
claudeCredentialSource: daemonClaudeCredentials.source
|
|
11875
|
+
claudeCredentialSource: daemonClaudeCredentials.source,
|
|
11876
|
+
supervisor: daemonSupervisor
|
|
12184
11877
|
};
|
|
12185
11878
|
writeDaemonState(fileState);
|
|
12186
11879
|
logger.debug("[DAEMON RUN] Daemon state written");
|
|
@@ -12217,9 +11910,12 @@ async function startDaemon() {
|
|
|
12217
11910
|
const mirrorManager = createMirrorManager({
|
|
12218
11911
|
api,
|
|
12219
11912
|
machineId,
|
|
12220
|
-
onBindingsChanged: () => apiMachine.requestTerminalListRefresh()
|
|
11913
|
+
onBindingsChanged: () => apiMachine.requestTerminalListRefresh(),
|
|
11914
|
+
onEdit: (edit) => peerCoordinator.onEdit(edit),
|
|
11915
|
+
onSessionEnded: (sessionId) => peerCoordinator.forget(sessionId)
|
|
12221
11916
|
});
|
|
12222
11917
|
mirrorManagerRef = mirrorManager;
|
|
11918
|
+
peerCoordinator.setMirrorSource(() => mirrorManager.listActiveMirrors());
|
|
12223
11919
|
apiMachine.setMirrorIntegration({
|
|
12224
11920
|
resolveMirrorSessionId: (terminalId) => mirrorManager.resolveMirrorSessionId(terminalId),
|
|
12225
11921
|
onTerminalClosed: (terminalId) => mirrorManager.onTerminalClosed(terminalId),
|
|
@@ -12350,6 +12046,10 @@ async function startDaemon() {
|
|
|
12350
12046
|
await cleanupDaemonState();
|
|
12351
12047
|
await releaseDaemonLock(daemonLockHandle);
|
|
12352
12048
|
await stopCaffeinate();
|
|
12049
|
+
if (daemonSupervisor === "systemd") {
|
|
12050
|
+
logger.debug(`[DAEMON RUN] systemd-supervised: exiting ${HANDOVER_EXIT_CODE} for the unit to restart on the new bundle`);
|
|
12051
|
+
process.exit(HANDOVER_EXIT_CODE);
|
|
12052
|
+
}
|
|
12353
12053
|
try {
|
|
12354
12054
|
spawnHappyCLI(["daemon", "start"], {
|
|
12355
12055
|
detached: true,
|
|
@@ -12551,127 +12251,6 @@ This session is now connected as the team lead. Call team_inspect to read the au
|
|
|
12551
12251
|
};
|
|
12552
12252
|
}
|
|
12553
12253
|
|
|
12554
|
-
const ACTIVE_FRESH_WINDOW_MS = 15 * 60 * 1e3;
|
|
12555
|
-
const DEFAULT_RESUME_WAIT_MS = 3e4;
|
|
12556
|
-
const DEFAULT_RESUME_POLL_MS = 1e3;
|
|
12557
|
-
function classifySessionState(input) {
|
|
12558
|
-
const { liveHere, server, now } = input;
|
|
12559
|
-
if (!server.found) return "not_found";
|
|
12560
|
-
if (typeof server.archivedAt === "number") return "archived";
|
|
12561
|
-
if (liveHere) return "live";
|
|
12562
|
-
if (server.active && typeof server.activeAt === "number" && now - server.activeAt <= ACTIVE_FRESH_WINDOW_MS) return "live";
|
|
12563
|
-
return "offline";
|
|
12564
|
-
}
|
|
12565
|
-
function explainNotLive(sessionId, status, resumeHint) {
|
|
12566
|
-
switch (status) {
|
|
12567
|
-
case "archived":
|
|
12568
|
-
return `Session ${sessionId} is archived \u2014 no live wrapper will receive this message. ${resumeHint}`;
|
|
12569
|
-
case "offline":
|
|
12570
|
-
return `Session ${sessionId} is offline (its wrapper exited or timed out) \u2014 no live wrapper will receive this message. ${resumeHint}`;
|
|
12571
|
-
case "not_found":
|
|
12572
|
-
return `Session ${sessionId} was not found on this account \u2014 it cannot be messaged or resumed. Spawn a new session instead.`;
|
|
12573
|
-
}
|
|
12574
|
-
}
|
|
12575
|
-
const CLI_RESUME_HINT = "Use --resume to bring it back on this machine, or spawn a new session.";
|
|
12576
|
-
const defaultDeps = {
|
|
12577
|
-
listLive: () => listDaemonSessions(),
|
|
12578
|
-
readState: (sessionId, persisted) => readSessionState(sessionId, persisted),
|
|
12579
|
-
send: (sessionId, persisted, text, client, options) => sendUserMessage(sessionId, persisted, text, client, options),
|
|
12580
|
-
unarchive: unarchiveSession,
|
|
12581
|
-
archive: archiveSession,
|
|
12582
|
-
resume: resumeDaemonSession,
|
|
12583
|
-
sleep: (ms) => delay(ms).then(() => void 0),
|
|
12584
|
-
now: () => Date.now()
|
|
12585
|
-
};
|
|
12586
|
-
async function deliverToSession(sessionId, persisted, text, client, options = {}, deps = {}) {
|
|
12587
|
-
const d = { ...defaultDeps, ...deps };
|
|
12588
|
-
const resumeHint = options.resumeHint ?? CLI_RESUME_HINT;
|
|
12589
|
-
const classify = async () => {
|
|
12590
|
-
const [live, server] = await Promise.all([d.listLive(), d.readState(sessionId, persisted)]);
|
|
12591
|
-
return classifySessionState({ liveHere: sessionLiveness(live, sessionId).live, server, now: d.now() });
|
|
12592
|
-
};
|
|
12593
|
-
let status = await classify();
|
|
12594
|
-
let resumed = false;
|
|
12595
|
-
let resumeOutcome;
|
|
12596
|
-
if (status !== "live") {
|
|
12597
|
-
if (!options.resume || status === "not_found") {
|
|
12598
|
-
return { sessionId, status, delivered: false, resumed: false, stored: false, error: explainNotLive(sessionId, status, resumeHint) };
|
|
12599
|
-
}
|
|
12600
|
-
const wasArchived = status === "archived";
|
|
12601
|
-
if (wasArchived) {
|
|
12602
|
-
try {
|
|
12603
|
-
await d.unarchive(sessionId);
|
|
12604
|
-
} catch (error) {
|
|
12605
|
-
const message = `unarchive failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
12606
|
-
return { sessionId, status, delivered: false, resumed: false, stored: false, error: message, resume: { ok: false, error: message } };
|
|
12607
|
-
}
|
|
12608
|
-
}
|
|
12609
|
-
const outcome = await d.resume(sessionId, {
|
|
12610
|
-
...typeof options.model === "string" ? { model: options.model } : {},
|
|
12611
|
-
...options.permissionMode !== void 0 ? { permissionMode: options.permissionMode } : {}
|
|
12612
|
-
});
|
|
12613
|
-
if (!outcome.success) {
|
|
12614
|
-
let message = outcome.error ?? "resume failed";
|
|
12615
|
-
if (wasArchived) {
|
|
12616
|
-
try {
|
|
12617
|
-
await d.archive(sessionId);
|
|
12618
|
-
} catch (error) {
|
|
12619
|
-
message += `; failed to restore archive state: ${error instanceof Error ? error.message : String(error)}`;
|
|
12620
|
-
}
|
|
12621
|
-
}
|
|
12622
|
-
return { sessionId, status, delivered: false, resumed: false, stored: false, error: message, resume: { ok: false, error: message } };
|
|
12623
|
-
}
|
|
12624
|
-
resumed = true;
|
|
12625
|
-
resumeOutcome = { ok: true };
|
|
12626
|
-
status = await waitUntilLive(classify, d, options);
|
|
12627
|
-
if (status !== "live") {
|
|
12628
|
-
const waited = Math.round((options.waitMs ?? DEFAULT_RESUME_WAIT_MS) / 1e3);
|
|
12629
|
-
return {
|
|
12630
|
-
sessionId,
|
|
12631
|
-
status,
|
|
12632
|
-
delivered: false,
|
|
12633
|
-
resumed,
|
|
12634
|
-
stored: false,
|
|
12635
|
-
resume: resumeOutcome,
|
|
12636
|
-
error: `Session ${sessionId} was resumed but no live wrapper showed up within ${waited}s (state: ${status}); message not sent`
|
|
12637
|
-
};
|
|
12638
|
-
}
|
|
12639
|
-
}
|
|
12640
|
-
await d.send(sessionId, persisted, text, client, {
|
|
12641
|
-
...options.model !== void 0 ? { model: options.model } : {},
|
|
12642
|
-
...options.sentFrom !== void 0 ? { sentFrom: options.sentFrom } : {}
|
|
12643
|
-
});
|
|
12644
|
-
let after = "live";
|
|
12645
|
-
try {
|
|
12646
|
-
after = await classify();
|
|
12647
|
-
} catch {
|
|
12648
|
-
after = "live";
|
|
12649
|
-
}
|
|
12650
|
-
if (after !== "live") {
|
|
12651
|
-
return {
|
|
12652
|
-
sessionId,
|
|
12653
|
-
status: after,
|
|
12654
|
-
delivered: false,
|
|
12655
|
-
resumed,
|
|
12656
|
-
stored: true,
|
|
12657
|
-
...resumeOutcome ? { resume: resumeOutcome } : {},
|
|
12658
|
-
error: `Session ${sessionId} went ${after} while sending: the message is stored server-side but no wrapper is attached to read it. ${after === "not_found" ? "Spawn a new session." : resumeHint}`
|
|
12659
|
-
};
|
|
12660
|
-
}
|
|
12661
|
-
return { sessionId, status: "live", delivered: true, resumed, stored: true, ...resumeOutcome ? { resume: resumeOutcome } : {} };
|
|
12662
|
-
}
|
|
12663
|
-
async function waitUntilLive(classify, d, options) {
|
|
12664
|
-
const waitMs = options.waitMs ?? DEFAULT_RESUME_WAIT_MS;
|
|
12665
|
-
const pollMs = options.pollMs ?? DEFAULT_RESUME_POLL_MS;
|
|
12666
|
-
const deadline = d.now() + waitMs;
|
|
12667
|
-
let status = await classify();
|
|
12668
|
-
while (status !== "live" && d.now() < deadline) {
|
|
12669
|
-
await d.sleep(pollMs);
|
|
12670
|
-
status = await classify();
|
|
12671
|
-
}
|
|
12672
|
-
return status;
|
|
12673
|
-
}
|
|
12674
|
-
|
|
12675
12254
|
const PERSONAL_MEMORY_SOFT_LIMIT_CHARS = 2e3;
|
|
12676
12255
|
function headingTitle(line) {
|
|
12677
12256
|
const m = /^##(?!#)\s*(.*?)\s*$/.exec(line);
|
|
@@ -13038,13 +12617,14 @@ function normalizeProgress(value) {
|
|
|
13038
12617
|
return flat.length > PROGRESS_MAX_CHARS$1 ? flat.slice(0, PROGRESS_MAX_CHARS$1) : flat;
|
|
13039
12618
|
}
|
|
13040
12619
|
|
|
13041
|
-
function createMcpServer(handlers, options, sessionId) {
|
|
12620
|
+
function createMcpServer(handlers, options, sessionId, client) {
|
|
13042
12621
|
const mcp = new McpServer({
|
|
13043
12622
|
name: "Very Happy Tools",
|
|
13044
12623
|
version: "1.0.0"
|
|
13045
12624
|
});
|
|
13046
12625
|
registerTeamsTools(mcp, sessionId);
|
|
13047
12626
|
registerAutomationTools(mcp);
|
|
12627
|
+
if (client) registerSessionPeerTools(mcp, createSessionPeerToolExecutor(client));
|
|
13048
12628
|
mcp.registerTool("change_title", {
|
|
13049
12629
|
description: "Change the title of the current chat session",
|
|
13050
12630
|
title: "Change Chat Title",
|
|
@@ -13247,7 +12827,7 @@ async function startHappyServer(client, options) {
|
|
|
13247
12827
|
}
|
|
13248
12828
|
return;
|
|
13249
12829
|
}
|
|
13250
|
-
const mcp = createMcpServer(handlers, options, client.sessionId);
|
|
12830
|
+
const mcp = createMcpServer(handlers, options, client.sessionId, client);
|
|
13251
12831
|
try {
|
|
13252
12832
|
const transport = new StreamableHTTPServerTransport({
|
|
13253
12833
|
sessionIdGenerator: void 0
|
|
@@ -13288,6 +12868,7 @@ async function startHappyServer(client, options) {
|
|
|
13288
12868
|
REPORT_PROGRESS_TOOL_NAME,
|
|
13289
12869
|
...TEAM_TOOL_NAMES,
|
|
13290
12870
|
...AUTOMATION_TOOL_NAMES,
|
|
12871
|
+
...SESSION_PEER_TOOL_NAMES,
|
|
13291
12872
|
...options?.assistant && !process.env.VH_TEAM_SCOPE_FILE ? ASSISTANT_TOOL_NAMES : []
|
|
13292
12873
|
],
|
|
13293
12874
|
stop: () => {
|
|
@@ -13448,6 +13029,40 @@ function registerKillSessionHandler(rpcHandlerManager, killThisHappy, archiveEve
|
|
|
13448
13029
|
};
|
|
13449
13030
|
}
|
|
13450
13031
|
|
|
13032
|
+
async function settleSessionOnExit(sessionId, intent, deps) {
|
|
13033
|
+
if (intent === "archive") {
|
|
13034
|
+
deps.updateMetadata((currentMetadata) => ({
|
|
13035
|
+
...currentMetadata,
|
|
13036
|
+
lifecycleState: "archived",
|
|
13037
|
+
lifecycleStateSince: Date.now(),
|
|
13038
|
+
archivedBy: "cli",
|
|
13039
|
+
archiveReason: "User terminated"
|
|
13040
|
+
}));
|
|
13041
|
+
}
|
|
13042
|
+
deps.sendSessionDeath();
|
|
13043
|
+
try {
|
|
13044
|
+
if (intent === "archive") await deps.archiveSession(sessionId);
|
|
13045
|
+
else await deps.deactivateSession(sessionId);
|
|
13046
|
+
} catch (error) {
|
|
13047
|
+
deps.log?.(`[START] ${intent === "archive" ? "archiveSession" : "deactivateSession"} during cleanup failed:`, error);
|
|
13048
|
+
}
|
|
13049
|
+
}
|
|
13050
|
+
class SessionExitGate {
|
|
13051
|
+
decided = null;
|
|
13052
|
+
/** Returns the intent to act on, or null when cleanup already started. */
|
|
13053
|
+
claim(intent) {
|
|
13054
|
+
if (this.decided !== null) return null;
|
|
13055
|
+
this.decided = intent;
|
|
13056
|
+
return intent;
|
|
13057
|
+
}
|
|
13058
|
+
get current() {
|
|
13059
|
+
return this.decided;
|
|
13060
|
+
}
|
|
13061
|
+
}
|
|
13062
|
+
function exitIntentFromArchiveFlag(archive) {
|
|
13063
|
+
return archive ?? true ? "archive" : "offline";
|
|
13064
|
+
}
|
|
13065
|
+
|
|
13451
13066
|
function isUnsupportedSideQuestionError(error) {
|
|
13452
13067
|
const message = error instanceof Error ? error.message : String(error);
|
|
13453
13068
|
return /unknown|unsupported|not supported|unrecognized|invalid.*subtype/i.test(message);
|
|
@@ -13624,7 +13239,7 @@ function registerSideQuestionHandler(rpc, deps) {
|
|
|
13624
13239
|
const retainMs = deps.retainMs ?? SIDE_QUESTION_RETAIN_MS;
|
|
13625
13240
|
const maxRunMs = deps.maxRunMs ?? SIDE_QUESTION_MAX_RUN_MS;
|
|
13626
13241
|
const fork = deps.run ?? (async (input) => {
|
|
13627
|
-
const { query } = await import('./index-
|
|
13242
|
+
const { query } = await import('./index-PYO8Mdz4.mjs');
|
|
13628
13243
|
return runSideQuestion(query, input);
|
|
13629
13244
|
});
|
|
13630
13245
|
const run = async (input, live) => {
|
|
@@ -14411,6 +14026,17 @@ async function runClaude(credentials, options = {}) {
|
|
|
14411
14026
|
});
|
|
14412
14027
|
session.on("claude-session-message", (body) => boardAnalyzer.noteClaudeMessage(body));
|
|
14413
14028
|
session.on("turn-ended", () => boardAnalyzer.onTurnEnd());
|
|
14029
|
+
const editThrottle = new EditReportThrottle();
|
|
14030
|
+
session.on("claude-session-message", (body) => {
|
|
14031
|
+
try {
|
|
14032
|
+
for (const edit of editThrottle.take(extractClaudeEditPaths(body))) {
|
|
14033
|
+
reportSessionEditToDaemon({ sessionId: response.id, path: edit.path, tool: edit.tool, cwd: workingDirectory, ...edit.at !== void 0 ? { at: edit.at } : {} });
|
|
14034
|
+
}
|
|
14035
|
+
} catch (error) {
|
|
14036
|
+
logger.debug("[START] edit report failed:", error);
|
|
14037
|
+
}
|
|
14038
|
+
});
|
|
14039
|
+
options = { ...options, claudeEnvVars: { ...options.claudeEnvVars, VH_PEER_SESSION_ID: response.id } };
|
|
14414
14040
|
const initialScannerSessionId = forkClaudeSessionId ?? (metadata.claudeSessionId ?? null);
|
|
14415
14041
|
const remoteScanner = await createSessionScanner({
|
|
14416
14042
|
sessionId: initialScannerSessionId,
|
|
@@ -14750,26 +14376,24 @@ async function runClaude(credentials, options = {}) {
|
|
|
14750
14376
|
hasMetadata: Boolean(message.meta)
|
|
14751
14377
|
});
|
|
14752
14378
|
});
|
|
14379
|
+
const exitGate = new SessionExitGate();
|
|
14753
14380
|
const cleanup = async (opts = { archive: true }) => {
|
|
14754
|
-
|
|
14381
|
+
const intent = exitGate.claim(exitIntentFromArchiveFlag(opts.archive));
|
|
14382
|
+
if (intent === null) {
|
|
14383
|
+
logger.debug(`[START] Termination (archive=${opts.archive ?? true}) ignored: cleanup already running as ${exitGate.current}`);
|
|
14384
|
+
return;
|
|
14385
|
+
}
|
|
14386
|
+
logger.debug(`[START] Received termination signal, cleaning up (archive=${intent === "archive"})...`);
|
|
14755
14387
|
try {
|
|
14756
14388
|
if (session) {
|
|
14757
|
-
if (opts.archive ?? true) {
|
|
14758
|
-
session.updateMetadata((currentMetadata) => ({
|
|
14759
|
-
...currentMetadata,
|
|
14760
|
-
lifecycleState: "archived",
|
|
14761
|
-
lifecycleStateSince: Date.now(),
|
|
14762
|
-
archivedBy: "cli",
|
|
14763
|
-
archiveReason: "User terminated"
|
|
14764
|
-
}));
|
|
14765
|
-
}
|
|
14766
14389
|
currentSession?.cleanup();
|
|
14767
|
-
session.
|
|
14768
|
-
|
|
14769
|
-
|
|
14770
|
-
|
|
14771
|
-
|
|
14772
|
-
|
|
14390
|
+
await settleSessionOnExit(session.sessionId, intent, {
|
|
14391
|
+
updateMetadata: (handler) => session.updateMetadata(handler),
|
|
14392
|
+
sendSessionDeath: () => session.sendSessionDeath(),
|
|
14393
|
+
archiveSession: (id) => api.archiveSession(id),
|
|
14394
|
+
deactivateSession: (id) => api.deactivateSession(id),
|
|
14395
|
+
log: (message, ...args) => logger.debug(message, ...args)
|
|
14396
|
+
});
|
|
14773
14397
|
await session.flush();
|
|
14774
14398
|
await session.close();
|
|
14775
14399
|
}
|
|
@@ -18397,6 +18021,7 @@ async function runCodex(opts) {
|
|
|
18397
18021
|
return "denied";
|
|
18398
18022
|
}
|
|
18399
18023
|
});
|
|
18024
|
+
const editThrottle = new EditReportThrottle();
|
|
18400
18025
|
client.setEventHandler((msg) => {
|
|
18401
18026
|
if (msg.type === "token_count") {
|
|
18402
18027
|
session.sendAgentUsageSnapshot("codex", msg);
|
|
@@ -18460,6 +18085,9 @@ async function runCodex(opts) {
|
|
|
18460
18085
|
}
|
|
18461
18086
|
if (msg.type === "patch_apply_begin") {
|
|
18462
18087
|
const { changes } = msg;
|
|
18088
|
+
for (const edit of editThrottle.take(extractCodexPatchPaths(changes))) {
|
|
18089
|
+
reportSessionEditToDaemon({ sessionId: session.sessionId, path: edit.path, tool: edit.tool, cwd: process.cwd() });
|
|
18090
|
+
}
|
|
18463
18091
|
const changeCount = Object.keys(changes).length;
|
|
18464
18092
|
const filesMsg = changeCount === 1 ? "1 file" : `${changeCount} files`;
|
|
18465
18093
|
messageBuffer.addMessage(`Modifying ${filesMsg}...`, "tool");
|
|
@@ -18505,6 +18133,7 @@ async function runCodex(opts) {
|
|
|
18505
18133
|
};
|
|
18506
18134
|
let first = true;
|
|
18507
18135
|
let appendSystemPromptInjected = false;
|
|
18136
|
+
process.env.VH_PEER_SESSION_ID = session.sessionId;
|
|
18508
18137
|
try {
|
|
18509
18138
|
logger.debug("[codex]: client.connect begin");
|
|
18510
18139
|
await client.connect();
|
|
@@ -18918,7 +18547,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
18918
18547
|
return;
|
|
18919
18548
|
} else if (subcommand === "install-terminal-hooks") {
|
|
18920
18549
|
try {
|
|
18921
|
-
const { installTerminalHooks, parseTerminalHooksArgs, TERMINAL_HOOKS_HELP } = await import('./installTerminalHooks-
|
|
18550
|
+
const { installTerminalHooks, parseTerminalHooksArgs, TERMINAL_HOOKS_HELP } = await import('./installTerminalHooks-CMpOV3RU.mjs');
|
|
18922
18551
|
const command = parseTerminalHooksArgs(args.slice(1));
|
|
18923
18552
|
if (command.action === "help") {
|
|
18924
18553
|
console.log(TERMINAL_HOOKS_HELP);
|
|
@@ -18935,7 +18564,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
18935
18564
|
}
|
|
18936
18565
|
} else if (subcommand === "agent-home") {
|
|
18937
18566
|
try {
|
|
18938
|
-
const { parseAgentHomeArgs, runAgentHomeCommand } = await import('./agentHome-
|
|
18567
|
+
const { parseAgentHomeArgs, runAgentHomeCommand } = await import('./agentHome-D2XGsoqs.mjs');
|
|
18939
18568
|
await runAgentHomeCommand(parseAgentHomeArgs(args.slice(1)));
|
|
18940
18569
|
process.exit(0);
|
|
18941
18570
|
} catch (error) {
|
|
@@ -18944,7 +18573,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
18944
18573
|
}
|
|
18945
18574
|
} else if (subcommand === "spawn") {
|
|
18946
18575
|
try {
|
|
18947
|
-
const { handleSpawnCommand } = await import('./spawn-
|
|
18576
|
+
const { handleSpawnCommand } = await import('./spawn-BKNabutV.mjs');
|
|
18948
18577
|
await handleSpawnCommand(args.slice(1));
|
|
18949
18578
|
} catch (error) {
|
|
18950
18579
|
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|
|
@@ -18956,7 +18585,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
18956
18585
|
return;
|
|
18957
18586
|
} else if (subcommand === "sessions") {
|
|
18958
18587
|
try {
|
|
18959
|
-
const { handleSessionsCommand } = await import('./sessions-
|
|
18588
|
+
const { handleSessionsCommand } = await import('./sessions-Bn304xF1.mjs');
|
|
18960
18589
|
await handleSessionsCommand(args.slice(1));
|
|
18961
18590
|
} catch (error) {
|
|
18962
18591
|
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|
|
@@ -18968,7 +18597,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
18968
18597
|
return;
|
|
18969
18598
|
} else if (subcommand === "send") {
|
|
18970
18599
|
try {
|
|
18971
|
-
const { handleSendCommand } = await import('./send-
|
|
18600
|
+
const { handleSendCommand } = await import('./send-BaOkDgH0.mjs');
|
|
18972
18601
|
await handleSendCommand(args.slice(1));
|
|
18973
18602
|
} catch (error) {
|
|
18974
18603
|
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|
|
@@ -19074,9 +18703,9 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
19074
18703
|
if (geminiSubcommand === "project" && args[2] === "set" && args[3]) {
|
|
19075
18704
|
const projectId = args[3];
|
|
19076
18705
|
try {
|
|
19077
|
-
const { saveGoogleCloudProjectToConfig } = await import('./config-
|
|
19078
|
-
const { readCredentialsForConfiguredRelay: readCredentialsForConfiguredRelay2 } = await import('./limits-
|
|
19079
|
-
const { ApiClient: ApiClient2 } = await import('./types-
|
|
18706
|
+
const { saveGoogleCloudProjectToConfig } = await import('./config-BkTu25bH.mjs');
|
|
18707
|
+
const { readCredentialsForConfiguredRelay: readCredentialsForConfiguredRelay2 } = await import('./limits-DmMdchTz.mjs').then(function (n) { return n.aG; });
|
|
18708
|
+
const { ApiClient: ApiClient2 } = await import('./types-BEVzYSoV.mjs').then(function (n) { return n.a1; });
|
|
19080
18709
|
let userEmail = void 0;
|
|
19081
18710
|
try {
|
|
19082
18711
|
const credentials = await readCredentialsForConfiguredRelay2();
|
|
@@ -19107,7 +18736,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
19107
18736
|
}
|
|
19108
18737
|
if (geminiSubcommand === "project" && args[2] === "get") {
|
|
19109
18738
|
try {
|
|
19110
|
-
const { readGeminiLocalConfig } = await import('./config-
|
|
18739
|
+
const { readGeminiLocalConfig } = await import('./config-BkTu25bH.mjs');
|
|
19111
18740
|
const config = readGeminiLocalConfig();
|
|
19112
18741
|
if (config.googleCloudProject) {
|
|
19113
18742
|
console.log(`Current Google Cloud Project: ${config.googleCloudProject}`);
|
|
@@ -19147,7 +18776,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
19147
18776
|
process.exit(0);
|
|
19148
18777
|
}
|
|
19149
18778
|
try {
|
|
19150
|
-
const { runGemini } = await import('./runGemini-
|
|
18779
|
+
const { runGemini } = await import('./runGemini-CZ_lhMWY.mjs');
|
|
19151
18780
|
let startedBy = void 0;
|
|
19152
18781
|
for (let i = 1; i < args.length; i++) {
|
|
19153
18782
|
if (args[i] === "--started-by") {
|
|
@@ -19170,7 +18799,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
19170
18799
|
} else if (subcommand === "pi") {
|
|
19171
18800
|
if (args[1] === "--terminal") {
|
|
19172
18801
|
try {
|
|
19173
|
-
const { runPiTerminal } = await import('./piTerminal-
|
|
18802
|
+
const { runPiTerminal } = await import('./piTerminal-DanZeBrO.mjs');
|
|
19174
18803
|
process.exitCode = await runPiTerminal(args.slice(2));
|
|
19175
18804
|
} catch (error) {
|
|
19176
18805
|
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|
|
@@ -19187,7 +18816,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
19187
18816
|
}
|
|
19188
18817
|
};
|
|
19189
18818
|
try {
|
|
19190
|
-
const { runAcp, resolveAcpAgentConfig } = await import('./index-
|
|
18819
|
+
const { runAcp, resolveAcpAgentConfig } = await import('./index-B5QbzEga.mjs');
|
|
19191
18820
|
const parsed = parsePiRunnerArgs(args.slice(1));
|
|
19192
18821
|
const resolved = resolveAcpAgentConfig(["pi", ...parsed.passthrough]);
|
|
19193
18822
|
const { credentials } = await authAndSetupMachineIfNeeded();
|
|
@@ -19215,7 +18844,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
19215
18844
|
return;
|
|
19216
18845
|
} else if (subcommand === "acp") {
|
|
19217
18846
|
try {
|
|
19218
|
-
const { runAcp, resolveAcpAgentConfig } = await import('./index-
|
|
18847
|
+
const { runAcp, resolveAcpAgentConfig } = await import('./index-B5QbzEga.mjs');
|
|
19219
18848
|
let startedBy = void 0;
|
|
19220
18849
|
let verbose = false;
|
|
19221
18850
|
const acpArgs = [];
|
|
@@ -19255,7 +18884,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
19255
18884
|
return;
|
|
19256
18885
|
} else if (subcommand === "openclaw") {
|
|
19257
18886
|
try {
|
|
19258
|
-
const { runOpenClaw } = await import('./runOpenClaw-
|
|
18887
|
+
const { runOpenClaw } = await import('./runOpenClaw-CxgLpm38.mjs');
|
|
19259
18888
|
let startedBy = void 0;
|
|
19260
18889
|
let verbose = false;
|
|
19261
18890
|
let gatewayUrl;
|
|
@@ -19306,7 +18935,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
19306
18935
|
return;
|
|
19307
18936
|
} else if (subcommand === "todo") {
|
|
19308
18937
|
try {
|
|
19309
|
-
const { handleTodoCommand } = await import('./todo-
|
|
18938
|
+
const { handleTodoCommand } = await import('./todo-BcPbNKt2.mjs');
|
|
19310
18939
|
await handleTodoCommand(args.slice(1));
|
|
19311
18940
|
} catch (error) {
|
|
19312
18941
|
console.error(JSON.stringify({ error: error instanceof Error ? error.message : "Todo command failed" }));
|
|
@@ -19315,7 +18944,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
19315
18944
|
return;
|
|
19316
18945
|
} else if (subcommand === "teams") {
|
|
19317
18946
|
try {
|
|
19318
|
-
const { handleTeamsCommand } = await import('./teams-
|
|
18947
|
+
const { handleTeamsCommand } = await import('./teams-CLbS2_4Q.mjs');
|
|
19319
18948
|
await handleTeamsCommand(args.slice(1));
|
|
19320
18949
|
} catch (error) {
|
|
19321
18950
|
console.error(error instanceof Error ? error.message : "Teams command failed");
|
|
@@ -19324,7 +18953,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
19324
18953
|
return;
|
|
19325
18954
|
} else if (subcommand === "auto") {
|
|
19326
18955
|
try {
|
|
19327
|
-
const { handleAutoCommand } = await import('./auto-
|
|
18956
|
+
const { handleAutoCommand } = await import('./auto-B1I6G_Nf.mjs');
|
|
19328
18957
|
await handleAutoCommand(args.slice(1));
|
|
19329
18958
|
} catch (error) {
|
|
19330
18959
|
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Automation command failed");
|
|
@@ -19336,7 +18965,7 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
19336
18965
|
return;
|
|
19337
18966
|
} else if (subcommand === "mcp" && (args.length === 1 || args.length === 2 && args[1] === "--terminal-tools")) {
|
|
19338
18967
|
try {
|
|
19339
|
-
const { handleMcpCommand } = await import('./mcp-
|
|
18968
|
+
const { handleMcpCommand } = await import('./mcp-C_uejotl.mjs');
|
|
19340
18969
|
await handleMcpCommand(args[1] === "--terminal-tools");
|
|
19341
18970
|
} catch (error) {
|
|
19342
18971
|
process.stderr.write(`[very-happy mcp] Fatal: ${error instanceof Error ? error.message : String(error)}
|
|
@@ -19365,6 +18994,8 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
19365
18994
|
} else {
|
|
19366
18995
|
console.log("Active sessions:");
|
|
19367
18996
|
console.log(JSON.stringify(sessions, null, 2));
|
|
18997
|
+
const busy = sessions.filter((s) => s.turnActive === true);
|
|
18998
|
+
console.log(busy.length === 0 ? "No turn in flight on this machine (wrappers older than 0.2.139 do not report turns)." : `${busy.length} session(s) with a turn in flight: ${busy.map((s) => s.happySessionId).join(", ")}`);
|
|
19368
18999
|
}
|
|
19369
19000
|
} catch (error) {
|
|
19370
19001
|
console.log("No daemon running");
|
|
@@ -19393,13 +19024,19 @@ Conversation history is preserved on the server, but in-flight tool calls are in
|
|
|
19393
19024
|
configuration.webappUrl
|
|
19394
19025
|
);
|
|
19395
19026
|
if (running && !currentDaemonMatches) {
|
|
19027
|
+
if (currentState?.supervisor === "systemd") {
|
|
19028
|
+
console.error(`The running daemon is supervised by systemd; hand over through the unit instead:
|
|
19029
|
+
${SYSTEMD_RESTART_HINT}`);
|
|
19030
|
+
process.exit(1);
|
|
19031
|
+
}
|
|
19396
19032
|
console.log("Configured relay or CLI version changed; restarting the existing daemon");
|
|
19397
19033
|
await stopDaemon();
|
|
19398
19034
|
}
|
|
19035
|
+
const { HAPPY_DAEMON_SUPERVISOR: _supervisor, INVOCATION_ID: _invocation, ...detachedEnv } = process.env;
|
|
19399
19036
|
const child = spawnHappyCLI(["daemon", "start-sync"], {
|
|
19400
19037
|
detached: true,
|
|
19401
19038
|
stdio: "ignore",
|
|
19402
|
-
env:
|
|
19039
|
+
env: detachedEnv
|
|
19403
19040
|
});
|
|
19404
19041
|
child.unref();
|
|
19405
19042
|
let started = false;
|
|
@@ -19706,4 +19343,4 @@ ${chalk.bold("Examples:")}
|
|
|
19706
19343
|
}
|
|
19707
19344
|
}
|
|
19708
19345
|
|
|
19709
|
-
export {
|
|
19346
|
+
export { ALLOWED_SPAWN_PERMISSION_MODES as A, BasePermissionHandler as B, CLAUDE_ATTACHMENT_KINDS as C, DEFAULT_GEMINI_MODEL as D, EditReportThrottle as E, normalizeAcpPermissionMode as F, GEMINI_MODEL_ENV as G, removeSessionModeFile as H, GEMINI_API_KEY_ENV as I, GOOGLE_API_KEY_ENV as J, BaseReasoningProcessor as K, MessageBuffer as L, MessageQueue2 as M, CHANGE_TITLE_INSTRUCTION as N, PI_TEAMS_EXTENSION as P, SPAWN_AGENTS as S, TEAM_SKILL as T, describeAction as a, tmuxSupportsSessionEnv as b, resolveFirstMessageMeta as c, describeTrigger as d, checkPreviewPath as e, registerAssistantSessionTools as f, suggestTitleForPrompt as g, initialMachineMetadata as h, isValidSpawnOrigin as i, createSessionMetadata as j, setupOfflineReconnection as k, StreamRelay as l, hashObject as m, startHappyServer as n, registerAgentAttachmentDownloads as o, parseDuration as p, query as q, resolveSpawnPermissionMode as r, sanitizeSpawnPermissionMode as s, triggerFromFlags as t, TitleGenerator as u, registerKillSessionHandler as v, stageClaudeAttachments as w, appendStagedAttachmentsToPrompt as x, extractAcpEditPaths as y, writeSessionModeFile as z };
|