very-happy-cli 0.2.158 → 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.
- package/dist/{AcpBackend-DIDRig5G.cjs → AcpBackend-DUYFlyfN.cjs} +2 -2
- package/dist/{AcpBackend-vDVLHuYJ.mjs → AcpBackend-VsfkZiVF.mjs} +2 -2
- package/dist/{AcpSessionManager-C44U99CC.cjs → AcpSessionManager-Bdv8LWXh.cjs} +1 -1
- package/dist/{AcpSessionManager-CmRjfhn3.mjs → AcpSessionManager-rYtVb0kN.mjs} +1 -1
- package/dist/{agentGuidance-DwOWbX55.cjs → agentGuidance-BRDN1NoZ.cjs} +1 -1
- package/dist/{agentGuidance-CrR9l62U.mjs → agentGuidance-KgvYuF3R.mjs} +1 -1
- package/dist/{agentHome-Dw8Jx_CJ.cjs → agentHome-CHh4lmjM.cjs} +2 -2
- package/dist/{agentHome-CyLmGH5L.mjs → agentHome-CvsJyeQB.mjs} +2 -2
- package/dist/{auto-DZbNcTXx.cjs → auto-DKmEls8Z.cjs} +6 -6
- package/dist/{auto-BlMZPwOk.mjs → auto-nb8GhRRy.mjs} +6 -6
- package/dist/codex/happyMcpStdioBridge.cjs +2 -2
- package/dist/codex/happyMcpStdioBridge.mjs +2 -2
- package/dist/{config-BtPArqRH.mjs → config-ClLtM6F3.mjs} +4 -4
- package/dist/{config-DG84bjwF.cjs → config-DLx-vW3I.cjs} +4 -4
- package/dist/{index-ZChGsuB7.mjs → index-2mCaeKyf.mjs} +19 -9
- package/dist/{index-DPCsWlWK.cjs → index-BEOMECtE.cjs} +18 -8
- package/dist/{index-CVCt3kSt.mjs → index-BF6WfgV5.mjs} +62 -29
- package/dist/{index-DgO0FHOx.mjs → index-C-JazrMB.mjs} +4 -4
- package/dist/{index-D5ScEErM.cjs → index-blDSadUR.cjs} +4 -4
- package/dist/{index-BG932eCh.cjs → index-rH3DrQ5B.cjs} +66 -33
- package/dist/index.cjs +4 -4
- package/dist/index.mjs +4 -4
- package/dist/{install-wKQGgXvN.cjs → install-BOHrUaFM.cjs} +1 -1
- package/dist/{install-DarS_Wco.mjs → install-BPhJppvd.mjs} +1 -1
- package/dist/{installTerminalHooks-DcQfee_O.cjs → installTerminalHooks-Cou3WUf3.cjs} +4 -4
- package/dist/{installTerminalHooks-CpNbRFPz.mjs → installTerminalHooks-WeznjkuC.mjs} +4 -4
- package/dist/lib.cjs +2 -2
- package/dist/lib.d.cts +114 -0
- package/dist/lib.d.mts +114 -0
- package/dist/lib.mjs +2 -2
- package/dist/{limits-kqFqhfsh.cjs → limits-BH5CV5vR.cjs} +38 -7
- package/dist/{limits-D2uN090G.mjs → limits-CjRj8IX5.mjs} +38 -8
- package/dist/{mcp-DGdeUUDJ.cjs → mcp-CPYbtsYG.cjs} +4 -4
- package/dist/{mcp-BN6bKIGb.mjs → mcp-Dy3f_K0h.mjs} +4 -4
- package/dist/{piRuntime-3qtbKujX.cjs → piRuntime-NtF0YqjH.cjs} +2 -2
- package/dist/{piRuntime-bkw2NAZU.mjs → piRuntime-kLZNpAAl.mjs} +2 -2
- package/dist/{piTerminal-BYxp3LMr.cjs → piTerminal-9J0GAKzb.cjs} +6 -6
- package/dist/{piTerminal-DilKwM7B.mjs → piTerminal-B0fEOV_2.mjs} +6 -6
- package/dist/{runGemini-Cjii069p.mjs → runGemini-DV6Ouadt.mjs} +6 -6
- package/dist/{runGemini-28O2sqyI.cjs → runGemini-F-mqx3_K.cjs} +6 -6
- package/dist/{runOpenClaw-jaLr7KF1.mjs → runOpenClaw-D7MlPS5n.mjs} +5 -5
- package/dist/{runOpenClaw-CiMZad-H.cjs → runOpenClaw-Qqo9j63M.cjs} +5 -5
- package/dist/{send-D5ejHPXG.cjs → send-4qJTCAYW.cjs} +2 -2
- package/dist/{send-C9mOtChi.mjs → send-CLhYXNgy.mjs} +2 -2
- package/dist/{sessions-BNMPQoLE.cjs → sessions-Bh1pyddi.cjs} +2 -2
- package/dist/{sessions-W6771g3X.mjs → sessions-CzIH2rGp.mjs} +2 -2
- package/dist/{spawn-D0QVhzRb.mjs → spawn-CPIWlPOM.mjs} +4 -4
- package/dist/{spawn-Ddtom1cb.cjs → spawn-CmFOSVdW.cjs} +4 -4
- package/dist/{teams-BZ1tayg_.cjs → teams-CB1R9-Z9.cjs} +5 -5
- package/dist/{teams-CqSwtw2O.mjs → teams-CEahj4cm.mjs} +5 -5
- package/dist/{todo-BbusB1zM.mjs → todo-Cknp5D4U.mjs} +2 -2
- package/dist/{todo-BYMg0fR0.cjs → todo-DqyRI4wP.cjs} +2 -2
- package/dist/{types-DnrPeN80.cjs → types-BbvK1Tr_.cjs} +191 -1
- package/dist/{types-DIBF4U0Y.mjs → types-Dh-s4Lqs.mjs} +191 -1
- package/package.json +7 -7
|
@@ -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-
|
|
6
|
-
import { h as readCredentialsForConfiguredRelay } from './limits-
|
|
7
|
-
import { a as installTeamSkill, i as installAutomationSkill } from './install-
|
|
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-
|
|
17
|
-
import './types-
|
|
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-
|
|
2
|
-
import { C as readTeamScope, E as teamScopePath } from './agentGuidance-
|
|
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-
|
|
4
|
-
var agentGuidance = require('./agentGuidance-
|
|
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-
|
|
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) {
|
|
@@ -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 {
|
|
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) {
|
|
@@ -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.
|
|
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.
|
|
121
|
-
"very-happy-tools-x64-darwin": "0.2.
|
|
122
|
-
"very-happy-tools-arm64-linux": "0.2.
|
|
123
|
-
"very-happy-tools-x64-linux": "0.2.
|
|
124
|
-
"very-happy-tools-arm64-win32": "0.2.
|
|
125
|
-
"very-happy-tools-x64-win32": "0.2.
|
|
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:*",
|