very-happy-cli 0.2.157 → 0.2.159

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 (55) hide show
  1. package/dist/{AcpBackend-BymWX-LI.cjs → AcpBackend-DUYFlyfN.cjs} +2 -2
  2. package/dist/{AcpBackend-Cy18U7yw.mjs → AcpBackend-VsfkZiVF.mjs} +2 -2
  3. package/dist/{AcpSessionManager-BF7zBGAi.cjs → AcpSessionManager-Bdv8LWXh.cjs} +1 -1
  4. package/dist/{AcpSessionManager-Ds8rcp95.mjs → AcpSessionManager-rYtVb0kN.mjs} +1 -1
  5. package/dist/{agentGuidance-Db76O1kw.cjs → agentGuidance-BRDN1NoZ.cjs} +3 -2
  6. package/dist/{agentGuidance-CU6YD_Ms.mjs → agentGuidance-KgvYuF3R.mjs} +3 -2
  7. package/dist/{agentHome-B0CLy0Qx.cjs → agentHome-CHh4lmjM.cjs} +2 -2
  8. package/dist/{agentHome-BaJ9xtxB.mjs → agentHome-CvsJyeQB.mjs} +2 -2
  9. package/dist/{auto-DqKGFq8U.cjs → auto-DKmEls8Z.cjs} +19 -10
  10. package/dist/{auto-B1YAwaeU.mjs → auto-nb8GhRRy.mjs} +19 -10
  11. package/dist/codex/happyMcpStdioBridge.cjs +2 -2
  12. package/dist/codex/happyMcpStdioBridge.mjs +2 -2
  13. package/dist/{config-aAbhJllb.mjs → config-ClLtM6F3.mjs} +4 -4
  14. package/dist/{config-CzkBjZnV.cjs → config-DLx-vW3I.cjs} +4 -4
  15. package/dist/{index-BVu8Cufk.mjs → index-2mCaeKyf.mjs} +19 -9
  16. package/dist/{index-CTbGqVFY.cjs → index-BEOMECtE.cjs} +18 -8
  17. package/dist/{index-BYU1e81k.mjs → index-BF6WfgV5.mjs} +63 -29
  18. package/dist/{index-DQcRkI9r.mjs → index-C-JazrMB.mjs} +4 -4
  19. package/dist/{index-Cz-uqnJk.cjs → index-blDSadUR.cjs} +4 -4
  20. package/dist/{index-C9QzZsUQ.cjs → index-rH3DrQ5B.cjs} +67 -33
  21. package/dist/index.cjs +4 -4
  22. package/dist/index.mjs +4 -4
  23. package/dist/{install-BMlV0Tnr.cjs → install-BOHrUaFM.cjs} +1 -1
  24. package/dist/{install-BuYnqtVa.mjs → install-BPhJppvd.mjs} +1 -1
  25. package/dist/{installTerminalHooks-BVVOEGhG.cjs → installTerminalHooks-Cou3WUf3.cjs} +4 -4
  26. package/dist/{installTerminalHooks-ELDoqh0A.mjs → installTerminalHooks-WeznjkuC.mjs} +4 -4
  27. package/dist/lib.cjs +2 -2
  28. package/dist/lib.d.cts +114 -0
  29. package/dist/lib.d.mts +114 -0
  30. package/dist/lib.mjs +2 -2
  31. package/dist/{limits-CyuIt-Vl.cjs → limits-BH5CV5vR.cjs} +39 -7
  32. package/dist/{limits-A2c-3M-_.mjs → limits-CjRj8IX5.mjs} +39 -8
  33. package/dist/{mcp-CXkvnztT.cjs → mcp-CPYbtsYG.cjs} +4 -4
  34. package/dist/{mcp-Bo934Kpy.mjs → mcp-Dy3f_K0h.mjs} +4 -4
  35. package/dist/{piRuntime-CJjA26oD.cjs → piRuntime-NtF0YqjH.cjs} +2 -2
  36. package/dist/{piRuntime-DdEY2mE9.mjs → piRuntime-kLZNpAAl.mjs} +2 -2
  37. package/dist/{piTerminal-CpU3hzrX.cjs → piTerminal-9J0GAKzb.cjs} +6 -6
  38. package/dist/{piTerminal-DwWmgumi.mjs → piTerminal-B0fEOV_2.mjs} +6 -6
  39. package/dist/{runGemini-Cj6oJQ-I.mjs → runGemini-DV6Ouadt.mjs} +6 -6
  40. package/dist/{runGemini-DS-0qm0L.cjs → runGemini-F-mqx3_K.cjs} +6 -6
  41. package/dist/{runOpenClaw-DUCQxkI-.mjs → runOpenClaw-D7MlPS5n.mjs} +5 -5
  42. package/dist/{runOpenClaw-CvSm8oTc.cjs → runOpenClaw-Qqo9j63M.cjs} +5 -5
  43. package/dist/{send-CHxobaoI.cjs → send-4qJTCAYW.cjs} +2 -2
  44. package/dist/{send-D5CyRyZ_.mjs → send-CLhYXNgy.mjs} +2 -2
  45. package/dist/{sessions-BFlIrKCY.cjs → sessions-Bh1pyddi.cjs} +2 -2
  46. package/dist/{sessions-DsE01xKc.mjs → sessions-CzIH2rGp.mjs} +2 -2
  47. package/dist/{spawn-CdihhwHs.mjs → spawn-CPIWlPOM.mjs} +4 -4
  48. package/dist/{spawn-HV345RN-.cjs → spawn-CmFOSVdW.cjs} +4 -4
  49. package/dist/{teams-BahinMnH.cjs → teams-CB1R9-Z9.cjs} +5 -5
  50. package/dist/{teams-BE8nMB6O.mjs → teams-CEahj4cm.mjs} +5 -5
  51. package/dist/{todo-CuQAkSp2.mjs → todo-Cknp5D4U.mjs} +2 -2
  52. package/dist/{todo-CMMQ6yhU.cjs → todo-DqyRI4wP.cjs} +2 -2
  53. package/dist/{types-B0sSi6eM.cjs → types-BbvK1Tr_.cjs} +192 -2
  54. package/dist/{types-BrGJ7PJz.mjs → types-Dh-s4Lqs.mjs} +192 -2
  55. package/package.json +7 -7
@@ -4,9 +4,9 @@ var os = require('node:os');
4
4
  var promises = require('node:fs/promises');
5
5
  var z = require('zod');
6
6
  var node_crypto = require('node:crypto');
7
- var agentGuidance = require('./agentGuidance-Db76O1kw.cjs');
8
- var persistence = require('./limits-CyuIt-Vl.cjs');
9
- var install = require('./install-BMlV0Tnr.cjs');
7
+ var agentGuidance = require('./agentGuidance-BRDN1NoZ.cjs');
8
+ var persistence = require('./limits-BH5CV5vR.cjs');
9
+ var install = require('./install-BOHrUaFM.cjs');
10
10
  require('node:fs');
11
11
  require('node:path');
12
12
  require('axios');
@@ -15,8 +15,8 @@ require('@paralleldrive/cuid2');
15
15
  require('tweetnacl');
16
16
  require('chalk');
17
17
  require('node:util');
18
- require('./index-C9QzZsUQ.cjs');
19
- require('./types-B0sSi6eM.cjs');
18
+ require('./index-rH3DrQ5B.cjs');
19
+ require('./types-BbvK1Tr_.cjs');
20
20
  require('node:events');
21
21
  require('child_process');
22
22
  require('util');
@@ -2,9 +2,9 @@ import { homedir } from 'node:os';
2
2
  import { readFile } from 'node:fs/promises';
3
3
  import { z } from 'zod';
4
4
  import { randomUUID } from 'node:crypto';
5
- import { I as teamRequest, J as createTeamsClient } from './agentGuidance-CU6YD_Ms.mjs';
6
- import { h as readCredentialsForConfiguredRelay } from './limits-A2c-3M-_.mjs';
7
- import { a as installTeamSkill, i as installAutomationSkill } from './install-BuYnqtVa.mjs';
5
+ import { I as teamRequest, J as createTeamsClient } from './agentGuidance-KgvYuF3R.mjs';
6
+ import { h as readCredentialsForConfiguredRelay } from './limits-CjRj8IX5.mjs';
7
+ import { a as installTeamSkill, i as installAutomationSkill } from './install-BPhJppvd.mjs';
8
8
  import 'node:fs';
9
9
  import 'node:path';
10
10
  import 'axios';
@@ -13,8 +13,8 @@ import '@paralleldrive/cuid2';
13
13
  import 'tweetnacl';
14
14
  import 'chalk';
15
15
  import 'node:util';
16
- import './index-BYU1e81k.mjs';
17
- import './types-BrGJ7PJz.mjs';
16
+ import './index-BF6WfgV5.mjs';
17
+ import './types-Dh-s4Lqs.mjs';
18
18
  import 'node:events';
19
19
  import 'child_process';
20
20
  import 'util';
@@ -1,5 +1,5 @@
1
- import { h as readCredentialsForConfiguredRelay, j as createBuiltinTodoClient, c as configuration, B as BUILTIN_TODO_SKILL } from './limits-A2c-3M-_.mjs';
2
- import { C as readTeamScope, E as teamScopePath } from './agentGuidance-CU6YD_Ms.mjs';
1
+ import { h as readCredentialsForConfiguredRelay, j as createBuiltinTodoClient, c as configuration, B as BUILTIN_TODO_SKILL } from './limits-CjRj8IX5.mjs';
2
+ import { C as readTeamScope, E as teamScopePath } from './agentGuidance-KgvYuF3R.mjs';
3
3
  import 'zod';
4
4
  import '@paralleldrive/cuid2';
5
5
  import 'node:fs/promises';
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
- var persistence = require('./limits-CyuIt-Vl.cjs');
4
- var agentGuidance = require('./agentGuidance-Db76O1kw.cjs');
3
+ var persistence = require('./limits-BH5CV5vR.cjs');
4
+ var agentGuidance = require('./agentGuidance-BRDN1NoZ.cjs');
5
5
  require('zod');
6
6
  require('@paralleldrive/cuid2');
7
7
  require('node:fs/promises');
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var axios = require('axios');
4
- var persistence = require('./limits-CyuIt-Vl.cjs');
4
+ var persistence = require('./limits-BH5CV5vR.cjs');
5
5
  var node_events = require('node:events');
6
6
  var socket_ioClient = require('socket.io-client');
7
7
  var z = require('zod');
@@ -2252,6 +2252,143 @@ class TurnReporter {
2252
2252
  }
2253
2253
  }
2254
2254
 
2255
+ const PROMPT_QUEUE_INFLIGHT_TIMEOUT_MS = 2e4;
2256
+ const PROMPT_QUEUE_RETRY_MIN_MS = 1e3;
2257
+ const PROMPT_QUEUE_RETRY_MAX_MS = 3e4;
2258
+ class PromptQueueDrain {
2259
+ constructor(deps) {
2260
+ this.deps = deps;
2261
+ this.now = deps.now ?? (() => Date.now());
2262
+ this.log = deps.log ?? (() => {
2263
+ });
2264
+ this.schedule = deps.schedule ?? ((fn, delayMs) => {
2265
+ const timer = setTimeout(fn, delayMs);
2266
+ timer.unref?.();
2267
+ });
2268
+ this.inflightTimeoutMs = deps.inflightTimeoutMs ?? PROMPT_QUEUE_INFLIGHT_TIMEOUT_MS;
2269
+ }
2270
+ inflight = null;
2271
+ /**
2272
+ * localIds routed while a dispatch call was still in flight. The server's
2273
+ * `new-message` echo regularly beats the HTTP response (both come off the
2274
+ * same transaction), so `onInbound` can precede the `dispatched` outcome;
2275
+ * without this the guard armed AFTER the message had arrived and sat
2276
+ * until the timeout — observed in the first isolated e2e run.
2277
+ */
2278
+ arrivedDuringDispatch = /* @__PURE__ */ new Set();
2279
+ /** Items the server still holds, from the last snapshot / dispatch response. */
2280
+ pending = 0;
2281
+ disabled = false;
2282
+ closed = false;
2283
+ busy = false;
2284
+ failures = 0;
2285
+ retryAt = 0;
2286
+ retryScheduled = false;
2287
+ now;
2288
+ log;
2289
+ schedule;
2290
+ inflightTimeoutMs;
2291
+ /** The runner is blocked on an empty input queue. */
2292
+ onIdle() {
2293
+ void this.maybeDispatch("idle");
2294
+ }
2295
+ /** A `prompt-queue` snapshot arrived; only a non-empty queue is worth a call. */
2296
+ onQueueChanged(count) {
2297
+ this.pending = Math.max(0, count);
2298
+ if (count > 0) void this.maybeDispatch("queue-changed");
2299
+ }
2300
+ /**
2301
+ * True while the server still holds queued prompts for this session. The
2302
+ * runners fold it into their "idle, waiting for the user" judgement so a
2303
+ * turn that ends with more queue behind it raises no reply_done /
2304
+ * input_needed notification and no daemon `completed` report per item.
2305
+ */
2306
+ hasPending() {
2307
+ return !this.disabled && !this.closed && this.pending > 0;
2308
+ }
2309
+ /** A user message with this localId was routed into the runner's queue. */
2310
+ onInbound(localId) {
2311
+ if (this.inflight?.localId === localId) {
2312
+ this.inflight = null;
2313
+ return;
2314
+ }
2315
+ if (this.busy) {
2316
+ this.arrivedDuringDispatch.add(localId);
2317
+ if (this.arrivedDuringDispatch.size > 64) {
2318
+ const oldest = this.arrivedDuringDispatch.values().next().value;
2319
+ if (oldest !== void 0) this.arrivedDuringDispatch.delete(oldest);
2320
+ }
2321
+ }
2322
+ }
2323
+ close() {
2324
+ this.closed = true;
2325
+ }
2326
+ state() {
2327
+ return { inflight: this.inflight, disabled: this.disabled, busy: this.busy, failures: this.failures, retryAt: this.retryAt, pending: this.pending };
2328
+ }
2329
+ async maybeDispatch(reason) {
2330
+ if (this.closed || this.disabled || this.busy) return false;
2331
+ const now = this.now();
2332
+ if (this.inflight) {
2333
+ if (now - this.inflight.since < this.inflightTimeoutMs) return false;
2334
+ this.log(`[prompt-queue] inflight ${this.inflight.localId} not routed within ${this.inflightTimeoutMs}ms; releasing`);
2335
+ this.inflight = null;
2336
+ }
2337
+ if (now < this.retryAt) {
2338
+ this.scheduleRetry(this.retryAt - now);
2339
+ return false;
2340
+ }
2341
+ if (!this.deps.isIdle()) return false;
2342
+ this.busy = true;
2343
+ this.arrivedDuringDispatch.clear();
2344
+ try {
2345
+ const outcome = await this.deps.dispatch();
2346
+ switch (outcome.kind) {
2347
+ case "dispatched": {
2348
+ this.failures = 0;
2349
+ if (typeof outcome.remaining === "number") this.pending = Math.max(0, outcome.remaining);
2350
+ const alreadyRouted = this.arrivedDuringDispatch.delete(outcome.localId) || outcome.created === false;
2351
+ if (!alreadyRouted) {
2352
+ this.inflight = { localId: outcome.localId, since: this.now() };
2353
+ this.schedule(() => {
2354
+ void this.maybeDispatch("inflight-timeout");
2355
+ }, this.inflightTimeoutMs + 1);
2356
+ }
2357
+ this.log(`[prompt-queue] dispatched ${outcome.localId} (${reason}${alreadyRouted ? ", already routed" : ""})`);
2358
+ return true;
2359
+ }
2360
+ case "empty":
2361
+ this.failures = 0;
2362
+ this.pending = 0;
2363
+ return false;
2364
+ case "unsupported":
2365
+ this.disabled = true;
2366
+ this.log("[prompt-queue] server has no prompt queue; drain disabled for this process");
2367
+ return false;
2368
+ case "error": {
2369
+ this.failures += 1;
2370
+ const delay = Math.min(PROMPT_QUEUE_RETRY_MAX_MS, PROMPT_QUEUE_RETRY_MIN_MS * 2 ** (this.failures - 1));
2371
+ this.retryAt = this.now() + delay;
2372
+ this.log(`[prompt-queue] dispatch failed (${outcome.message}); retry in ${delay}ms`);
2373
+ this.scheduleRetry(delay);
2374
+ return false;
2375
+ }
2376
+ }
2377
+ } finally {
2378
+ this.busy = false;
2379
+ this.arrivedDuringDispatch.clear();
2380
+ }
2381
+ }
2382
+ scheduleRetry(delayMs) {
2383
+ if (this.retryScheduled || this.closed) return;
2384
+ this.retryScheduled = true;
2385
+ this.schedule(() => {
2386
+ this.retryScheduled = false;
2387
+ void this.maybeDispatch("retry");
2388
+ }, delayMs);
2389
+ }
2390
+ }
2391
+
2255
2392
  class ApiSessionClient extends node_events.EventEmitter {
2256
2393
  token;
2257
2394
  sessionId;
@@ -2315,6 +2452,8 @@ class ApiSessionClient extends node_events.EventEmitter {
2315
2452
  * must wait for the first fetch, otherwise a message that lands between
2316
2453
  * the seed and the first fetch is routed twice (socket + fetch). */
2317
2454
  awaitingInitialFetch = false;
2455
+ /** B-509: server-side prompt queue drain, attached by the runner. */
2456
+ promptQueueDrain = null;
2318
2457
  constructor(token, session, opts) {
2319
2458
  super();
2320
2459
  this.token = token;
@@ -2349,6 +2488,7 @@ class ApiSessionClient extends node_events.EventEmitter {
2349
2488
  this.rpcHandlerManager.onSocketConnect(controlSocket);
2350
2489
  this.receiveSync.invalidate();
2351
2490
  void this.connectSessionRelay();
2491
+ void this.promptQueueDrain?.maybeDispatch("connect");
2352
2492
  });
2353
2493
  this.socket.on("rpc-request", async (data, callback) => {
2354
2494
  await answerSocketRequest(
@@ -2384,6 +2524,11 @@ class ApiSessionClient extends node_events.EventEmitter {
2384
2524
  persistence.logger.debug("[SOCKET] [UPDATE] [ERROR] No body in update!");
2385
2525
  return;
2386
2526
  }
2527
+ if (data.body.t === "prompt-queue") {
2528
+ const items = data.body.items;
2529
+ this.promptQueueDrain?.onQueueChanged(Array.isArray(items) ? items.length : 0);
2530
+ return;
2531
+ }
2387
2532
  if (data.body.t === "new-message") {
2388
2533
  const messageSeq = data.body.message?.seq;
2389
2534
  if (this.lastSeq === 0 || this.awaitingInitialFetch) {
@@ -2634,7 +2779,51 @@ class ApiSessionClient extends node_events.EventEmitter {
2634
2779
  "X-Happy-Client": `cli-coding-session/${persistence.configuration.currentCliVersion}`
2635
2780
  };
2636
2781
  }
2782
+ /**
2783
+ * B-509: attach the server-side prompt queue drain. `isIdle` is the
2784
+ * runner's own notion (input queue empty + blocked waiting); dispatching is
2785
+ * `POST /v1/sessions/:id/prompt-queue/dispatch`, whose message then comes
2786
+ * back through the ordinary socket / catch-up path and is routed below.
2787
+ */
2788
+ attachPromptQueueDrain(opts) {
2789
+ this.promptQueueDrain?.close();
2790
+ const drain = new PromptQueueDrain({
2791
+ isIdle: opts.isIdle,
2792
+ dispatch: () => this.dispatchPromptQueueHead(),
2793
+ log: (message) => persistence.logger.debug(`[API] ${message}`)
2794
+ });
2795
+ this.promptQueueDrain = drain;
2796
+ return drain;
2797
+ }
2798
+ /** B-509: the server still holds queued prompts for this session (see PromptQueueDrain.hasPending). */
2799
+ promptQueueHasPending() {
2800
+ return this.promptQueueDrain?.hasPending() === true;
2801
+ }
2802
+ /** B-509: stop draining before the process winds down (cleanup's first line). */
2803
+ closePromptQueueDrain() {
2804
+ this.promptQueueDrain?.close();
2805
+ }
2806
+ async dispatchPromptQueueHead() {
2807
+ try {
2808
+ const response = await axios.post(
2809
+ `${persistence.configuration.serverUrl}/v1/sessions/${encodeURIComponent(this.sessionId)}/prompt-queue/dispatch`,
2810
+ {},
2811
+ { headers: this.authHeaders(), timeout: 3e4, validateStatus: () => true }
2812
+ );
2813
+ if (response.status === 404) return { kind: "unsupported" };
2814
+ if (response.status !== 200) return { kind: "error", message: `HTTP ${response.status}` };
2815
+ const dispatched = response.data?.dispatched;
2816
+ const remaining = typeof response.data?.remaining === "number" ? response.data.remaining : void 0;
2817
+ if (dispatched && typeof dispatched.localId === "string") {
2818
+ return { kind: "dispatched", localId: dispatched.localId, remaining, created: dispatched.created !== false };
2819
+ }
2820
+ return { kind: "empty" };
2821
+ } catch (error) {
2822
+ return { kind: "error", message: error instanceof Error ? error.message : String(error) };
2823
+ }
2824
+ }
2637
2825
  routeIncomingMessage(message, sourceLocalId) {
2826
+ if (sourceLocalId) this.promptQueueDrain?.onInbound(sourceLocalId);
2638
2827
  const messageWithSource = sourceLocalId && typeof message === "object" && message !== null ? { ...message, localKey: message.localKey ?? sourceLocalId } : message;
2639
2828
  const userResult = UserMessageSchema.safeParse(messageWithSource);
2640
2829
  if (userResult.success) {
@@ -2817,7 +3006,7 @@ class ApiSessionClient extends node_events.EventEmitter {
2817
3006
  const persisted = await axios.post(
2818
3007
  `${persistence.configuration.serverUrl}/v3/sessions/${encodeURIComponent(this.sessionId)}/messages`,
2819
3008
  { messages: batch },
2820
- { headers: this.authHeaders(), timeout: 6e4 }
3009
+ { headers: { ...this.authHeaders(), "X-Happy-Message-Origin": "relay-client" }, timeout: 6e4 }
2821
3010
  );
2822
3011
  const stored = Array.isArray(persisted.data.messages) ? persisted.data.messages : [];
2823
3012
  const storedLocalIds = new Set(stored.map((message) => message.localId));
@@ -3320,6 +3509,7 @@ class ApiSessionClient extends node_events.EventEmitter {
3320
3509
  }
3321
3510
  async close() {
3322
3511
  persistence.logger.debug("[API] socket.close() called");
3512
+ this.promptQueueDrain?.close();
3323
3513
  this.sendSync.stop();
3324
3514
  this.receiveSync.stop();
3325
3515
  if (this.reconnectInterval) {
@@ -1,5 +1,5 @@
1
1
  import axios from 'axios';
2
- import { S as AgentVersionsSchema, l as logger, w as encodeBase64, L as encrypt, N as decrypt, M as decodeBase64, c as configuration, e as createEnvelope, s as streamKeyOf, T as backoff, U as ReleaseDrainNoticeSchema, V as decryptBlob, W as RelayAssignmentResponseSchema, X as notifyDaemonTurnEvent, Y as BackgroundTaskReporter, Z as notifyDaemonBackgroundTasks, _ as prepareClipboardText, $ as prepareClipboardHistoryText, g as delay, r as readSettings, d as readPersistedSessions, a0 as credentialRelayProblem, F as packageJson, a1 as RelayCandidatesResponseSchema, a2 as readTrackedClaudeSessionIds, a3 as readTrackedCodexThreadIds, a4 as isValidTerminalId, a5 as exponentialBackoffDelay, a6 as libsodiumEncryptForPublicKey, a7 as libsodiumPublicKeyFromSecretKey, a8 as getRandomBytes, a9 as redactLogValue } from './limits-A2c-3M-_.mjs';
2
+ import { T as AgentVersionsSchema, l as logger, w as encodeBase64, L as encrypt, N as decrypt, M as decodeBase64, c as configuration, e as createEnvelope, s as streamKeyOf, U as backoff, V as ReleaseDrainNoticeSchema, W as decryptBlob, X as RelayAssignmentResponseSchema, Y as notifyDaemonTurnEvent, Z as BackgroundTaskReporter, _ as notifyDaemonBackgroundTasks, $ as prepareClipboardText, a0 as prepareClipboardHistoryText, g as delay, r as readSettings, d as readPersistedSessions, a1 as credentialRelayProblem, F as packageJson, a2 as RelayCandidatesResponseSchema, a3 as readTrackedClaudeSessionIds, a4 as readTrackedCodexThreadIds, a5 as isValidTerminalId, a6 as exponentialBackoffDelay, a7 as libsodiumEncryptForPublicKey, a8 as libsodiumPublicKeyFromSecretKey, a9 as getRandomBytes, aa as redactLogValue } from './limits-CjRj8IX5.mjs';
3
3
  import { EventEmitter } from 'node:events';
4
4
  import { io } from 'socket.io-client';
5
5
  import { z } from 'zod';
@@ -2231,6 +2231,143 @@ class TurnReporter {
2231
2231
  }
2232
2232
  }
2233
2233
 
2234
+ const PROMPT_QUEUE_INFLIGHT_TIMEOUT_MS = 2e4;
2235
+ const PROMPT_QUEUE_RETRY_MIN_MS = 1e3;
2236
+ const PROMPT_QUEUE_RETRY_MAX_MS = 3e4;
2237
+ class PromptQueueDrain {
2238
+ constructor(deps) {
2239
+ this.deps = deps;
2240
+ this.now = deps.now ?? (() => Date.now());
2241
+ this.log = deps.log ?? (() => {
2242
+ });
2243
+ this.schedule = deps.schedule ?? ((fn, delayMs) => {
2244
+ const timer = setTimeout(fn, delayMs);
2245
+ timer.unref?.();
2246
+ });
2247
+ this.inflightTimeoutMs = deps.inflightTimeoutMs ?? PROMPT_QUEUE_INFLIGHT_TIMEOUT_MS;
2248
+ }
2249
+ inflight = null;
2250
+ /**
2251
+ * localIds routed while a dispatch call was still in flight. The server's
2252
+ * `new-message` echo regularly beats the HTTP response (both come off the
2253
+ * same transaction), so `onInbound` can precede the `dispatched` outcome;
2254
+ * without this the guard armed AFTER the message had arrived and sat
2255
+ * until the timeout — observed in the first isolated e2e run.
2256
+ */
2257
+ arrivedDuringDispatch = /* @__PURE__ */ new Set();
2258
+ /** Items the server still holds, from the last snapshot / dispatch response. */
2259
+ pending = 0;
2260
+ disabled = false;
2261
+ closed = false;
2262
+ busy = false;
2263
+ failures = 0;
2264
+ retryAt = 0;
2265
+ retryScheduled = false;
2266
+ now;
2267
+ log;
2268
+ schedule;
2269
+ inflightTimeoutMs;
2270
+ /** The runner is blocked on an empty input queue. */
2271
+ onIdle() {
2272
+ void this.maybeDispatch("idle");
2273
+ }
2274
+ /** A `prompt-queue` snapshot arrived; only a non-empty queue is worth a call. */
2275
+ onQueueChanged(count) {
2276
+ this.pending = Math.max(0, count);
2277
+ if (count > 0) void this.maybeDispatch("queue-changed");
2278
+ }
2279
+ /**
2280
+ * True while the server still holds queued prompts for this session. The
2281
+ * runners fold it into their "idle, waiting for the user" judgement so a
2282
+ * turn that ends with more queue behind it raises no reply_done /
2283
+ * input_needed notification and no daemon `completed` report per item.
2284
+ */
2285
+ hasPending() {
2286
+ return !this.disabled && !this.closed && this.pending > 0;
2287
+ }
2288
+ /** A user message with this localId was routed into the runner's queue. */
2289
+ onInbound(localId) {
2290
+ if (this.inflight?.localId === localId) {
2291
+ this.inflight = null;
2292
+ return;
2293
+ }
2294
+ if (this.busy) {
2295
+ this.arrivedDuringDispatch.add(localId);
2296
+ if (this.arrivedDuringDispatch.size > 64) {
2297
+ const oldest = this.arrivedDuringDispatch.values().next().value;
2298
+ if (oldest !== void 0) this.arrivedDuringDispatch.delete(oldest);
2299
+ }
2300
+ }
2301
+ }
2302
+ close() {
2303
+ this.closed = true;
2304
+ }
2305
+ state() {
2306
+ return { inflight: this.inflight, disabled: this.disabled, busy: this.busy, failures: this.failures, retryAt: this.retryAt, pending: this.pending };
2307
+ }
2308
+ async maybeDispatch(reason) {
2309
+ if (this.closed || this.disabled || this.busy) return false;
2310
+ const now = this.now();
2311
+ if (this.inflight) {
2312
+ if (now - this.inflight.since < this.inflightTimeoutMs) return false;
2313
+ this.log(`[prompt-queue] inflight ${this.inflight.localId} not routed within ${this.inflightTimeoutMs}ms; releasing`);
2314
+ this.inflight = null;
2315
+ }
2316
+ if (now < this.retryAt) {
2317
+ this.scheduleRetry(this.retryAt - now);
2318
+ return false;
2319
+ }
2320
+ if (!this.deps.isIdle()) return false;
2321
+ this.busy = true;
2322
+ this.arrivedDuringDispatch.clear();
2323
+ try {
2324
+ const outcome = await this.deps.dispatch();
2325
+ switch (outcome.kind) {
2326
+ case "dispatched": {
2327
+ this.failures = 0;
2328
+ if (typeof outcome.remaining === "number") this.pending = Math.max(0, outcome.remaining);
2329
+ const alreadyRouted = this.arrivedDuringDispatch.delete(outcome.localId) || outcome.created === false;
2330
+ if (!alreadyRouted) {
2331
+ this.inflight = { localId: outcome.localId, since: this.now() };
2332
+ this.schedule(() => {
2333
+ void this.maybeDispatch("inflight-timeout");
2334
+ }, this.inflightTimeoutMs + 1);
2335
+ }
2336
+ this.log(`[prompt-queue] dispatched ${outcome.localId} (${reason}${alreadyRouted ? ", already routed" : ""})`);
2337
+ return true;
2338
+ }
2339
+ case "empty":
2340
+ this.failures = 0;
2341
+ this.pending = 0;
2342
+ return false;
2343
+ case "unsupported":
2344
+ this.disabled = true;
2345
+ this.log("[prompt-queue] server has no prompt queue; drain disabled for this process");
2346
+ return false;
2347
+ case "error": {
2348
+ this.failures += 1;
2349
+ const delay = Math.min(PROMPT_QUEUE_RETRY_MAX_MS, PROMPT_QUEUE_RETRY_MIN_MS * 2 ** (this.failures - 1));
2350
+ this.retryAt = this.now() + delay;
2351
+ this.log(`[prompt-queue] dispatch failed (${outcome.message}); retry in ${delay}ms`);
2352
+ this.scheduleRetry(delay);
2353
+ return false;
2354
+ }
2355
+ }
2356
+ } finally {
2357
+ this.busy = false;
2358
+ this.arrivedDuringDispatch.clear();
2359
+ }
2360
+ }
2361
+ scheduleRetry(delayMs) {
2362
+ if (this.retryScheduled || this.closed) return;
2363
+ this.retryScheduled = true;
2364
+ this.schedule(() => {
2365
+ this.retryScheduled = false;
2366
+ void this.maybeDispatch("retry");
2367
+ }, delayMs);
2368
+ }
2369
+ }
2370
+
2234
2371
  class ApiSessionClient extends EventEmitter {
2235
2372
  token;
2236
2373
  sessionId;
@@ -2294,6 +2431,8 @@ class ApiSessionClient extends EventEmitter {
2294
2431
  * must wait for the first fetch, otherwise a message that lands between
2295
2432
  * the seed and the first fetch is routed twice (socket + fetch). */
2296
2433
  awaitingInitialFetch = false;
2434
+ /** B-509: server-side prompt queue drain, attached by the runner. */
2435
+ promptQueueDrain = null;
2297
2436
  constructor(token, session, opts) {
2298
2437
  super();
2299
2438
  this.token = token;
@@ -2328,6 +2467,7 @@ class ApiSessionClient extends EventEmitter {
2328
2467
  this.rpcHandlerManager.onSocketConnect(controlSocket);
2329
2468
  this.receiveSync.invalidate();
2330
2469
  void this.connectSessionRelay();
2470
+ void this.promptQueueDrain?.maybeDispatch("connect");
2331
2471
  });
2332
2472
  this.socket.on("rpc-request", async (data, callback) => {
2333
2473
  await answerSocketRequest(
@@ -2363,6 +2503,11 @@ class ApiSessionClient extends EventEmitter {
2363
2503
  logger.debug("[SOCKET] [UPDATE] [ERROR] No body in update!");
2364
2504
  return;
2365
2505
  }
2506
+ if (data.body.t === "prompt-queue") {
2507
+ const items = data.body.items;
2508
+ this.promptQueueDrain?.onQueueChanged(Array.isArray(items) ? items.length : 0);
2509
+ return;
2510
+ }
2366
2511
  if (data.body.t === "new-message") {
2367
2512
  const messageSeq = data.body.message?.seq;
2368
2513
  if (this.lastSeq === 0 || this.awaitingInitialFetch) {
@@ -2613,7 +2758,51 @@ class ApiSessionClient extends EventEmitter {
2613
2758
  "X-Happy-Client": `cli-coding-session/${configuration.currentCliVersion}`
2614
2759
  };
2615
2760
  }
2761
+ /**
2762
+ * B-509: attach the server-side prompt queue drain. `isIdle` is the
2763
+ * runner's own notion (input queue empty + blocked waiting); dispatching is
2764
+ * `POST /v1/sessions/:id/prompt-queue/dispatch`, whose message then comes
2765
+ * back through the ordinary socket / catch-up path and is routed below.
2766
+ */
2767
+ attachPromptQueueDrain(opts) {
2768
+ this.promptQueueDrain?.close();
2769
+ const drain = new PromptQueueDrain({
2770
+ isIdle: opts.isIdle,
2771
+ dispatch: () => this.dispatchPromptQueueHead(),
2772
+ log: (message) => logger.debug(`[API] ${message}`)
2773
+ });
2774
+ this.promptQueueDrain = drain;
2775
+ return drain;
2776
+ }
2777
+ /** B-509: the server still holds queued prompts for this session (see PromptQueueDrain.hasPending). */
2778
+ promptQueueHasPending() {
2779
+ return this.promptQueueDrain?.hasPending() === true;
2780
+ }
2781
+ /** B-509: stop draining before the process winds down (cleanup's first line). */
2782
+ closePromptQueueDrain() {
2783
+ this.promptQueueDrain?.close();
2784
+ }
2785
+ async dispatchPromptQueueHead() {
2786
+ try {
2787
+ const response = await axios.post(
2788
+ `${configuration.serverUrl}/v1/sessions/${encodeURIComponent(this.sessionId)}/prompt-queue/dispatch`,
2789
+ {},
2790
+ { headers: this.authHeaders(), timeout: 3e4, validateStatus: () => true }
2791
+ );
2792
+ if (response.status === 404) return { kind: "unsupported" };
2793
+ if (response.status !== 200) return { kind: "error", message: `HTTP ${response.status}` };
2794
+ const dispatched = response.data?.dispatched;
2795
+ const remaining = typeof response.data?.remaining === "number" ? response.data.remaining : void 0;
2796
+ if (dispatched && typeof dispatched.localId === "string") {
2797
+ return { kind: "dispatched", localId: dispatched.localId, remaining, created: dispatched.created !== false };
2798
+ }
2799
+ return { kind: "empty" };
2800
+ } catch (error) {
2801
+ return { kind: "error", message: error instanceof Error ? error.message : String(error) };
2802
+ }
2803
+ }
2616
2804
  routeIncomingMessage(message, sourceLocalId) {
2805
+ if (sourceLocalId) this.promptQueueDrain?.onInbound(sourceLocalId);
2617
2806
  const messageWithSource = sourceLocalId && typeof message === "object" && message !== null ? { ...message, localKey: message.localKey ?? sourceLocalId } : message;
2618
2807
  const userResult = UserMessageSchema.safeParse(messageWithSource);
2619
2808
  if (userResult.success) {
@@ -2796,7 +2985,7 @@ class ApiSessionClient extends EventEmitter {
2796
2985
  const persisted = await axios.post(
2797
2986
  `${configuration.serverUrl}/v3/sessions/${encodeURIComponent(this.sessionId)}/messages`,
2798
2987
  { messages: batch },
2799
- { headers: this.authHeaders(), timeout: 6e4 }
2988
+ { headers: { ...this.authHeaders(), "X-Happy-Message-Origin": "relay-client" }, timeout: 6e4 }
2800
2989
  );
2801
2990
  const stored = Array.isArray(persisted.data.messages) ? persisted.data.messages : [];
2802
2991
  const storedLocalIds = new Set(stored.map((message) => message.localId));
@@ -3299,6 +3488,7 @@ class ApiSessionClient extends EventEmitter {
3299
3488
  }
3300
3489
  async close() {
3301
3490
  logger.debug("[API] socket.close() called");
3491
+ this.promptQueueDrain?.close();
3302
3492
  this.sendSync.stop();
3303
3493
  this.receiveSync.stop();
3304
3494
  if (this.reconnectInterval) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "very-happy-cli",
3
- "version": "0.2.157",
3
+ "version": "0.2.159",
4
4
  "description": "Remote multi-agent workspace CLI with deep Claude integration, native terminal streaming, and self-hosting.",
5
5
  "author": "Kirill Dubovitskiy",
6
6
  "contributors": [
@@ -117,12 +117,12 @@
117
117
  "zod": "^4.0.0"
118
118
  },
119
119
  "optionalDependencies": {
120
- "very-happy-tools-arm64-darwin": "0.2.157",
121
- "very-happy-tools-x64-darwin": "0.2.157",
122
- "very-happy-tools-arm64-linux": "0.2.157",
123
- "very-happy-tools-x64-linux": "0.2.157",
124
- "very-happy-tools-arm64-win32": "0.2.157",
125
- "very-happy-tools-x64-win32": "0.2.157"
120
+ "very-happy-tools-arm64-darwin": "0.2.159",
121
+ "very-happy-tools-x64-darwin": "0.2.159",
122
+ "very-happy-tools-arm64-linux": "0.2.159",
123
+ "very-happy-tools-x64-linux": "0.2.159",
124
+ "very-happy-tools-arm64-win32": "0.2.159",
125
+ "very-happy-tools-x64-win32": "0.2.159"
126
126
  },
127
127
  "devDependencies": {
128
128
  "@slopus/happy-wire": "workspace:*",