very-happy-cli 0.2.110 → 0.2.112

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-D0_wbTs6.cjs → AcpBackend-B3x-gQWP.cjs} +1 -1
  2. package/dist/{AcpBackend-DzZR7TEv.mjs → AcpBackend-DDEdYVW2.mjs} +1 -1
  3. package/dist/{AcpSessionManager-tWuFWeIF.mjs → AcpSessionManager-BORAhQ1r.mjs} +1 -1
  4. package/dist/{AcpSessionManager-Dcj_iHw8.cjs → AcpSessionManager-DGPLD-go.cjs} +1 -1
  5. package/dist/{config-CTyyVXHj.mjs → config-B_L5vXXu.mjs} +2 -2
  6. package/dist/{config-CTCGilz_.cjs → config-KX6e7GEu.cjs} +2 -2
  7. package/dist/{index-D-adqB7g.cjs → index-B7-t5Kwz.cjs} +4 -4
  8. package/dist/{index-CvnMiYgh.cjs → index-BnRQP3PL.cjs} +250 -20
  9. package/dist/{index-DW78HyvK.cjs → index-CTd0skWH.cjs} +2 -2
  10. package/dist/{index-Brhvj7p0.mjs → index-Cu5FdgTq.mjs} +5 -5
  11. package/dist/{index-BW9kDx5m.mjs → index-inkhalP5.mjs} +245 -17
  12. package/dist/{index-jYltd-ZW.mjs → index-lQzCAx9I.mjs} +2 -2
  13. package/dist/index.cjs +2 -2
  14. package/dist/index.mjs +2 -2
  15. package/dist/{installTerminalHooks-DhQ6k7YD.cjs → installTerminalHooks-BVk-r0yD.cjs} +2 -2
  16. package/dist/{installTerminalHooks-CJyoR9jp.mjs → installTerminalHooks-CQiW7Jn9.mjs} +2 -2
  17. package/dist/lib.cjs +1 -1
  18. package/dist/lib.d.cts +44 -0
  19. package/dist/lib.d.mts +44 -0
  20. package/dist/lib.mjs +1 -1
  21. package/dist/{mcp-Dr1PegOw.cjs → mcp-D0DIU8Q2.cjs} +2 -2
  22. package/dist/{mcp-BO9-sxMr.mjs → mcp-DD-0VkiA.mjs} +2 -2
  23. package/dist/{runGemini-CuzqF6Ys.mjs → runGemini-DHyBDo5P.mjs} +4 -4
  24. package/dist/{runGemini-D6Wwfk_0.cjs → runGemini-Dq1d8Ccb.cjs} +4 -4
  25. package/dist/{runOpenClaw-BYmlS-4Q.cjs → runOpenClaw-BHXnUF_m.cjs} +3 -3
  26. package/dist/{runOpenClaw-B5pkmSTb.mjs → runOpenClaw-DX-tgQ2V.mjs} +3 -3
  27. package/dist/{send-Btz1IZ9U.mjs → send-0TYDXiGx.mjs} +2 -2
  28. package/dist/{send-CuajooZC.cjs → send-By6zEUWo.cjs} +2 -2
  29. package/dist/{sessions-CVUBS3LG.cjs → sessions-DTBRcBRk.cjs} +2 -2
  30. package/dist/{sessions-DDwCphTQ.mjs → sessions-TM8VaCMY.mjs} +2 -2
  31. package/dist/{spawn-CwS8CPbr.cjs → spawn-CweTEYze.cjs} +57 -4
  32. package/dist/{spawn-DcA80_VN.mjs → spawn-D0oTo5BT.mjs} +56 -5
  33. package/dist/{types-BdO0jg7G.mjs → types-C5ewhu1Q.mjs} +96 -11
  34. package/dist/{types--t4zLibT.cjs → types-R11tMGyX.cjs} +97 -11
  35. package/package.json +7 -7
@@ -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--t4zLibT.cjs');
8
- var index = require('./index-CvnMiYgh.cjs');
9
- var AcpBackend = require('./AcpBackend-D0_wbTs6.cjs');
10
- var config = require('./config-CTCGilz_.cjs');
7
+ var persistence = require('./types-R11tMGyX.cjs');
8
+ var index = require('./index-BnRQP3PL.cjs');
9
+ var AcpBackend = require('./AcpBackend-B3x-gQWP.cjs');
10
+ var config = require('./config-KX6e7GEu.cjs');
11
11
  require('axios');
12
12
  require('chalk');
13
13
  require('node:util');
@@ -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--t4zLibT.cjs');
9
- var AcpSessionManager = require('./AcpSessionManager-Dcj_iHw8.cjs');
10
- var index = require('./index-CvnMiYgh.cjs');
8
+ var persistence = require('./types-R11tMGyX.cjs');
9
+ var AcpSessionManager = require('./AcpSessionManager-DGPLD-go.cjs');
10
+ var index = require('./index-BnRQP3PL.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, e as errorLogMetadata, d as contentLogMetadata, h as connectionState, A as ApiClient, j as readSettings, k as encodeBase64, l as logger, c as configuration } from './types-BdO0jg7G.mjs';
7
- import { A as AcpSessionManager } from './AcpSessionManager-tWuFWeIF.mjs';
8
- import { g as initialMachineMetadata, h as createSessionMetadata, j as setupOfflineReconnection, n as notifyDaemonSessionStarted, k as MessageQueue2, u as registerKillSessionHandler } from './index-BW9kDx5m.mjs';
6
+ import { m as ensurePrivateDirectorySync, n as hardenPrivateFileSync, w as writePrivateFileSync, e as errorLogMetadata, d as contentLogMetadata, h as connectionState, A as ApiClient, j as readSettings, k as encodeBase64, l as logger, c as configuration } from './types-C5ewhu1Q.mjs';
7
+ import { A as AcpSessionManager } from './AcpSessionManager-BORAhQ1r.mjs';
8
+ import { h as initialMachineMetadata, j as createSessionMetadata, k as setupOfflineReconnection, n as notifyDaemonSessionStarted, m as MessageQueue2, v as registerKillSessionHandler } from './index-inkhalP5.mjs';
9
9
  import WebSocket from 'ws';
10
10
  import * as ed from '@noble/ed25519';
11
11
  import { sha512 } from '@noble/hashes/sha2.js';
@@ -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-BdO0jg7G.mjs';
5
- import { b as sendUserMessage, a as sessionWebUrl } from './index-BW9kDx5m.mjs';
4
+ import { r as readPersistedSessions, c as configuration } from './types-C5ewhu1Q.mjs';
5
+ import { d as sendUserMessage, a as sessionWebUrl } from './index-inkhalP5.mjs';
6
6
  import 'axios';
7
7
  import 'node:util';
8
8
  import 'node:os';
@@ -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--t4zLibT.cjs');
7
- var index = require('./index-CvnMiYgh.cjs');
6
+ var persistence = require('./types-R11tMGyX.cjs');
7
+ var index = require('./index-BnRQP3PL.cjs');
8
8
  require('axios');
9
9
  require('node:util');
10
10
  require('node:os');
@@ -1,8 +1,8 @@
1
1
  'use strict';
2
2
 
3
3
  var chalk = require('chalk');
4
- var index = require('./index-CvnMiYgh.cjs');
5
- var persistence = require('./types--t4zLibT.cjs');
4
+ var index = require('./index-BnRQP3PL.cjs');
5
+ var persistence = require('./types-R11tMGyX.cjs');
6
6
  require('node:os');
7
7
  require('node:crypto');
8
8
  require('cross-spawn');
@@ -1,6 +1,6 @@
1
1
  import chalk from 'chalk';
2
- import { D as DEFAULT_RECENT_LIMIT, M as MAX_READ_LIMIT, l as listSessions, r as readSessionTranscript, d as stopSession, e as archiveSession } from './index-BW9kDx5m.mjs';
3
- import { i as isValidSessionId } from './types-BdO0jg7G.mjs';
2
+ import { D as DEFAULT_RECENT_LIMIT, M as MAX_READ_LIMIT, l as listSessions, r as readSessionTranscript, e as stopSession, f as archiveSession } from './index-inkhalP5.mjs';
3
+ import { i as isValidSessionId } from './types-C5ewhu1Q.mjs';
4
4
  import 'node:os';
5
5
  import 'node:crypto';
6
6
  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-CvnMiYgh.cjs');
7
- var persistence = require('./types--t4zLibT.cjs');
6
+ var index = require('./index-BnRQP3PL.cjs');
7
+ var persistence = require('./types-R11tMGyX.cjs');
8
8
  require('node:os');
9
9
  require('node:crypto');
10
10
  require('cross-spawn');
@@ -48,6 +48,14 @@ require('node:util');
48
48
  require('url');
49
49
  require('node-pty');
50
50
 
51
+ const SPAWN_AGENTS = ["claude", "codex", "gemini", "openclaw"];
52
+ function parseEnvAssignment(raw) {
53
+ const eq = raw.indexOf("=");
54
+ if (eq <= 0) throw new Error(`--env expects KEY=VALUE, got: ${raw}`);
55
+ const key = raw.slice(0, eq);
56
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) throw new Error(`--env key is not a valid variable name: ${key}`);
57
+ return [key, raw.slice(eq + 1)];
58
+ }
51
59
  function parseSpawnArgs(args) {
52
60
  const options = { json: false, help: false };
53
61
  for (let i = 0; i < args.length; i++) {
@@ -71,6 +79,25 @@ function parseSpawnArgs(args) {
71
79
  throw new Error("--spawned-by must be 1-24 chars of [a-z0-9] plus - or _, starting with a letter or digit");
72
80
  }
73
81
  options.spawnedBy = value;
82
+ } else if (arg === "--permission-mode") {
83
+ const value = args[++i];
84
+ if (value === void 0) throw new Error("--permission-mode requires a value");
85
+ if (index.sanitizeSpawnPermissionMode(value) === null) {
86
+ throw new Error(`--permission-mode must be one of: ${index.ALLOWED_SPAWN_PERMISSION_MODES.join(", ")}`);
87
+ }
88
+ options.permissionMode = value;
89
+ } else if (arg === "--agent") {
90
+ const value = args[++i];
91
+ if (value === void 0) throw new Error("--agent requires a value");
92
+ if (!SPAWN_AGENTS.includes(value)) {
93
+ throw new Error(`--agent must be one of: ${SPAWN_AGENTS.join(", ")}`);
94
+ }
95
+ options.agent = value;
96
+ } else if (arg === "--env") {
97
+ const value = args[++i];
98
+ if (value === void 0) throw new Error("--env requires a value");
99
+ const [key, val] = parseEnvAssignment(value);
100
+ options.env = { ...options.env ?? {}, [key]: val };
74
101
  } else if (arg === "--json") {
75
102
  options.json = true;
76
103
  } else if (arg === "--help" || arg === "-h") {
@@ -89,7 +116,9 @@ function printHelp() {
89
116
  ${chalk.bold("very-happy spawn")} - Spawn a remote session via the local daemon (for automation)
90
117
 
91
118
  ${chalk.bold("Usage:")}
92
- very-happy spawn --dir <path> [--prompt <text> | --prompt-file <file>] [--json]
119
+ very-happy spawn --dir <path> [--prompt <text> | --prompt-file <file>]
120
+ [--spawned-by <name>] [--permission-mode <mode>]
121
+ [--agent <name>] [--env KEY=VALUE]... [--json]
93
122
 
94
123
  ${chalk.bold("Options:")}
95
124
  --dir, -d <path> Working directory for the new session (required)
@@ -98,6 +127,17 @@ ${chalk.bold("Options:")}
98
127
  --spawned-by <name> Tag the new session with its origin (e.g. tanka).
99
128
  1-24 chars: [a-z0-9] plus - or _. Shows as a chip in
100
129
  the web list and is searchable as #<name>.
130
+ --permission-mode <m> Permission mode for the new session: default,
131
+ acceptEdits, plan, yolo, bypassPermissions.
132
+ WITHOUT this the session runs in 'default' and stops
133
+ at the first un-allowlisted tool waiting for someone
134
+ to approve \u2014 if nothing is watching, it just hangs.
135
+ Unattended dispatchers want bypassPermissions.
136
+ --agent <name> Backend to run: claude (default), codex, gemini,
137
+ openclaw.
138
+ --env KEY=VALUE Extra environment for the session process. Repeatable.
139
+ \${VAR} is expanded against the daemon's environment;
140
+ an unresolved reference fails the spawn.
101
141
  --json Machine-readable output: {"sessionId", "url"}
102
142
  -h, --help Show this help
103
143
 
@@ -163,7 +203,12 @@ async function handleSpawnCommand(args) {
163
203
  process.exit(1);
164
204
  }
165
205
  persistence.logger.debug(`[SPAWN CMD] Spawning session in ${directory}`);
166
- const result = await index.spawnDaemonSession(directory, void 0, { spawnedBy: options.spawnedBy });
206
+ const result = await index.spawnDaemonSession(directory, void 0, {
207
+ spawnedBy: options.spawnedBy,
208
+ permissionMode: options.permissionMode,
209
+ agent: options.agent,
210
+ environmentVariables: options.env
211
+ });
167
212
  if (result?.error || !result?.success || !result?.sessionId) {
168
213
  const message = result?.error || "Daemon returned no session ID";
169
214
  console.error(chalk.red("Error:"), `Failed to spawn session: ${message}`);
@@ -184,6 +229,12 @@ async function handleSpawnCommand(args) {
184
229
  if (options.spawnedBy !== void 0) {
185
230
  payload.spawnedBy = options.spawnedBy;
186
231
  }
232
+ if (options.permissionMode !== void 0) {
233
+ payload.permissionMode = options.permissionMode;
234
+ }
235
+ if (options.agent !== void 0) {
236
+ payload.agent = options.agent;
237
+ }
187
238
  if (prompt !== void 0) {
188
239
  payload.promptDelivered = promptError === null;
189
240
  }
@@ -207,5 +258,7 @@ async function handleSpawnCommand(args) {
207
258
  }
208
259
 
209
260
  exports.sessionWebUrl = index.sessionWebUrl;
261
+ exports.SPAWN_AGENTS = SPAWN_AGENTS;
210
262
  exports.handleSpawnCommand = handleSpawnCommand;
263
+ exports.parseEnvAssignment = parseEnvAssignment;
211
264
  exports.parseSpawnArgs = parseSpawnArgs;
@@ -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, w as waitForSessionKey, b as sendUserMessage } from './index-BW9kDx5m.mjs';
5
- import { l as logger } from './types-BdO0jg7G.mjs';
4
+ import { c as checkIfDaemonRunningAndCleanupStaleState, s as spawnDaemonSession, a as sessionWebUrl, i as isValidSpawnOrigin, b as sanitizeSpawnPermissionMode, A as ALLOWED_SPAWN_PERMISSION_MODES, w as waitForSessionKey, d as sendUserMessage } from './index-inkhalP5.mjs';
5
+ import { l as logger } from './types-C5ewhu1Q.mjs';
6
6
  import 'node:os';
7
7
  import 'node:crypto';
8
8
  import 'cross-spawn';
@@ -46,6 +46,14 @@ import 'node:util';
46
46
  import 'url';
47
47
  import 'node-pty';
48
48
 
49
+ const SPAWN_AGENTS = ["claude", "codex", "gemini", "openclaw"];
50
+ function parseEnvAssignment(raw) {
51
+ const eq = raw.indexOf("=");
52
+ if (eq <= 0) throw new Error(`--env expects KEY=VALUE, got: ${raw}`);
53
+ const key = raw.slice(0, eq);
54
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) throw new Error(`--env key is not a valid variable name: ${key}`);
55
+ return [key, raw.slice(eq + 1)];
56
+ }
49
57
  function parseSpawnArgs(args) {
50
58
  const options = { json: false, help: false };
51
59
  for (let i = 0; i < args.length; i++) {
@@ -69,6 +77,25 @@ function parseSpawnArgs(args) {
69
77
  throw new Error("--spawned-by must be 1-24 chars of [a-z0-9] plus - or _, starting with a letter or digit");
70
78
  }
71
79
  options.spawnedBy = value;
80
+ } else if (arg === "--permission-mode") {
81
+ const value = args[++i];
82
+ if (value === void 0) throw new Error("--permission-mode requires a value");
83
+ if (sanitizeSpawnPermissionMode(value) === null) {
84
+ throw new Error(`--permission-mode must be one of: ${ALLOWED_SPAWN_PERMISSION_MODES.join(", ")}`);
85
+ }
86
+ options.permissionMode = value;
87
+ } else if (arg === "--agent") {
88
+ const value = args[++i];
89
+ if (value === void 0) throw new Error("--agent requires a value");
90
+ if (!SPAWN_AGENTS.includes(value)) {
91
+ throw new Error(`--agent must be one of: ${SPAWN_AGENTS.join(", ")}`);
92
+ }
93
+ options.agent = value;
94
+ } else if (arg === "--env") {
95
+ const value = args[++i];
96
+ if (value === void 0) throw new Error("--env requires a value");
97
+ const [key, val] = parseEnvAssignment(value);
98
+ options.env = { ...options.env ?? {}, [key]: val };
72
99
  } else if (arg === "--json") {
73
100
  options.json = true;
74
101
  } else if (arg === "--help" || arg === "-h") {
@@ -87,7 +114,9 @@ function printHelp() {
87
114
  ${chalk.bold("very-happy spawn")} - Spawn a remote session via the local daemon (for automation)
88
115
 
89
116
  ${chalk.bold("Usage:")}
90
- very-happy spawn --dir <path> [--prompt <text> | --prompt-file <file>] [--json]
117
+ very-happy spawn --dir <path> [--prompt <text> | --prompt-file <file>]
118
+ [--spawned-by <name>] [--permission-mode <mode>]
119
+ [--agent <name>] [--env KEY=VALUE]... [--json]
91
120
 
92
121
  ${chalk.bold("Options:")}
93
122
  --dir, -d <path> Working directory for the new session (required)
@@ -96,6 +125,17 @@ ${chalk.bold("Options:")}
96
125
  --spawned-by <name> Tag the new session with its origin (e.g. tanka).
97
126
  1-24 chars: [a-z0-9] plus - or _. Shows as a chip in
98
127
  the web list and is searchable as #<name>.
128
+ --permission-mode <m> Permission mode for the new session: default,
129
+ acceptEdits, plan, yolo, bypassPermissions.
130
+ WITHOUT this the session runs in 'default' and stops
131
+ at the first un-allowlisted tool waiting for someone
132
+ to approve \u2014 if nothing is watching, it just hangs.
133
+ Unattended dispatchers want bypassPermissions.
134
+ --agent <name> Backend to run: claude (default), codex, gemini,
135
+ openclaw.
136
+ --env KEY=VALUE Extra environment for the session process. Repeatable.
137
+ \${VAR} is expanded against the daemon's environment;
138
+ an unresolved reference fails the spawn.
99
139
  --json Machine-readable output: {"sessionId", "url"}
100
140
  -h, --help Show this help
101
141
 
@@ -161,7 +201,12 @@ async function handleSpawnCommand(args) {
161
201
  process.exit(1);
162
202
  }
163
203
  logger.debug(`[SPAWN CMD] Spawning session in ${directory}`);
164
- const result = await spawnDaemonSession(directory, void 0, { spawnedBy: options.spawnedBy });
204
+ const result = await spawnDaemonSession(directory, void 0, {
205
+ spawnedBy: options.spawnedBy,
206
+ permissionMode: options.permissionMode,
207
+ agent: options.agent,
208
+ environmentVariables: options.env
209
+ });
165
210
  if (result?.error || !result?.success || !result?.sessionId) {
166
211
  const message = result?.error || "Daemon returned no session ID";
167
212
  console.error(chalk.red("Error:"), `Failed to spawn session: ${message}`);
@@ -182,6 +227,12 @@ async function handleSpawnCommand(args) {
182
227
  if (options.spawnedBy !== void 0) {
183
228
  payload.spawnedBy = options.spawnedBy;
184
229
  }
230
+ if (options.permissionMode !== void 0) {
231
+ payload.permissionMode = options.permissionMode;
232
+ }
233
+ if (options.agent !== void 0) {
234
+ payload.agent = options.agent;
235
+ }
185
236
  if (prompt !== void 0) {
186
237
  payload.promptDelivered = promptError === null;
187
238
  }
@@ -204,4 +255,4 @@ async function handleSpawnCommand(args) {
204
255
  process.exit(0);
205
256
  }
206
257
 
207
- export { handleSpawnCommand, parseSpawnArgs, sessionWebUrl };
258
+ export { SPAWN_AGENTS, handleSpawnCommand, parseEnvAssignment, parseSpawnArgs, sessionWebUrl };
@@ -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.110";
32
+ var version = "0.2.112";
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.110",
146
- "very-happy-tools-x64-darwin": "0.2.110",
147
- "very-happy-tools-arm64-linux": "0.2.110",
148
- "very-happy-tools-x64-linux": "0.2.110",
149
- "very-happy-tools-arm64-win32": "0.2.110",
150
- "very-happy-tools-x64-win32": "0.2.110"
145
+ "very-happy-tools-arm64-darwin": "0.2.112",
146
+ "very-happy-tools-x64-darwin": "0.2.112",
147
+ "very-happy-tools-arm64-linux": "0.2.112",
148
+ "very-happy-tools-x64-linux": "0.2.112",
149
+ "very-happy-tools-arm64-win32": "0.2.112",
150
+ "very-happy-tools-x64-win32": "0.2.112"
151
151
  };
152
152
  var devDependencies = {
153
153
  "@slopus/happy-wire": "workspace:*",
@@ -747,6 +747,14 @@ const sessionEnvelopeSchema = z.object({
747
747
  // Codex app-server item id for this envelope. Used as the precise
748
748
  // rollback point for Codex thread duplicate/fork-from-message.
749
749
  codexItemId: z.string().min(1).optional(),
750
+ // B-309: identity of the live draft this envelope supersedes,
751
+ // `"<api message id>:<content block index>"`. Set on text/thinking
752
+ // envelopes mapped from a Claude assistant message so the web can replace
753
+ // the streamed draft with the persisted message in a single frame instead
754
+ // of letting it flicker out and back in. Absent from every other producer
755
+ // (and from CLIs older than the streaming relay) — consumers must treat a
756
+ // missing key as "no draft to claim", never as an error.
757
+ streamKey: z.string().min(1).optional(),
750
758
  // Per-API-call token usage of the assistant message this envelope was
751
759
  // mapped from (B-108). Envelope-level (not inside `ev`) so every envelope
752
760
  // type carries it uniformly — the web feeds it into its context meter.
@@ -785,6 +793,7 @@ function createEnvelope(role, ev, opts = {}) {
785
793
  ...opts.subagent ? { subagent: opts.subagent } : {},
786
794
  ...opts.claudeUuid ? { claudeUuid: opts.claudeUuid } : {},
787
795
  ...opts.codexItemId ? { codexItemId: opts.codexItemId } : {},
796
+ ...opts.streamKey ? { streamKey: opts.streamKey } : {},
788
797
  ...opts.usage ? { usage: opts.usage } : {},
789
798
  ev
790
799
  });
@@ -887,6 +896,44 @@ z.object({
887
896
  body: CoreUpdateBodySchema,
888
897
  createdAt: z.number()
889
898
  });
899
+ const STREAM_DELTA_MAX_CHARS = 16 * 1024;
900
+ const sessionStreamBlockKindSchema = z.enum(["text", "thinking"]);
901
+ const sessionStreamBlockStartSchema = z.object({
902
+ t: z.literal("block-start"),
903
+ mid: z.string().min(1),
904
+ idx: z.number().int().min(0),
905
+ kind: sessionStreamBlockKindSchema
906
+ });
907
+ const sessionStreamBlockDeltaSchema = z.object({
908
+ t: z.literal("block-delta"),
909
+ mid: z.string().min(1),
910
+ idx: z.number().int().min(0),
911
+ text: z.string().max(STREAM_DELTA_MAX_CHARS)
912
+ });
913
+ const sessionStreamBlockEndSchema = z.object({
914
+ t: z.literal("block-end"),
915
+ mid: z.string().min(1),
916
+ idx: z.number().int().min(0)
917
+ });
918
+ const sessionStreamProgressSchema = z.object({
919
+ t: z.literal("progress"),
920
+ thinkingTokens: z.number().int().min(0).optional(),
921
+ outputTokens: z.number().int().min(0).optional(),
922
+ status: z.enum(["requesting", "compacting"]).optional()
923
+ });
924
+ const sessionStreamTurnEndSchema = z.object({
925
+ t: z.literal("turn-end")
926
+ });
927
+ z.discriminatedUnion("t", [
928
+ sessionStreamBlockStartSchema,
929
+ sessionStreamBlockDeltaSchema,
930
+ sessionStreamBlockEndSchema,
931
+ sessionStreamProgressSchema,
932
+ sessionStreamTurnEndSchema
933
+ ]);
934
+ function streamKeyOf(messageId, blockIndex) {
935
+ return `${messageId}:${blockIndex}`;
936
+ }
890
937
  const VoiceConversationGrantedSchema = z.object({
891
938
  allowed: z.literal(true),
892
939
  conversationToken: z.string(),
@@ -2769,13 +2816,24 @@ function mapClaudeLogMessageToSessionEnvelopesInternal(message, state) {
2769
2816
  maybeEmitSubagentStart(state, turnId, subagent, envelopes);
2770
2817
  const usage = pickAssistantUsage(message);
2771
2818
  const blocks = Array.isArray(message.message?.content) ? message.message.content : [];
2772
- for (const block of blocks) {
2819
+ const apiMessageId = !isSidechainMessage(message) && typeof message.message?.id === "string" && message.message.id.length > 0 ? message.message.id : void 0;
2820
+ let blockCursor = 0;
2821
+ if (apiMessageId) {
2822
+ if (state.streamBlockMessageId !== apiMessageId) {
2823
+ state.streamBlockMessageId = apiMessageId;
2824
+ state.streamBlockCursor = 0;
2825
+ }
2826
+ blockCursor = state.streamBlockCursor ?? 0;
2827
+ state.streamBlockCursor = blockCursor + blocks.length;
2828
+ }
2829
+ const draftKey = (index) => apiMessageId ? streamKeyOf(apiMessageId, blockCursor + index) : void 0;
2830
+ for (const [blockIndex, block] of blocks.entries()) {
2773
2831
  if (block.type === "text" && typeof block.text === "string" && block.text.trim().length > 0) {
2774
- envelopes.push(createEnvelope("agent", { t: "text", text: block.text }, { turn: turnId, subagent, claudeUuid, usage }));
2832
+ envelopes.push(createEnvelope("agent", { t: "text", text: block.text }, { turn: turnId, subagent, claudeUuid, usage, streamKey: draftKey(blockIndex) }));
2775
2833
  continue;
2776
2834
  }
2777
2835
  if (block.type === "thinking" && typeof block.thinking === "string" && block.thinking.trim().length > 0) {
2778
- envelopes.push(createEnvelope("agent", { t: "text", text: block.thinking, thinking: true }, { turn: turnId, subagent, claudeUuid, usage }));
2836
+ envelopes.push(createEnvelope("agent", { t: "text", text: block.thinking, thinking: true }, { turn: turnId, subagent, claudeUuid, usage, streamKey: draftKey(blockIndex) }));
2779
2837
  continue;
2780
2838
  }
2781
2839
  if (block.type === "tool_use") {
@@ -3774,6 +3832,33 @@ class ApiSessionClient extends EventEmitter {
3774
3832
  mode
3775
3833
  });
3776
3834
  }
3835
+ /**
3836
+ * B-309: push one live stream frame (thinking/text delta or progress) to
3837
+ * this account's web clients.
3838
+ *
3839
+ * Encrypted with the SESSION key and relayed without being stored —
3840
+ * clipboard-push's shape, for clipboard-push's reasons: the relay must not
3841
+ * be able to read thinking text, and the session id is stamped by the
3842
+ * server from the authenticated connection rather than trusted from here.
3843
+ *
3844
+ * `volatile` on purpose. A draft that misses the wire while disconnected
3845
+ * is worth nothing — the persisted message is still coming, and replaying
3846
+ * stale deltas after a reconnect would paint text that has already landed.
3847
+ */
3848
+ sendStreamFrame(frame) {
3849
+ if (!this.socket.connected) return;
3850
+ try {
3851
+ this.socket.volatile.emit("session-stream", {
3852
+ // The frame object, NOT a JSON string: `encrypt` serializes
3853
+ // internally, so pre-stringifying would double-encode and
3854
+ // escape every quote in the streamed text.
3855
+ payload: encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, frame)),
3856
+ enc: true
3857
+ });
3858
+ } catch (error) {
3859
+ logger.debug(`[API] Failed to relay stream frame: ${error}`);
3860
+ }
3861
+ }
3777
3862
  /**
3778
3863
  * Send session death message
3779
3864
  */
@@ -16302,4 +16387,4 @@ const RawJSONLinesSchema = z$1.discriminatedUnion("type", [
16302
16387
  }).passthrough()
16303
16388
  ]);
16304
16389
 
16305
- export { mirrorLocalId as $, ApiClient as A, credentialRelayProblem as B, deriveLocalCliUpdateSummary as C, updateSettings as D, encodeBase64Url as E, decodeBase64 as F, writeCredentialsLegacy as G, writeCredentialsDataKey as H, InvalidateSync as I, readCredentialsForConfiguredRelay as J, encrypt as K, isClaudeSessionId as L, MAX_CHAT_ATTACHMENT_SOURCE_BYTES as M, getLocalHappyAgentCredentialPath as N, readLocalHappyAgentCredentials as O, decryptWithDataKey as P, decryptLegacy as Q, RawJSONLinesSchema as R, extractCompleteLines as S, decideBackfill as T, parseTerminalHookPayload as U, tmuxArgs as V, scrubTmuxClientEnv as W, decideMirrorBinding as X, MIRROR_BACKFILL_LINES_DEFAULT as Y, persistSession as Z, mirrorLineKey as _, ApiSessionClient as a, detectResumeSupport as a0, detectCLIAvailability as a1, acquireDaemonLock as a2, summarizeSpawnSessionForLog as a3, deletePersistedSessions as a4, shellescape as a5, decrypt as a6, writeDaemonState as a7, writeSettings as a8, fetchCliUpdateState as a9, resolveCliUpdateCheckInterval as aa, DEFAULT_CLI_UPDATE_CHECK_INTERVAL_MS as ab, releaseDaemonLock as ac, listVhTerminals as ad, isValidTerminalId as ae, readVhTerminal as af, sendToVhTerminal as ag, TITLE_PROMPT_PREFIX as ah, BOARD_ANALYZER_PROMPT_PREFIX as ai, startOfflineReconnection as aj, clearCredentials as ak, clearMachineId as al, SandboxConfigSchema as am, CodexAppServerClient as an, getLatestDaemonLog as ao, persistence as ap, api as aq, createEnvelope as b, configuration as c, contentLogMetadata as d, errorLogMetadata as e, packageJson as f, delay as g, connectionState as h, isValidSessionId as i, readSettings as j, encodeBase64 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, readCredentials as z };
16390
+ export { mirrorLineKey as $, ApiClient as A, credentialRelayProblem as B, deriveLocalCliUpdateSummary as C, updateSettings as D, encodeBase64Url as E, decodeBase64 as F, writeCredentialsLegacy as G, writeCredentialsDataKey as H, InvalidateSync as I, readCredentialsForConfiguredRelay as J, encrypt as K, isClaudeSessionId as L, MAX_CHAT_ATTACHMENT_SOURCE_BYTES as M, getLocalHappyAgentCredentialPath as N, readLocalHappyAgentCredentials as O, decryptWithDataKey as P, decryptLegacy as Q, RawJSONLinesSchema as R, STREAM_DELTA_MAX_CHARS as S, extractCompleteLines as T, decideBackfill as U, parseTerminalHookPayload as V, tmuxArgs as W, scrubTmuxClientEnv as X, decideMirrorBinding as Y, MIRROR_BACKFILL_LINES_DEFAULT as Z, persistSession as _, ApiSessionClient as a, mirrorLocalId as a0, detectResumeSupport as a1, detectCLIAvailability as a2, acquireDaemonLock as a3, summarizeSpawnSessionForLog as a4, deletePersistedSessions as a5, shellescape as a6, decrypt as a7, writeDaemonState as a8, writeSettings as a9, fetchCliUpdateState as aa, resolveCliUpdateCheckInterval as ab, DEFAULT_CLI_UPDATE_CHECK_INTERVAL_MS as ac, releaseDaemonLock as ad, listVhTerminals as ae, isValidTerminalId as af, readVhTerminal as ag, sendToVhTerminal as ah, TITLE_PROMPT_PREFIX as ai, BOARD_ANALYZER_PROMPT_PREFIX as aj, startOfflineReconnection as ak, clearCredentials as al, clearMachineId as am, SandboxConfigSchema as an, CodexAppServerClient as ao, getLatestDaemonLog as ap, persistence as aq, api as ar, createEnvelope as b, configuration as c, contentLogMetadata as d, errorLogMetadata as e, packageJson as f, delay as g, connectionState as h, isValidSessionId as i, readSettings as j, encodeBase64 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, readCredentials as z };