very-happy-cli 0.2.120 → 0.2.121

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/{AcpBackend-DiIJaJWG.mjs → AcpBackend-BxvQ6O4k.mjs} +1 -1
  2. package/dist/{AcpBackend-tou8FQXI.cjs → AcpBackend-Cdj56ZfU.cjs} +1 -1
  3. package/dist/{AcpSessionManager-fF_HFB98.cjs → AcpSessionManager-DahCU78z.cjs} +51 -11
  4. package/dist/{AcpSessionManager-CZ0Az0GG.mjs → AcpSessionManager-DlWlQVhw.mjs} +51 -11
  5. package/dist/{config-CEbOao_0.cjs → config-DxOhdFEx.cjs} +2 -2
  6. package/dist/{config-CGDxsTZg.mjs → config-osO37dR1.mjs} +2 -2
  7. package/dist/{index-D4_LW69g.mjs → index-BcwAnM9k.mjs} +25 -11
  8. package/dist/{index-HKcuQ_fQ.cjs → index-Bv53Zygv.cjs} +24 -10
  9. package/dist/{index-BlkIz9BQ.cjs → index-DObePs_K.cjs} +143 -44
  10. package/dist/{index-DKj_0dqB.mjs → index-DtZFY8aS.mjs} +2 -2
  11. package/dist/{index-CUJ6faw1.mjs → index-Iz-M3qbz.mjs} +139 -41
  12. package/dist/{index-CYizH-JC.cjs → index-Vjo1jIJH.cjs} +2 -2
  13. package/dist/index.cjs +2 -2
  14. package/dist/index.mjs +2 -2
  15. package/dist/{installTerminalHooks-B3EGNaJr.mjs → installTerminalHooks-Bq11vOqF.mjs} +2 -2
  16. package/dist/{installTerminalHooks-MWVPJDxV.cjs → installTerminalHooks-DehsXLmV.cjs} +2 -2
  17. package/dist/lib.cjs +1 -1
  18. package/dist/lib.d.cts +25 -1
  19. package/dist/lib.d.mts +25 -1
  20. package/dist/lib.mjs +1 -1
  21. package/dist/{mcp-CZ1Z6AI5.mjs → mcp-Bx1B3uny.mjs} +2 -2
  22. package/dist/{mcp-DBKk5L6_.cjs → mcp-Cpx9Qlch.cjs} +2 -2
  23. package/dist/{runGemini-C3BCKTMp.mjs → runGemini-CMgudbJs.mjs} +9 -5
  24. package/dist/{runGemini-BRUmjZgV.cjs → runGemini-DI8DipUx.cjs} +9 -5
  25. package/dist/{runOpenClaw-o1ImBlcJ.cjs → runOpenClaw-CetdjTl-.cjs} +3 -3
  26. package/dist/{runOpenClaw-DEYzcGSV.mjs → runOpenClaw-DR3fLtVf.mjs} +3 -3
  27. package/dist/{send-B9fh96vR.cjs → send-Cq0NZCn8.cjs} +2 -2
  28. package/dist/{send-DFGH_-s_.mjs → send-M4b_hD7t.mjs} +2 -2
  29. package/dist/{sessions-CKVfXHyi.mjs → sessions-CelkWhqX.mjs} +11 -4
  30. package/dist/{sessions-BNyEJ2_6.cjs → sessions-DW4lrLOp.cjs} +11 -4
  31. package/dist/{spawn-B6kP4YjA.mjs → spawn-C4fF0q3Z.mjs} +2 -2
  32. package/dist/{spawn-xPV7-s4S.cjs → spawn-D3cwBlWI.cjs} +2 -2
  33. package/dist/{types-7ZSxm6Pi.cjs → types-COOYpI8_.cjs} +135 -10
  34. package/dist/{types-Di0o0BdV.mjs → types-P2cJWD0E.mjs} +134 -10
  35. package/package.json +7 -7
@@ -2,10 +2,10 @@ import { useStdout, useInput, Box, Text, render } from 'ink';
2
2
  import React, { useState, useRef, useEffect, useCallback } from 'react';
3
3
  import { randomUUID } from 'node:crypto';
4
4
  import { join } from 'node:path';
5
- import { l as logger, d as connectionState, A as ApiClient, e as readSettings, h as errorLogMetadata, f as encodeBase64, p as projectPath, g as contentLogMetadata } from './types-Di0o0BdV.mjs';
6
- import { I as GEMINI_API_KEY_ENV, J as GOOGLE_API_KEY_ENV, G as GEMINI_MODEL_ENV, C as BasePermissionHandler, K as BaseReasoningProcessor, n as initialMachineMetadata, o as createSessionMetadata, u as setupOfflineReconnection, v as notifyDaemonSessionStarted, x as MessageQueue2, y as hashObject, L as MessageBuffer, N as CHANGE_TITLE_INSTRUCTION, B as registerKillSessionHandler, z as startHappyServer } from './index-CUJ6faw1.mjs';
7
- import { A as AcpBackend } from './AcpBackend-DiIJaJWG.mjs';
8
- import { readGeminiLocalConfig, determineGeminiModel, getGeminiModelSource, getInitialGeminiModel, saveGeminiModelToConfig } from './config-CGDxsTZg.mjs';
5
+ import { l as logger, d as connectionState, A as ApiClient, e as readSettings, h as errorLogMetadata, f as encodeBase64, p as projectPath, g as contentLogMetadata } from './types-P2cJWD0E.mjs';
6
+ import { J as GEMINI_API_KEY_ENV, K as GOOGLE_API_KEY_ENV, G as GEMINI_MODEL_ENV, E as BasePermissionHandler, L as BaseReasoningProcessor, n as initialMachineMetadata, o as createSessionMetadata, u as setupOfflineReconnection, v as notifyDaemonSessionStarted, y as MessageQueue2, z as hashObject, N as MessageBuffer, O as CHANGE_TITLE_INSTRUCTION, C as registerKillSessionHandler, B as startHappyServer } from './index-Iz-M3qbz.mjs';
7
+ import { A as AcpBackend } from './AcpBackend-BxvQ6O4k.mjs';
8
+ import { readGeminiLocalConfig, determineGeminiModel, getGeminiModelSource, getInitialGeminiModel, saveGeminiModelToConfig } from './config-osO37dR1.mjs';
9
9
  import 'axios';
10
10
  import 'chalk';
11
11
  import 'node:util';
@@ -960,6 +960,10 @@ async function runGemini(opts) {
960
960
  permissionMode: mode.permissionMode,
961
961
  model: mode.model
962
962
  }));
963
+ messageQueue.setOnDiscard((entries) => {
964
+ const keys = entries.map((entry) => entry.sourceId).filter((key) => typeof key === "string");
965
+ if (keys.length > 0) session.sendQueueCancelReason(keys, entries[0].reason);
966
+ });
963
967
  const conversationHistory = new ConversationHistory({ maxMessages: 20, maxCharacters: 5e4 });
964
968
  let currentPermissionMode = void 0;
965
969
  let currentModel = void 0;
@@ -1010,7 +1014,7 @@ async function runGemini(opts) {
1010
1014
  originalUserMessage
1011
1015
  // Store original message separately
1012
1016
  };
1013
- messageQueue.push(fullPrompt, mode);
1017
+ messageQueue.push(fullPrompt, mode, void 0, message.localKey);
1014
1018
  conversationHistory.addUserMessage(originalUserMessage);
1015
1019
  });
1016
1020
  let thinking = false;
@@ -4,10 +4,10 @@ var ink = require('ink');
4
4
  var React = require('react');
5
5
  var node_crypto = require('node:crypto');
6
6
  var path = require('node:path');
7
- var persistence = require('./types-7ZSxm6Pi.cjs');
8
- var index = require('./index-BlkIz9BQ.cjs');
9
- var AcpBackend = require('./AcpBackend-tou8FQXI.cjs');
10
- var config = require('./config-CEbOao_0.cjs');
7
+ var persistence = require('./types-COOYpI8_.cjs');
8
+ var index = require('./index-DObePs_K.cjs');
9
+ var AcpBackend = require('./AcpBackend-Cdj56ZfU.cjs');
10
+ var config = require('./config-DxOhdFEx.cjs');
11
11
  require('axios');
12
12
  require('chalk');
13
13
  require('node:util');
@@ -962,6 +962,10 @@ async function runGemini(opts) {
962
962
  permissionMode: mode.permissionMode,
963
963
  model: mode.model
964
964
  }));
965
+ messageQueue.setOnDiscard((entries) => {
966
+ const keys = entries.map((entry) => entry.sourceId).filter((key) => typeof key === "string");
967
+ if (keys.length > 0) session.sendQueueCancelReason(keys, entries[0].reason);
968
+ });
965
969
  const conversationHistory = new ConversationHistory({ maxMessages: 20, maxCharacters: 5e4 });
966
970
  let currentPermissionMode = void 0;
967
971
  let currentModel = void 0;
@@ -1012,7 +1016,7 @@ async function runGemini(opts) {
1012
1016
  originalUserMessage
1013
1017
  // Store original message separately
1014
1018
  };
1015
- messageQueue.push(fullPrompt, mode);
1019
+ messageQueue.push(fullPrompt, mode, void 0, message.localKey);
1016
1020
  conversationHistory.addUserMessage(originalUserMessage);
1017
1021
  });
1018
1022
  let thinking = false;
@@ -5,9 +5,9 @@ var node_child_process = require('node:child_process');
5
5
  var os = require('node:os');
6
6
  var node_fs = require('node:fs');
7
7
  var path = require('node:path');
8
- var persistence = require('./types-7ZSxm6Pi.cjs');
9
- var AcpSessionManager = require('./AcpSessionManager-fF_HFB98.cjs');
10
- var index = require('./index-BlkIz9BQ.cjs');
8
+ var persistence = require('./types-COOYpI8_.cjs');
9
+ var AcpSessionManager = require('./AcpSessionManager-DahCU78z.cjs');
10
+ var index = require('./index-DObePs_K.cjs');
11
11
  var WebSocket = require('ws');
12
12
  var ed = require('@noble/ed25519');
13
13
  var sha2_js = require('@noble/hashes/sha2.js');
@@ -3,9 +3,9 @@ import { execFileSync } from 'node:child_process';
3
3
  import os from 'node:os';
4
4
  import { existsSync, readFileSync } from 'node:fs';
5
5
  import { join } from 'node:path';
6
- import { m as ensurePrivateDirectorySync, n as hardenPrivateFileSync, w as writePrivateFileSync, h as errorLogMetadata, g as contentLogMetadata, d as connectionState, A as ApiClient, e as readSettings, f as encodeBase64, l as logger, c as configuration } from './types-Di0o0BdV.mjs';
7
- import { A as AcpSessionManager } from './AcpSessionManager-CZ0Az0GG.mjs';
8
- import { n as initialMachineMetadata, o as createSessionMetadata, u as setupOfflineReconnection, v as notifyDaemonSessionStarted, x as MessageQueue2, B as registerKillSessionHandler } from './index-CUJ6faw1.mjs';
6
+ import { m as ensurePrivateDirectorySync, n as hardenPrivateFileSync, w as writePrivateFileSync, h as errorLogMetadata, g as contentLogMetadata, d as connectionState, A as ApiClient, e as readSettings, f as encodeBase64, l as logger, c as configuration } from './types-P2cJWD0E.mjs';
7
+ import { A as AcpSessionManager } from './AcpSessionManager-DlWlQVhw.mjs';
8
+ import { n as initialMachineMetadata, o as createSessionMetadata, u as setupOfflineReconnection, v as notifyDaemonSessionStarted, y as MessageQueue2, C as registerKillSessionHandler } from './index-Iz-M3qbz.mjs';
9
9
  import WebSocket from 'ws';
10
10
  import * as ed from '@noble/ed25519';
11
11
  import { sha512 } from '@noble/hashes/sha2.js';
@@ -3,8 +3,8 @@
3
3
  var chalk = require('chalk');
4
4
  var node_fs = require('node:fs');
5
5
  var path = require('node:path');
6
- var persistence = require('./types-7ZSxm6Pi.cjs');
7
- var index = require('./index-BlkIz9BQ.cjs');
6
+ var persistence = require('./types-COOYpI8_.cjs');
7
+ var index = require('./index-DObePs_K.cjs');
8
8
  require('axios');
9
9
  require('node:util');
10
10
  require('node:os');
@@ -1,8 +1,8 @@
1
1
  import chalk from 'chalk';
2
2
  import { readFileSync } from 'node:fs';
3
3
  import { resolve } from 'node:path';
4
- import { r as readPersistedSessions, c as configuration } from './types-Di0o0BdV.mjs';
5
- import { d as sendUserMessage, a as sessionWebUrl } from './index-CUJ6faw1.mjs';
4
+ import { r as readPersistedSessions, c as configuration } from './types-P2cJWD0E.mjs';
5
+ import { d as sendUserMessage, a as sessionWebUrl } from './index-Iz-M3qbz.mjs';
6
6
  import 'axios';
7
7
  import 'node:util';
8
8
  import 'node:os';
@@ -1,6 +1,6 @@
1
1
  import chalk from 'chalk';
2
- import { D as DEFAULT_RECENT_LIMIT, M as MAX_READ_LIMIT, l as listAccountSessions, e as listSessions, r as readSessionTranscript, f as stopSession, g as resolvePermissionRequest, h as archiveSession } from './index-CUJ6faw1.mjs';
3
- import { i as isValidSessionId } from './types-Di0o0BdV.mjs';
2
+ import { D as DEFAULT_RECENT_LIMIT, M as MAX_READ_LIMIT, l as listAccountSessions, e as listSessions, r as readSessionTranscript, f as stopSession, g as resolvePermissionRequest, h as archiveSession } from './index-Iz-M3qbz.mjs';
3
+ import { i as isValidSessionId } from './types-P2cJWD0E.mjs';
4
4
  import 'node:os';
5
5
  import 'node:crypto';
6
6
  import 'cross-spawn';
@@ -286,14 +286,21 @@ async function handleSessionsCommand(args) {
286
286
  const verdict = options.action === "approve" ? { kind: "approve", forSession: options.forSession } : { kind: "deny", reason: options.reason };
287
287
  let result;
288
288
  try {
289
- result = await resolvePermissionRequest(sessionId, requestId, verdict);
289
+ result = await resolvePermissionRequest(sessionId, requestId, verdict, {
290
+ // T-014: a rate refusal waits (bounded) before re-sending;
291
+ // say so on stderr so the wait does not look like a hang.
292
+ onRateLimited: ({ attempt, waitMs, serverError }) => {
293
+ console.error(chalk.yellow("Rate limited:"), `${serverError} \u2014 waiting ${Math.ceil(waitMs / 1e3)}s before retry ${attempt}`);
294
+ }
295
+ });
290
296
  } catch (error) {
291
297
  if (options.json) console.log(JSON.stringify(permissionFailureRecord(sessionId, requestId, error)));
292
298
  throw error;
293
299
  }
294
300
  if (result.outcome.status !== "acknowledged") {
295
301
  if (options.json) console.log(JSON.stringify(result));
296
- console.error(chalk.red("Error:"), `${options.action} of ${requestId} on ${sessionId} was not delivered (${result.outcome.status}): ${result.outcome.message}`);
302
+ const hint = result.outcome.status === "rate-limited" ? ` \u2014 the request is still pending; re-run in ~${Math.ceil(result.outcome.retryAfterMs / 1e3)}s` : "";
303
+ console.error(chalk.red("Error:"), `${options.action} of ${requestId} on ${sessionId} was not delivered (${result.outcome.status}): ${result.outcome.message}${hint}`);
297
304
  process.exit(1);
298
305
  }
299
306
  if (options.json) {
@@ -1,8 +1,8 @@
1
1
  'use strict';
2
2
 
3
3
  var chalk = require('chalk');
4
- var index = require('./index-BlkIz9BQ.cjs');
5
- var persistence = require('./types-7ZSxm6Pi.cjs');
4
+ var index = require('./index-DObePs_K.cjs');
5
+ var persistence = require('./types-COOYpI8_.cjs');
6
6
  require('node:os');
7
7
  require('node:crypto');
8
8
  require('cross-spawn');
@@ -288,14 +288,21 @@ async function handleSessionsCommand(args) {
288
288
  const verdict = options.action === "approve" ? { kind: "approve", forSession: options.forSession } : { kind: "deny", reason: options.reason };
289
289
  let result;
290
290
  try {
291
- result = await index.resolvePermissionRequest(sessionId, requestId, verdict);
291
+ result = await index.resolvePermissionRequest(sessionId, requestId, verdict, {
292
+ // T-014: a rate refusal waits (bounded) before re-sending;
293
+ // say so on stderr so the wait does not look like a hang.
294
+ onRateLimited: ({ attempt, waitMs, serverError }) => {
295
+ console.error(chalk.yellow("Rate limited:"), `${serverError} \u2014 waiting ${Math.ceil(waitMs / 1e3)}s before retry ${attempt}`);
296
+ }
297
+ });
292
298
  } catch (error) {
293
299
  if (options.json) console.log(JSON.stringify(permissionFailureRecord(sessionId, requestId, error)));
294
300
  throw error;
295
301
  }
296
302
  if (result.outcome.status !== "acknowledged") {
297
303
  if (options.json) console.log(JSON.stringify(result));
298
- console.error(chalk.red("Error:"), `${options.action} of ${requestId} on ${sessionId} was not delivered (${result.outcome.status}): ${result.outcome.message}`);
304
+ const hint = result.outcome.status === "rate-limited" ? ` \u2014 the request is still pending; re-run in ~${Math.ceil(result.outcome.retryAfterMs / 1e3)}s` : "";
305
+ console.error(chalk.red("Error:"), `${options.action} of ${requestId} on ${sessionId} was not delivered (${result.outcome.status}): ${result.outcome.message}${hint}`);
299
306
  process.exit(1);
300
307
  }
301
308
  if (options.json) {
@@ -1,8 +1,8 @@
1
1
  import chalk from 'chalk';
2
2
  import { readFileSync, statSync } from 'node:fs';
3
3
  import { resolve } from 'node:path';
4
- import { c as checkIfDaemonRunningAndCleanupStaleState, s as spawnDaemonSession, a as sessionWebUrl, i as isValidSpawnOrigin, b as sanitizeSpawnPermissionMode, A as ALLOWED_SPAWN_PERMISSION_MODES, S as SPAWN_AGENTS, w as waitForSessionKey, d as sendUserMessage } from './index-CUJ6faw1.mjs';
5
- import { l as logger } from './types-Di0o0BdV.mjs';
4
+ import { c as checkIfDaemonRunningAndCleanupStaleState, s as spawnDaemonSession, a as sessionWebUrl, i as isValidSpawnOrigin, b as sanitizeSpawnPermissionMode, A as ALLOWED_SPAWN_PERMISSION_MODES, S as SPAWN_AGENTS, w as waitForSessionKey, d as sendUserMessage } from './index-Iz-M3qbz.mjs';
5
+ import { l as logger } from './types-P2cJWD0E.mjs';
6
6
  import 'node:os';
7
7
  import 'node:crypto';
8
8
  import 'cross-spawn';
@@ -3,8 +3,8 @@
3
3
  var chalk = require('chalk');
4
4
  var node_fs = require('node:fs');
5
5
  var path = require('node:path');
6
- var index = require('./index-BlkIz9BQ.cjs');
7
- var persistence = require('./types-7ZSxm6Pi.cjs');
6
+ var index = require('./index-DObePs_K.cjs');
7
+ var persistence = require('./types-COOYpI8_.cjs');
8
8
  require('node:os');
9
9
  require('node:crypto');
10
10
  require('cross-spawn');
@@ -51,7 +51,7 @@ var z__namespace = /*#__PURE__*/_interopNamespaceDefault(z);
51
51
  var pty__namespace = /*#__PURE__*/_interopNamespaceDefault(pty);
52
52
 
53
53
  var name = "very-happy-cli";
54
- var version = "0.2.120";
54
+ var version = "0.2.121";
55
55
  var description = "Remote multi-agent workspace CLI with deep Claude integration, native terminal streaming, and self-hosting.";
56
56
  var author = "Kirill Dubovitskiy";
57
57
  var contributors = [
@@ -164,12 +164,12 @@ var dependencies = {
164
164
  zod: "^4.0.0"
165
165
  };
166
166
  var optionalDependencies = {
167
- "very-happy-tools-arm64-darwin": "0.2.120",
168
- "very-happy-tools-x64-darwin": "0.2.120",
169
- "very-happy-tools-arm64-linux": "0.2.120",
170
- "very-happy-tools-x64-linux": "0.2.120",
171
- "very-happy-tools-arm64-win32": "0.2.120",
172
- "very-happy-tools-x64-win32": "0.2.120"
167
+ "very-happy-tools-arm64-darwin": "0.2.121",
168
+ "very-happy-tools-x64-darwin": "0.2.121",
169
+ "very-happy-tools-arm64-linux": "0.2.121",
170
+ "very-happy-tools-x64-linux": "0.2.121",
171
+ "very-happy-tools-arm64-win32": "0.2.121",
172
+ "very-happy-tools-x64-win32": "0.2.121"
173
173
  };
174
174
  var devDependencies = {
175
175
  "@slopus/happy-wire": "workspace:*",
@@ -742,6 +742,11 @@ const sessionProgressEventSchema = z__namespace.object({
742
742
  durationMs: z__namespace.number().optional(),
743
743
  summary: z__namespace.string().optional()
744
744
  });
745
+ const sessionQueueCancelEventSchema = z__namespace.object({
746
+ t: z__namespace.literal("queue-cancel"),
747
+ targetLocalKeys: z__namespace.array(z__namespace.string().min(1)).min(1),
748
+ reason: z__namespace.string().min(1).optional()
749
+ });
745
750
  const sessionEventSchema = z__namespace.discriminatedUnion("t", [
746
751
  sessionTextEventSchema,
747
752
  sessionServiceMessageEventSchema,
@@ -752,7 +757,8 @@ const sessionEventSchema = z__namespace.discriminatedUnion("t", [
752
757
  sessionStartEventSchema,
753
758
  sessionTurnEndEventSchema,
754
759
  sessionStopEventSchema,
755
- sessionProgressEventSchema
760
+ sessionProgressEventSchema,
761
+ sessionQueueCancelEventSchema
756
762
  ]);
757
763
  const sessionEnvelopeSchema = z__namespace.object({
758
764
  id: z__namespace.string(),
@@ -1685,7 +1691,7 @@ class RpcHandlerManager {
1685
1691
  }
1686
1692
  }
1687
1693
 
1688
- const __dirname$1 = path$1.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('types-7ZSxm6Pi.cjs', document.baseURI).href))));
1694
+ const __dirname$1 = path$1.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('types-COOYpI8_.cjs', document.baseURI).href))));
1689
1695
  function projectPath() {
1690
1696
  const path = path$1.resolve(__dirname$1, "..");
1691
1697
  return path;
@@ -3135,6 +3141,48 @@ class InvalidateSync {
3135
3141
  };
3136
3142
  }
3137
3143
 
3144
+ function sessionEnvelopeOf(body) {
3145
+ if (!body || typeof body !== "object") return null;
3146
+ const record = body;
3147
+ if (record.role !== "session" || !record.content || typeof record.content !== "object") return null;
3148
+ const content = record.content;
3149
+ if (content.type === "session" && content.data && typeof content.data === "object") {
3150
+ return content.data;
3151
+ }
3152
+ if (content.ev !== void 0) return content;
3153
+ return null;
3154
+ }
3155
+ function isQueuedUserText(body) {
3156
+ if (!body || typeof body !== "object") return false;
3157
+ const record = body;
3158
+ return record.role === "user" && record.content?.type === "text" && typeof record.meta?.queuedAt === "number";
3159
+ }
3160
+ function scanUndeliveredQueuedInputs(records, prior) {
3161
+ const canceled = prior?.canceled ?? /* @__PURE__ */ new Set();
3162
+ const undelivered = [];
3163
+ let reachedBoundary = false;
3164
+ for (const record of records) {
3165
+ const envelope = sessionEnvelopeOf(record.body);
3166
+ if (envelope) {
3167
+ const t = envelope.ev?.t;
3168
+ if (t === "turn-end") {
3169
+ reachedBoundary = true;
3170
+ break;
3171
+ }
3172
+ if (t === "queue-cancel" && Array.isArray(envelope.ev?.targetLocalKeys)) {
3173
+ for (const key of envelope.ev.targetLocalKeys) {
3174
+ if (typeof key === "string") canceled.add(key);
3175
+ }
3176
+ }
3177
+ continue;
3178
+ }
3179
+ if (isQueuedUserText(record.body) && record.localId && !canceled.has(record.localId)) {
3180
+ undelivered.push(record.localId);
3181
+ }
3182
+ }
3183
+ return { undeliveredLocalKeys: undelivered, reachedBoundary, canceled };
3184
+ }
3185
+
3138
3186
  function record(value) {
3139
3187
  return value && typeof value === "object" && !Array.isArray(value) ? value : null;
3140
3188
  }
@@ -3778,8 +3826,75 @@ class ApiSessionClient extends node_events.EventEmitter {
3778
3826
  this.sendSync.invalidate();
3779
3827
  }
3780
3828
  }
3829
+ /** B-332: emit a queue-cancel tombstone, mirroring the web's own signal. */
3830
+ sendQueueCancelReason(localKeys, reason) {
3831
+ if (localKeys.length === 0) return;
3832
+ const envelope = createEnvelope("user", { t: "queue-cancel", targetLocalKeys: localKeys, reason });
3833
+ this.sendSessionProtocolMessage(envelope);
3834
+ }
3781
3835
  /**
3782
- * Send message to session
3836
+ * B-332 site ③: find queued user input this wrapper destroyed (it was still
3837
+ * in the previous wrapper's in-memory queue when it died) and tell the web.
3838
+ *
3839
+ * Walks NEWEST → OLDEST via `before_seq`, decrypting each page, and stops at
3840
+ * the first turn-end. The runner calls this on EVERY reconnect (runClaude /
3841
+ * runCodex), seeded or not: a cursor seeded at the server's latest seq skips
3842
+ * the undelivered tail just as thoroughly as skipExistingMessages does —
3843
+ * and the daemon's resume/restart always seeds, so gating on the skip path
3844
+ * would miss the common case. Best-effort: a failed scan must not take a
3845
+ * session down. Needs no socket; POSTs through the same session-message
3846
+ * channel so the web receives it identically to a cancel-button tombstone.
3847
+ */
3848
+ async cancelUndeliveredQueuedInputs() {
3849
+ const MAX_SCAN_PAGES = 10;
3850
+ const canceled = /* @__PURE__ */ new Set();
3851
+ const undelivered = [];
3852
+ let beforeSeq = 2147483647;
3853
+ let reachedBoundary = false;
3854
+ let pages = 0;
3855
+ try {
3856
+ while (!reachedBoundary && pages < MAX_SCAN_PAGES) {
3857
+ pages += 1;
3858
+ const response = await axios.get(
3859
+ `${configuration.serverUrl}/v3/sessions/${encodeURIComponent(this.sessionId)}/messages`,
3860
+ {
3861
+ params: { before_seq: beforeSeq, limit: 100 },
3862
+ headers: this.authHeaders(),
3863
+ timeout: 6e4
3864
+ }
3865
+ );
3866
+ const messages = Array.isArray(response.data.messages) ? response.data.messages : [];
3867
+ if (messages.length === 0) break;
3868
+ const records = messages.map((message) => {
3869
+ let body = null;
3870
+ try {
3871
+ body = message.content?.t === "encrypted" ? decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(message.content.c)) : null;
3872
+ } catch (error) {
3873
+ logger.debug("[API] Failed to decrypt during undelivered scan:", error);
3874
+ }
3875
+ return { seq: message.seq, localId: message.localId, body };
3876
+ });
3877
+ const scan = scanUndeliveredQueuedInputs(records, { canceled });
3878
+ for (const key of scan.canceled) canceled.add(key);
3879
+ undelivered.push(...scan.undeliveredLocalKeys);
3880
+ reachedBoundary = scan.reachedBoundary;
3881
+ const oldest = records.at(-1)?.seq;
3882
+ if (oldest === void 0 || messages.length < 100) break;
3883
+ beforeSeq = oldest;
3884
+ }
3885
+ } catch (error) {
3886
+ logger.debug("[API] cancelUndeliveredQueuedInputs scan failed:", error);
3887
+ return;
3888
+ }
3889
+ if (!reachedBoundary) {
3890
+ logger.debug("[API] cancelUndeliveredQueuedInputs found no turn boundary; leaving messages as-is");
3891
+ return;
3892
+ }
3893
+ undelivered.reverse();
3894
+ this.sendQueueCancelReason(undelivered, "restarted");
3895
+ }
3896
+ /**
3897
+ * Send a message to session
3783
3898
  * @param body - Message body (can be MessageContent or raw content for agent messages)
3784
3899
  */
3785
3900
  sendClaudeSessionMessage(body, opts) {
@@ -14552,6 +14667,7 @@ async function withCodexAppServerClient(handler) {
14552
14667
  await client.disconnect();
14553
14668
  }
14554
14669
  }
14670
+ const CLI_AVAILABILITY_RECHECK_MS = 5 * 60 * 1e3;
14555
14671
  class ApiMachineClient {
14556
14672
  constructor(token, machine) {
14557
14673
  this.token = token;
@@ -14575,6 +14691,10 @@ class ApiMachineClient {
14575
14691
  keepAliveInterval = null;
14576
14692
  lastKnownCLIAvailability = null;
14577
14693
  lastKnownResumeSupport = null;
14694
+ /** Wall-clock of the last CLI availability probe. Initialised as "already
14695
+ * stale" so the FIRST keep-alive tick always probes and populates the
14696
+ * machine metadata that a connecting/reconnecting web client reads. */
14697
+ lastCliProbeAt = Date.now() - CLI_AVAILABILITY_RECHECK_MS;
14578
14698
  cliUpdateState = null;
14579
14699
  cliUpdatePushChain = Promise.resolve();
14580
14700
  claudeAuthState = null;
@@ -15442,6 +15562,10 @@ class ApiMachineClient {
15442
15562
  }
15443
15563
  this.socket.emit("machine-alive", payload);
15444
15564
  void this.refreshRelayConnection();
15565
+ if (Date.now() - this.lastCliProbeAt < CLI_AVAILABILITY_RECHECK_MS) {
15566
+ return;
15567
+ }
15568
+ this.lastCliProbeAt = Date.now();
15445
15569
  const newAvailability = detectCLIAvailability();
15446
15570
  const prev = this.lastKnownCLIAvailability;
15447
15571
  const newResumeSupport = detectResumeSupport();
@@ -16745,6 +16869,7 @@ exports.scrubTmuxClientEnv = scrubTmuxClientEnv;
16745
16869
  exports.sendToVhTerminal = sendToVhTerminal;
16746
16870
  exports.shellescape = shellescape;
16747
16871
  exports.startOfflineReconnection = startOfflineReconnection;
16872
+ exports.streamKeyOf = streamKeyOf;
16748
16873
  exports.summarizeSpawnSessionForLog = summarizeSpawnSessionForLog;
16749
16874
  exports.tmuxArgs = tmuxArgs;
16750
16875
  exports.updateSettings = updateSettings;
@@ -29,7 +29,7 @@ import { SandboxManager } from '@anthropic-ai/sandbox-runtime';
29
29
  import { Expo } from 'expo-server-sdk';
30
30
 
31
31
  var name = "very-happy-cli";
32
- var version = "0.2.120";
32
+ var version = "0.2.121";
33
33
  var description = "Remote multi-agent workspace CLI with deep Claude integration, native terminal streaming, and self-hosting.";
34
34
  var author = "Kirill Dubovitskiy";
35
35
  var contributors = [
@@ -142,12 +142,12 @@ var dependencies = {
142
142
  zod: "^4.0.0"
143
143
  };
144
144
  var optionalDependencies = {
145
- "very-happy-tools-arm64-darwin": "0.2.120",
146
- "very-happy-tools-x64-darwin": "0.2.120",
147
- "very-happy-tools-arm64-linux": "0.2.120",
148
- "very-happy-tools-x64-linux": "0.2.120",
149
- "very-happy-tools-arm64-win32": "0.2.120",
150
- "very-happy-tools-x64-win32": "0.2.120"
145
+ "very-happy-tools-arm64-darwin": "0.2.121",
146
+ "very-happy-tools-x64-darwin": "0.2.121",
147
+ "very-happy-tools-arm64-linux": "0.2.121",
148
+ "very-happy-tools-x64-linux": "0.2.121",
149
+ "very-happy-tools-arm64-win32": "0.2.121",
150
+ "very-happy-tools-x64-win32": "0.2.121"
151
151
  };
152
152
  var devDependencies = {
153
153
  "@slopus/happy-wire": "workspace:*",
@@ -720,6 +720,11 @@ const sessionProgressEventSchema = z.object({
720
720
  durationMs: z.number().optional(),
721
721
  summary: z.string().optional()
722
722
  });
723
+ const sessionQueueCancelEventSchema = z.object({
724
+ t: z.literal("queue-cancel"),
725
+ targetLocalKeys: z.array(z.string().min(1)).min(1),
726
+ reason: z.string().min(1).optional()
727
+ });
723
728
  const sessionEventSchema = z.discriminatedUnion("t", [
724
729
  sessionTextEventSchema,
725
730
  sessionServiceMessageEventSchema,
@@ -730,7 +735,8 @@ const sessionEventSchema = z.discriminatedUnion("t", [
730
735
  sessionStartEventSchema,
731
736
  sessionTurnEndEventSchema,
732
737
  sessionStopEventSchema,
733
- sessionProgressEventSchema
738
+ sessionProgressEventSchema,
739
+ sessionQueueCancelEventSchema
734
740
  ]);
735
741
  const sessionEnvelopeSchema = z.object({
736
742
  id: z.string(),
@@ -3113,6 +3119,48 @@ class InvalidateSync {
3113
3119
  };
3114
3120
  }
3115
3121
 
3122
+ function sessionEnvelopeOf(body) {
3123
+ if (!body || typeof body !== "object") return null;
3124
+ const record = body;
3125
+ if (record.role !== "session" || !record.content || typeof record.content !== "object") return null;
3126
+ const content = record.content;
3127
+ if (content.type === "session" && content.data && typeof content.data === "object") {
3128
+ return content.data;
3129
+ }
3130
+ if (content.ev !== void 0) return content;
3131
+ return null;
3132
+ }
3133
+ function isQueuedUserText(body) {
3134
+ if (!body || typeof body !== "object") return false;
3135
+ const record = body;
3136
+ return record.role === "user" && record.content?.type === "text" && typeof record.meta?.queuedAt === "number";
3137
+ }
3138
+ function scanUndeliveredQueuedInputs(records, prior) {
3139
+ const canceled = prior?.canceled ?? /* @__PURE__ */ new Set();
3140
+ const undelivered = [];
3141
+ let reachedBoundary = false;
3142
+ for (const record of records) {
3143
+ const envelope = sessionEnvelopeOf(record.body);
3144
+ if (envelope) {
3145
+ const t = envelope.ev?.t;
3146
+ if (t === "turn-end") {
3147
+ reachedBoundary = true;
3148
+ break;
3149
+ }
3150
+ if (t === "queue-cancel" && Array.isArray(envelope.ev?.targetLocalKeys)) {
3151
+ for (const key of envelope.ev.targetLocalKeys) {
3152
+ if (typeof key === "string") canceled.add(key);
3153
+ }
3154
+ }
3155
+ continue;
3156
+ }
3157
+ if (isQueuedUserText(record.body) && record.localId && !canceled.has(record.localId)) {
3158
+ undelivered.push(record.localId);
3159
+ }
3160
+ }
3161
+ return { undeliveredLocalKeys: undelivered, reachedBoundary, canceled };
3162
+ }
3163
+
3116
3164
  function record(value) {
3117
3165
  return value && typeof value === "object" && !Array.isArray(value) ? value : null;
3118
3166
  }
@@ -3756,8 +3804,75 @@ class ApiSessionClient extends EventEmitter {
3756
3804
  this.sendSync.invalidate();
3757
3805
  }
3758
3806
  }
3807
+ /** B-332: emit a queue-cancel tombstone, mirroring the web's own signal. */
3808
+ sendQueueCancelReason(localKeys, reason) {
3809
+ if (localKeys.length === 0) return;
3810
+ const envelope = createEnvelope("user", { t: "queue-cancel", targetLocalKeys: localKeys, reason });
3811
+ this.sendSessionProtocolMessage(envelope);
3812
+ }
3759
3813
  /**
3760
- * Send message to session
3814
+ * B-332 site ③: find queued user input this wrapper destroyed (it was still
3815
+ * in the previous wrapper's in-memory queue when it died) and tell the web.
3816
+ *
3817
+ * Walks NEWEST → OLDEST via `before_seq`, decrypting each page, and stops at
3818
+ * the first turn-end. The runner calls this on EVERY reconnect (runClaude /
3819
+ * runCodex), seeded or not: a cursor seeded at the server's latest seq skips
3820
+ * the undelivered tail just as thoroughly as skipExistingMessages does —
3821
+ * and the daemon's resume/restart always seeds, so gating on the skip path
3822
+ * would miss the common case. Best-effort: a failed scan must not take a
3823
+ * session down. Needs no socket; POSTs through the same session-message
3824
+ * channel so the web receives it identically to a cancel-button tombstone.
3825
+ */
3826
+ async cancelUndeliveredQueuedInputs() {
3827
+ const MAX_SCAN_PAGES = 10;
3828
+ const canceled = /* @__PURE__ */ new Set();
3829
+ const undelivered = [];
3830
+ let beforeSeq = 2147483647;
3831
+ let reachedBoundary = false;
3832
+ let pages = 0;
3833
+ try {
3834
+ while (!reachedBoundary && pages < MAX_SCAN_PAGES) {
3835
+ pages += 1;
3836
+ const response = await axios.get(
3837
+ `${configuration.serverUrl}/v3/sessions/${encodeURIComponent(this.sessionId)}/messages`,
3838
+ {
3839
+ params: { before_seq: beforeSeq, limit: 100 },
3840
+ headers: this.authHeaders(),
3841
+ timeout: 6e4
3842
+ }
3843
+ );
3844
+ const messages = Array.isArray(response.data.messages) ? response.data.messages : [];
3845
+ if (messages.length === 0) break;
3846
+ const records = messages.map((message) => {
3847
+ let body = null;
3848
+ try {
3849
+ body = message.content?.t === "encrypted" ? decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(message.content.c)) : null;
3850
+ } catch (error) {
3851
+ logger.debug("[API] Failed to decrypt during undelivered scan:", error);
3852
+ }
3853
+ return { seq: message.seq, localId: message.localId, body };
3854
+ });
3855
+ const scan = scanUndeliveredQueuedInputs(records, { canceled });
3856
+ for (const key of scan.canceled) canceled.add(key);
3857
+ undelivered.push(...scan.undeliveredLocalKeys);
3858
+ reachedBoundary = scan.reachedBoundary;
3859
+ const oldest = records.at(-1)?.seq;
3860
+ if (oldest === void 0 || messages.length < 100) break;
3861
+ beforeSeq = oldest;
3862
+ }
3863
+ } catch (error) {
3864
+ logger.debug("[API] cancelUndeliveredQueuedInputs scan failed:", error);
3865
+ return;
3866
+ }
3867
+ if (!reachedBoundary) {
3868
+ logger.debug("[API] cancelUndeliveredQueuedInputs found no turn boundary; leaving messages as-is");
3869
+ return;
3870
+ }
3871
+ undelivered.reverse();
3872
+ this.sendQueueCancelReason(undelivered, "restarted");
3873
+ }
3874
+ /**
3875
+ * Send a message to session
3761
3876
  * @param body - Message body (can be MessageContent or raw content for agent messages)
3762
3877
  */
3763
3878
  sendClaudeSessionMessage(body, opts) {
@@ -14530,6 +14645,7 @@ async function withCodexAppServerClient(handler) {
14530
14645
  await client.disconnect();
14531
14646
  }
14532
14647
  }
14648
+ const CLI_AVAILABILITY_RECHECK_MS = 5 * 60 * 1e3;
14533
14649
  class ApiMachineClient {
14534
14650
  constructor(token, machine) {
14535
14651
  this.token = token;
@@ -14553,6 +14669,10 @@ class ApiMachineClient {
14553
14669
  keepAliveInterval = null;
14554
14670
  lastKnownCLIAvailability = null;
14555
14671
  lastKnownResumeSupport = null;
14672
+ /** Wall-clock of the last CLI availability probe. Initialised as "already
14673
+ * stale" so the FIRST keep-alive tick always probes and populates the
14674
+ * machine metadata that a connecting/reconnecting web client reads. */
14675
+ lastCliProbeAt = Date.now() - CLI_AVAILABILITY_RECHECK_MS;
14556
14676
  cliUpdateState = null;
14557
14677
  cliUpdatePushChain = Promise.resolve();
14558
14678
  claudeAuthState = null;
@@ -15420,6 +15540,10 @@ class ApiMachineClient {
15420
15540
  }
15421
15541
  this.socket.emit("machine-alive", payload);
15422
15542
  void this.refreshRelayConnection();
15543
+ if (Date.now() - this.lastCliProbeAt < CLI_AVAILABILITY_RECHECK_MS) {
15544
+ return;
15545
+ }
15546
+ this.lastCliProbeAt = Date.now();
15423
15547
  const newAvailability = detectCLIAvailability();
15424
15548
  const prev = this.lastKnownCLIAvailability;
15425
15549
  const newResumeSupport = detectResumeSupport();
@@ -16647,4 +16771,4 @@ const RawJSONLinesSchema = z$1.discriminatedUnion("type", [
16647
16771
  }).passthrough()
16648
16772
  ]);
16649
16773
 
16650
- export { decideMirrorBinding as $, ApiClient as A, readCredentials as B, credentialRelayProblem as C, deriveLocalCliUpdateSummary as D, readSettingsOutcome as E, updateSettings as F, encodeBase64Url as G, decodeBase64 as H, InvalidateSync as I, writeCredentialsLegacy as J, writeCredentialsDataKey as K, readCredentialsForConfiguredRelay as L, MAX_CHAT_ATTACHMENT_SOURCE_BYTES as M, encrypt as N, isClaudeSessionId as O, getLocalHappyAgentCredentialPath as P, readLocalHappyAgentCredentials as Q, RawJSONLinesSchema as R, STREAM_DELTA_MAX_CHARS as S, decryptWithDataKey as T, decryptLegacy as U, compareExactVersions as V, extractCompleteLines as W, decideBackfill as X, parseTerminalHookPayload as Y, tmuxArgs as Z, scrubTmuxClientEnv as _, ApiSessionClient as a, MIRROR_BACKFILL_LINES_DEFAULT as a0, persistSession as a1, mirrorLineKey as a2, mirrorLocalId as a3, detectCLIAvailability as a4, detectResumeSupport as a5, acquireDaemonLock as a6, summarizeSpawnSessionForLog as a7, deletePersistedSessions as a8, shellescape as a9, decrypt as aa, writeDaemonState as ab, writeSettings as ac, fetchCliUpdateState as ad, resolveCliUpdateCheckInterval as ae, DEFAULT_CLI_UPDATE_CHECK_INTERVAL_MS as af, releaseDaemonLock as ag, listVhTerminals as ah, isValidTerminalId as ai, readVhTerminal as aj, sendToVhTerminal as ak, TITLE_PROMPT_PREFIX as al, BOARD_ANALYZER_PROMPT_PREFIX as am, startOfflineReconnection as an, clearCredentials as ao, clearMachineId as ap, SandboxConfigSchema as aq, CodexAppServerClient as ar, getLatestDaemonLog as as, persistence as at, api as au, createEnvelope as b, configuration as c, connectionState as d, readSettings as e, encodeBase64 as f, contentLogMetadata as g, errorLogMetadata as h, isValidSessionId as i, packageJson as j, delay as k, logger as l, ensurePrivateDirectorySync as m, hardenPrivateFileSync as n, readDaemonState as o, projectPath as p, clearDaemonState as q, readPersistedSessions as r, getProjectPath as s, initializeSandbox as t, wrapCommand as u, AsyncLock as v, writePrivateFileSync as w, ensurePrivateDirectory as x, hardenPrivateFile as y, commandOnPath as z };
16774
+ export { scrubTmuxClientEnv as $, ApiClient as A, commandOnPath as B, readCredentials as C, credentialRelayProblem as D, deriveLocalCliUpdateSummary as E, readSettingsOutcome as F, updateSettings as G, encodeBase64Url as H, InvalidateSync as I, decodeBase64 as J, writeCredentialsLegacy as K, writeCredentialsDataKey as L, MAX_CHAT_ATTACHMENT_SOURCE_BYTES as M, readCredentialsForConfiguredRelay as N, encrypt as O, isClaudeSessionId as P, getLocalHappyAgentCredentialPath as Q, RawJSONLinesSchema as R, STREAM_DELTA_MAX_CHARS as S, readLocalHappyAgentCredentials as T, decryptWithDataKey as U, decryptLegacy as V, compareExactVersions as W, extractCompleteLines as X, decideBackfill as Y, parseTerminalHookPayload as Z, tmuxArgs as _, ApiSessionClient as a, decideMirrorBinding as a0, MIRROR_BACKFILL_LINES_DEFAULT as a1, persistSession as a2, mirrorLineKey as a3, mirrorLocalId as a4, detectCLIAvailability as a5, detectResumeSupport as a6, acquireDaemonLock as a7, summarizeSpawnSessionForLog as a8, deletePersistedSessions as a9, shellescape as aa, decrypt as ab, writeDaemonState as ac, writeSettings as ad, fetchCliUpdateState as ae, resolveCliUpdateCheckInterval as af, DEFAULT_CLI_UPDATE_CHECK_INTERVAL_MS as ag, releaseDaemonLock as ah, listVhTerminals as ai, isValidTerminalId as aj, readVhTerminal as ak, sendToVhTerminal as al, TITLE_PROMPT_PREFIX as am, BOARD_ANALYZER_PROMPT_PREFIX as an, startOfflineReconnection as ao, clearCredentials as ap, clearMachineId as aq, SandboxConfigSchema as ar, CodexAppServerClient as as, getLatestDaemonLog as at, persistence as au, api as av, createEnvelope as b, configuration as c, connectionState as d, readSettings as e, encodeBase64 as f, contentLogMetadata as g, errorLogMetadata as h, isValidSessionId as i, packageJson as j, delay as k, logger as l, ensurePrivateDirectorySync as m, hardenPrivateFileSync as n, readDaemonState as o, projectPath as p, clearDaemonState as q, readPersistedSessions as r, streamKeyOf as s, getProjectPath as t, initializeSandbox as u, wrapCommand as v, writePrivateFileSync as w, AsyncLock as x, ensurePrivateDirectory as y, hardenPrivateFile as z };