@hydra-acp/cli 0.1.0 → 0.1.2
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/README.md +0 -0
- package/dist/cli.js +1085 -310
- package/dist/index.d.ts +76 -5
- package/dist/index.js +551 -47
- package/package.json +5 -1
package/dist/cli.js
CHANGED
|
@@ -34,7 +34,12 @@ var init_paths = __esm({
|
|
|
34
34
|
agentsDir: () => path.join(hydraHome(), "agents"),
|
|
35
35
|
agentDir: (id) => path.join(hydraHome(), "agents", id),
|
|
36
36
|
sessionsDir: () => path.join(hydraHome(), "sessions"),
|
|
37
|
-
|
|
37
|
+
// One directory per session id under sessions/. Co-locates the
|
|
38
|
+
// session record, its transcript, and any future per-session state
|
|
39
|
+
// (uploads, scratch, etc.) so the lifecycle is just "rm -rf the dir".
|
|
40
|
+
sessionDir: (id) => path.join(hydraHome(), "sessions", id),
|
|
41
|
+
sessionFile: (id) => path.join(hydraHome(), "sessions", id, "meta.json"),
|
|
42
|
+
historyFile: (id) => path.join(hydraHome(), "sessions", id, "history.jsonl"),
|
|
38
43
|
extensionsDir: () => path.join(hydraHome(), "extensions"),
|
|
39
44
|
extensionLogFile: (name) => path.join(hydraHome(), "extensions", `${name}.log`),
|
|
40
45
|
extensionPidFile: (name) => path.join(hydraHome(), "extensions", `${name}.pid`),
|
|
@@ -215,6 +220,32 @@ function extractHydraMeta(meta) {
|
|
|
215
220
|
out.resume = parsed.data;
|
|
216
221
|
}
|
|
217
222
|
}
|
|
223
|
+
if (typeof obj.currentModel === "string") {
|
|
224
|
+
out.currentModel = obj.currentModel;
|
|
225
|
+
}
|
|
226
|
+
if (typeof obj.currentMode === "string") {
|
|
227
|
+
out.currentMode = obj.currentMode;
|
|
228
|
+
}
|
|
229
|
+
if (Array.isArray(obj.availableCommands)) {
|
|
230
|
+
const cmds = [];
|
|
231
|
+
for (const raw of obj.availableCommands) {
|
|
232
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
const c = raw;
|
|
236
|
+
if (typeof c.name !== "string") {
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
const cmd = { name: c.name };
|
|
240
|
+
if (typeof c.description === "string") {
|
|
241
|
+
cmd.description = c.description;
|
|
242
|
+
}
|
|
243
|
+
cmds.push(cmd);
|
|
244
|
+
}
|
|
245
|
+
if (cmds.length > 0) {
|
|
246
|
+
out.availableCommands = cmds;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
218
249
|
return out;
|
|
219
250
|
}
|
|
220
251
|
function mergeMeta(passthrough, ours) {
|
|
@@ -502,6 +533,25 @@ function withCode(err, code) {
|
|
|
502
533
|
err.code = code;
|
|
503
534
|
return err;
|
|
504
535
|
}
|
|
536
|
+
function isStateUpdate(method, params) {
|
|
537
|
+
if (method !== "session/update") {
|
|
538
|
+
return false;
|
|
539
|
+
}
|
|
540
|
+
const obj = params ?? {};
|
|
541
|
+
const kind = obj.update?.sessionUpdate;
|
|
542
|
+
return typeof kind === "string" && STATE_UPDATE_KINDS.has(kind);
|
|
543
|
+
}
|
|
544
|
+
function sameAdvertisedCommands(a, b) {
|
|
545
|
+
if (a.length !== b.length) {
|
|
546
|
+
return false;
|
|
547
|
+
}
|
|
548
|
+
for (let i = 0; i < a.length; i++) {
|
|
549
|
+
if (a[i]?.name !== b[i]?.name || a[i]?.description !== b[i]?.description) {
|
|
550
|
+
return false;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
return true;
|
|
554
|
+
}
|
|
505
555
|
function captureInternalChunk(capture, params) {
|
|
506
556
|
const obj = params ?? {};
|
|
507
557
|
const update = obj.update ?? {};
|
|
@@ -564,7 +614,7 @@ function firstLine(text, max) {
|
|
|
564
614
|
}
|
|
565
615
|
return void 0;
|
|
566
616
|
}
|
|
567
|
-
var HYDRA_ID_ALPHABET, generateHydraId, HYDRA_SESSION_PREFIX, Session;
|
|
617
|
+
var HYDRA_ID_ALPHABET, generateHydraId, HYDRA_SESSION_PREFIX, Session, STATE_UPDATE_KINDS;
|
|
568
618
|
var init_session = __esm({
|
|
569
619
|
"src/core/session.ts"() {
|
|
570
620
|
"use strict";
|
|
@@ -586,14 +636,26 @@ var init_session = __esm({
|
|
|
586
636
|
agentMeta;
|
|
587
637
|
agentArgs;
|
|
588
638
|
title;
|
|
639
|
+
// Snapshot state delivered to attaching clients via the attach
|
|
640
|
+
// response _meta rather than via history replay (which would be
|
|
641
|
+
// stale-prone for snapshot-shaped events).
|
|
642
|
+
currentModel;
|
|
643
|
+
currentMode;
|
|
589
644
|
updatedAt;
|
|
590
645
|
clients = /* @__PURE__ */ new Map();
|
|
591
646
|
history = [];
|
|
647
|
+
historyStore;
|
|
592
648
|
promptQueue = [];
|
|
593
649
|
promptInFlight = false;
|
|
594
650
|
closed = false;
|
|
595
651
|
closeHandlers = [];
|
|
596
652
|
titleHandlers = [];
|
|
653
|
+
// Subscribers notified after every entry that's actually persisted to
|
|
654
|
+
// history (skipping snapshot-shaped events filtered by
|
|
655
|
+
// recordAndBroadcast). The HTTP /v1/sessions/:id/history?follow=1
|
|
656
|
+
// endpoint uses this to tail a live session's conversation stream
|
|
657
|
+
// without participating in turns or prompts.
|
|
658
|
+
broadcastHandlers = [];
|
|
597
659
|
// True once we've observed our first session/prompt; gates the
|
|
598
660
|
// first-prompt-seeded title so subsequent prompts don't churn it.
|
|
599
661
|
firstPromptSeeded = false;
|
|
@@ -613,12 +675,18 @@ var init_session = __esm({
|
|
|
613
675
|
idleTimer;
|
|
614
676
|
spawnReplacementAgent;
|
|
615
677
|
agentChangeHandlers = [];
|
|
616
|
-
// Last available_commands_update we observed from the agent. Stored
|
|
617
|
-
// we can re-broadcast a merged (hydra ∪ agent) list whenever
|
|
618
|
-
// half changes
|
|
619
|
-
//
|
|
620
|
-
//
|
|
678
|
+
// Last available_commands_update we observed from the agent. Stored
|
|
679
|
+
// so we can re-broadcast a merged (hydra ∪ agent) list whenever
|
|
680
|
+
// either half changes, and persisted to meta.json so a fresh attach
|
|
681
|
+
// can deliver the merged list via _meta without depending on history
|
|
682
|
+
// replay.
|
|
621
683
|
agentAdvertisedCommands = [];
|
|
684
|
+
// Persist hooks for snapshot-shaped state. SessionManager hooks these
|
|
685
|
+
// to mirror changes into meta.json so cold-resurrect attaches can
|
|
686
|
+
// surface the latest snapshot via the attach response _meta.
|
|
687
|
+
agentCommandsHandlers = [];
|
|
688
|
+
modelHandlers = [];
|
|
689
|
+
modeHandlers = [];
|
|
622
690
|
constructor(init) {
|
|
623
691
|
this.sessionId = init.sessionId ?? `${HYDRA_SESSION_PREFIX}${generateHydraId()}`;
|
|
624
692
|
this.cwd = init.cwd;
|
|
@@ -628,11 +696,19 @@ var init_session = __esm({
|
|
|
628
696
|
this.agentMeta = init.agentMeta;
|
|
629
697
|
this.agentArgs = init.agentArgs;
|
|
630
698
|
this.title = init.title;
|
|
699
|
+
this.currentModel = init.currentModel;
|
|
700
|
+
this.currentMode = init.currentMode;
|
|
701
|
+
if (init.agentCommands && init.agentCommands.length > 0) {
|
|
702
|
+
this.agentAdvertisedCommands = [...init.agentCommands];
|
|
703
|
+
}
|
|
631
704
|
this.idleTimeoutMs = init.idleTimeoutMs ?? 0;
|
|
632
705
|
this.spawnReplacementAgent = init.spawnReplacementAgent;
|
|
706
|
+
this.historyStore = init.historyStore;
|
|
707
|
+
if (init.seedHistory && init.seedHistory.length > 0) {
|
|
708
|
+
this.history = [...init.seedHistory];
|
|
709
|
+
}
|
|
633
710
|
this.updatedAt = Date.now();
|
|
634
711
|
this.wireAgent(this.agent);
|
|
635
|
-
this.broadcastMergedCommands();
|
|
636
712
|
}
|
|
637
713
|
broadcastMergedCommands() {
|
|
638
714
|
const merged = [
|
|
@@ -661,8 +737,15 @@ var init_session = __esm({
|
|
|
661
737
|
}
|
|
662
738
|
const agentCmds = extractAdvertisedCommands(params);
|
|
663
739
|
if (agentCmds !== null) {
|
|
664
|
-
this.
|
|
665
|
-
|
|
740
|
+
this.setAgentAdvertisedCommands(agentCmds);
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
if (this.maybeApplyAgentModel(params)) {
|
|
744
|
+
this.recordAndBroadcast("session/update", params);
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
if (this.maybeApplyAgentMode(params)) {
|
|
748
|
+
this.recordAndBroadcast("session/update", params);
|
|
666
749
|
return;
|
|
667
750
|
}
|
|
668
751
|
this.maybeApplyAgentSessionInfo(params);
|
|
@@ -684,6 +767,27 @@ var init_session = __esm({
|
|
|
684
767
|
get attachedCount() {
|
|
685
768
|
return this.clients.size;
|
|
686
769
|
}
|
|
770
|
+
// Snapshot of the current in-memory replay history. Used by the
|
|
771
|
+
// HTTP history endpoint to deliver the "what's accumulated so far"
|
|
772
|
+
// prefix before optionally tailing with onBroadcast. Returns a copy
|
|
773
|
+
// so callers can't mutate our cache.
|
|
774
|
+
getHistorySnapshot() {
|
|
775
|
+
return [...this.history];
|
|
776
|
+
}
|
|
777
|
+
// Subscribe to recordable broadcast entries — fires once per entry
|
|
778
|
+
// that lands in history (so snapshot-shaped session_info/model/mode/
|
|
779
|
+
// available_commands updates do NOT trigger this; they're broadcast
|
|
780
|
+
// live but not recorded). Returns an unsubscribe function the caller
|
|
781
|
+
// must invoke when done.
|
|
782
|
+
onBroadcast(handler) {
|
|
783
|
+
this.broadcastHandlers.push(handler);
|
|
784
|
+
return () => {
|
|
785
|
+
const i = this.broadcastHandlers.indexOf(handler);
|
|
786
|
+
if (i >= 0) {
|
|
787
|
+
this.broadcastHandlers.splice(i, 1);
|
|
788
|
+
}
|
|
789
|
+
};
|
|
790
|
+
}
|
|
687
791
|
attach(client, historyPolicy) {
|
|
688
792
|
if (this.closed) {
|
|
689
793
|
throw withCode(
|
|
@@ -889,6 +993,91 @@ var init_session = __esm({
|
|
|
889
993
|
this.firstPromptSeeded = true;
|
|
890
994
|
this.setTitle(seed);
|
|
891
995
|
}
|
|
996
|
+
// Apply an agent-emitted current_model_update. Returns true if the
|
|
997
|
+
// notification was a model update (caller still needs to broadcast
|
|
998
|
+
// it). Returns false otherwise so the caller can try the next kind.
|
|
999
|
+
maybeApplyAgentModel(params) {
|
|
1000
|
+
const obj = params ?? {};
|
|
1001
|
+
const update = obj.update ?? {};
|
|
1002
|
+
if (update.sessionUpdate !== "current_model_update") {
|
|
1003
|
+
return false;
|
|
1004
|
+
}
|
|
1005
|
+
const raw = typeof update.currentModel === "string" ? update.currentModel : typeof update.model === "string" ? update.model : void 0;
|
|
1006
|
+
if (raw === void 0) {
|
|
1007
|
+
return true;
|
|
1008
|
+
}
|
|
1009
|
+
const trimmed = raw.trim();
|
|
1010
|
+
if (!trimmed || trimmed === this.currentModel) {
|
|
1011
|
+
return true;
|
|
1012
|
+
}
|
|
1013
|
+
this.currentModel = trimmed;
|
|
1014
|
+
for (const handler of this.modelHandlers) {
|
|
1015
|
+
try {
|
|
1016
|
+
handler(trimmed);
|
|
1017
|
+
} catch {
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
return true;
|
|
1021
|
+
}
|
|
1022
|
+
maybeApplyAgentMode(params) {
|
|
1023
|
+
const obj = params ?? {};
|
|
1024
|
+
const update = obj.update ?? {};
|
|
1025
|
+
if (update.sessionUpdate !== "current_mode_update") {
|
|
1026
|
+
return false;
|
|
1027
|
+
}
|
|
1028
|
+
const raw = typeof update.currentMode === "string" ? update.currentMode : typeof update.mode === "string" ? update.mode : void 0;
|
|
1029
|
+
if (raw === void 0) {
|
|
1030
|
+
return true;
|
|
1031
|
+
}
|
|
1032
|
+
const trimmed = raw.trim();
|
|
1033
|
+
if (!trimmed || trimmed === this.currentMode) {
|
|
1034
|
+
return true;
|
|
1035
|
+
}
|
|
1036
|
+
this.currentMode = trimmed;
|
|
1037
|
+
for (const handler of this.modeHandlers) {
|
|
1038
|
+
try {
|
|
1039
|
+
handler(trimmed);
|
|
1040
|
+
} catch {
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
return true;
|
|
1044
|
+
}
|
|
1045
|
+
// Update the cached agent command list, fire persist handlers, and
|
|
1046
|
+
// broadcast the merged list to attached clients. Idempotent on a
|
|
1047
|
+
// structurally identical list so we don't churn meta.json on noisy
|
|
1048
|
+
// re-emissions.
|
|
1049
|
+
setAgentAdvertisedCommands(commands) {
|
|
1050
|
+
if (sameAdvertisedCommands(this.agentAdvertisedCommands, commands)) {
|
|
1051
|
+
this.broadcastMergedCommands();
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
this.agentAdvertisedCommands = commands;
|
|
1055
|
+
for (const handler of this.agentCommandsHandlers) {
|
|
1056
|
+
try {
|
|
1057
|
+
handler(commands);
|
|
1058
|
+
} catch {
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
this.broadcastMergedCommands();
|
|
1062
|
+
}
|
|
1063
|
+
// Subscribe to snapshot-state updates. SessionManager wires these to
|
|
1064
|
+
// persist the new value into meta.json so cold resurrect can restore
|
|
1065
|
+
// them via the attach response _meta.
|
|
1066
|
+
onAgentCommandsChange(handler) {
|
|
1067
|
+
this.agentCommandsHandlers.push(handler);
|
|
1068
|
+
}
|
|
1069
|
+
onModelChange(handler) {
|
|
1070
|
+
this.modelHandlers.push(handler);
|
|
1071
|
+
}
|
|
1072
|
+
onModeChange(handler) {
|
|
1073
|
+
this.modeHandlers.push(handler);
|
|
1074
|
+
}
|
|
1075
|
+
// Returns a freshly merged command list (hydra ∪ agent) for callers
|
|
1076
|
+
// that need a snapshot — notably acp-ws.ts's buildResponseMeta when
|
|
1077
|
+
// assembling the attach response.
|
|
1078
|
+
mergedAvailableCommands() {
|
|
1079
|
+
return [...hydraCommandsAsAdvertised(), ...this.agentAdvertisedCommands];
|
|
1080
|
+
}
|
|
892
1081
|
// Pick up an agent-emitted session_info_update and store its title
|
|
893
1082
|
// as our canonical record. The notification is also forwarded to
|
|
894
1083
|
// clients via the surrounding recordAndBroadcast call. Authoritative
|
|
@@ -1199,9 +1388,34 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
1199
1388
|
}
|
|
1200
1389
|
recordAndBroadcast(method, params, excludeClientId) {
|
|
1201
1390
|
const rewritten = this.rewriteForClient(params);
|
|
1202
|
-
|
|
1203
|
-
if (
|
|
1204
|
-
|
|
1391
|
+
const recordable = !isStateUpdate(method, rewritten);
|
|
1392
|
+
if (recordable) {
|
|
1393
|
+
const entry = {
|
|
1394
|
+
method,
|
|
1395
|
+
params: rewritten,
|
|
1396
|
+
recordedAt: Date.now()
|
|
1397
|
+
};
|
|
1398
|
+
this.history.push(entry);
|
|
1399
|
+
let trimmed = false;
|
|
1400
|
+
if (this.history.length > 1e3) {
|
|
1401
|
+
this.history = this.history.slice(-500);
|
|
1402
|
+
trimmed = true;
|
|
1403
|
+
}
|
|
1404
|
+
if (this.historyStore) {
|
|
1405
|
+
if (trimmed) {
|
|
1406
|
+
void this.historyStore.rewrite(this.sessionId, [...this.history]).catch(() => void 0);
|
|
1407
|
+
} else {
|
|
1408
|
+
void this.historyStore.append(this.sessionId, entry).catch(
|
|
1409
|
+
() => void 0
|
|
1410
|
+
);
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
for (const handler of this.broadcastHandlers) {
|
|
1414
|
+
try {
|
|
1415
|
+
handler(entry);
|
|
1416
|
+
} catch {
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1205
1419
|
}
|
|
1206
1420
|
this.updatedAt = Date.now();
|
|
1207
1421
|
for (const client of this.clients.values()) {
|
|
@@ -1297,6 +1511,12 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
1297
1511
|
}
|
|
1298
1512
|
}
|
|
1299
1513
|
};
|
|
1514
|
+
STATE_UPDATE_KINDS = /* @__PURE__ */ new Set([
|
|
1515
|
+
"session_info_update",
|
|
1516
|
+
"current_model_update",
|
|
1517
|
+
"current_mode_update",
|
|
1518
|
+
"available_commands_update"
|
|
1519
|
+
]);
|
|
1300
1520
|
}
|
|
1301
1521
|
});
|
|
1302
1522
|
|
|
@@ -1552,13 +1772,248 @@ var init_sessions = __esm({
|
|
|
1552
1772
|
}
|
|
1553
1773
|
});
|
|
1554
1774
|
|
|
1775
|
+
// src/shim/resilient-ws.ts
|
|
1776
|
+
import { setTimeout as sleep3 } from "timers/promises";
|
|
1777
|
+
import { WebSocket } from "ws";
|
|
1778
|
+
function isResponse(msg) {
|
|
1779
|
+
return !("method" in msg) && "id" in msg && msg.id !== void 0;
|
|
1780
|
+
}
|
|
1781
|
+
async function openWs(url, subprotocols) {
|
|
1782
|
+
return new Promise((resolve2, reject) => {
|
|
1783
|
+
const ws = new WebSocket(url, subprotocols);
|
|
1784
|
+
const onOpen = () => {
|
|
1785
|
+
ws.off("error", onError);
|
|
1786
|
+
resolve2(wsToMessageStream(ws));
|
|
1787
|
+
};
|
|
1788
|
+
const onError = (err) => {
|
|
1789
|
+
ws.off("open", onOpen);
|
|
1790
|
+
reject(err);
|
|
1791
|
+
};
|
|
1792
|
+
ws.once("open", onOpen);
|
|
1793
|
+
ws.once("error", onError);
|
|
1794
|
+
});
|
|
1795
|
+
}
|
|
1796
|
+
var BACKOFF_INITIAL_MS, BACKOFF_MAX_MS, BACKOFF_MULTIPLIER, MAX_RECONNECT_ATTEMPTS, ResilientWsStream;
|
|
1797
|
+
var init_resilient_ws = __esm({
|
|
1798
|
+
"src/shim/resilient-ws.ts"() {
|
|
1799
|
+
"use strict";
|
|
1800
|
+
init_ws_stream();
|
|
1801
|
+
init_types();
|
|
1802
|
+
BACKOFF_INITIAL_MS = 200;
|
|
1803
|
+
BACKOFF_MAX_MS = 5e3;
|
|
1804
|
+
BACKOFF_MULTIPLIER = 2;
|
|
1805
|
+
MAX_RECONNECT_ATTEMPTS = 60;
|
|
1806
|
+
ResilientWsStream = class {
|
|
1807
|
+
constructor(opts) {
|
|
1808
|
+
this.opts = opts;
|
|
1809
|
+
}
|
|
1810
|
+
opts;
|
|
1811
|
+
current;
|
|
1812
|
+
outboundQueue = [];
|
|
1813
|
+
messageHandlers = [];
|
|
1814
|
+
closeHandlers = [];
|
|
1815
|
+
destroyed = false;
|
|
1816
|
+
firstConnect = true;
|
|
1817
|
+
reconnectInFlight;
|
|
1818
|
+
connectGate;
|
|
1819
|
+
releaseConnectGate;
|
|
1820
|
+
pendingRequests = /* @__PURE__ */ new Map();
|
|
1821
|
+
async start() {
|
|
1822
|
+
await this.connectWithRetry();
|
|
1823
|
+
}
|
|
1824
|
+
onMessage(handler) {
|
|
1825
|
+
this.messageHandlers.push(handler);
|
|
1826
|
+
}
|
|
1827
|
+
onClose(handler) {
|
|
1828
|
+
this.closeHandlers.push(handler);
|
|
1829
|
+
}
|
|
1830
|
+
async send(message) {
|
|
1831
|
+
if (this.destroyed) {
|
|
1832
|
+
throw new Error("resilient ws stream is destroyed");
|
|
1833
|
+
}
|
|
1834
|
+
if (this.connectGate || !this.current) {
|
|
1835
|
+
this.outboundQueue.push(message);
|
|
1836
|
+
return;
|
|
1837
|
+
}
|
|
1838
|
+
try {
|
|
1839
|
+
await this.current.send(message);
|
|
1840
|
+
} catch (err) {
|
|
1841
|
+
this.outboundQueue.push(message);
|
|
1842
|
+
this.scheduleReconnect(err);
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
// Send a request directly and resolve when the matching response arrives
|
|
1846
|
+
// on the same connection. Used by onConnect handlers to await replay-attach
|
|
1847
|
+
// responses before letting the outbound queue drain. Bypasses the
|
|
1848
|
+
// connectGate intentionally.
|
|
1849
|
+
async request(message) {
|
|
1850
|
+
if (this.destroyed) {
|
|
1851
|
+
throw new Error("resilient ws stream is destroyed");
|
|
1852
|
+
}
|
|
1853
|
+
if (!this.current) {
|
|
1854
|
+
throw new Error("resilient ws stream not connected");
|
|
1855
|
+
}
|
|
1856
|
+
const id = message.id;
|
|
1857
|
+
const promise = new Promise((resolve2, reject) => {
|
|
1858
|
+
this.pendingRequests.set(id, { resolve: resolve2, reject });
|
|
1859
|
+
});
|
|
1860
|
+
try {
|
|
1861
|
+
await this.current.send(message);
|
|
1862
|
+
} catch (err) {
|
|
1863
|
+
this.pendingRequests.delete(id);
|
|
1864
|
+
throw err;
|
|
1865
|
+
}
|
|
1866
|
+
return promise;
|
|
1867
|
+
}
|
|
1868
|
+
async close() {
|
|
1869
|
+
this.destroyed = true;
|
|
1870
|
+
if (this.current) {
|
|
1871
|
+
await this.current.close().catch(() => void 0);
|
|
1872
|
+
}
|
|
1873
|
+
for (const handler of this.closeHandlers) {
|
|
1874
|
+
handler();
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
async connectWithRetry() {
|
|
1878
|
+
let attempt = 0;
|
|
1879
|
+
let backoff = BACKOFF_INITIAL_MS;
|
|
1880
|
+
while (!this.destroyed) {
|
|
1881
|
+
try {
|
|
1882
|
+
const stream = await openWs(this.opts.url, this.opts.subprotocols);
|
|
1883
|
+
this.bindStream(stream);
|
|
1884
|
+
const wasFirst = this.firstConnect;
|
|
1885
|
+
this.firstConnect = false;
|
|
1886
|
+
this.connectGate = new Promise((resolve2) => {
|
|
1887
|
+
this.releaseConnectGate = resolve2;
|
|
1888
|
+
});
|
|
1889
|
+
try {
|
|
1890
|
+
if (this.opts.onConnect) {
|
|
1891
|
+
try {
|
|
1892
|
+
await this.opts.onConnect(wasFirst);
|
|
1893
|
+
} catch (err) {
|
|
1894
|
+
this.log(
|
|
1895
|
+
`hydra-acp: post-connect handler failed: ${err.message}`
|
|
1896
|
+
);
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
} finally {
|
|
1900
|
+
this.releaseConnectGate?.();
|
|
1901
|
+
this.releaseConnectGate = void 0;
|
|
1902
|
+
this.connectGate = void 0;
|
|
1903
|
+
}
|
|
1904
|
+
await this.flushQueue();
|
|
1905
|
+
return;
|
|
1906
|
+
} catch (err) {
|
|
1907
|
+
attempt += 1;
|
|
1908
|
+
if (this.opts.onConnectFailure) {
|
|
1909
|
+
this.opts.onConnectFailure(err);
|
|
1910
|
+
}
|
|
1911
|
+
if (attempt >= MAX_RECONNECT_ATTEMPTS) {
|
|
1912
|
+
throw new Error(
|
|
1913
|
+
`hydra-acp: gave up reconnecting after ${attempt} attempts: ${err.message}`
|
|
1914
|
+
);
|
|
1915
|
+
}
|
|
1916
|
+
this.log(
|
|
1917
|
+
`hydra-acp: connect attempt ${attempt} failed (${err.message}); retrying in ${backoff}ms`
|
|
1918
|
+
);
|
|
1919
|
+
await sleep3(backoff);
|
|
1920
|
+
backoff = Math.min(backoff * BACKOFF_MULTIPLIER, BACKOFF_MAX_MS);
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
bindStream(stream) {
|
|
1925
|
+
this.current = stream;
|
|
1926
|
+
stream.onMessage((msg) => {
|
|
1927
|
+
if (isResponse(msg)) {
|
|
1928
|
+
const pending = this.pendingRequests.get(msg.id);
|
|
1929
|
+
if (pending) {
|
|
1930
|
+
this.pendingRequests.delete(msg.id);
|
|
1931
|
+
pending.resolve(msg);
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
for (const handler of this.messageHandlers) {
|
|
1935
|
+
handler(msg);
|
|
1936
|
+
}
|
|
1937
|
+
});
|
|
1938
|
+
stream.onClose((err) => {
|
|
1939
|
+
if (this.destroyed) {
|
|
1940
|
+
return;
|
|
1941
|
+
}
|
|
1942
|
+
this.current = void 0;
|
|
1943
|
+
if (this.pendingRequests.size > 0) {
|
|
1944
|
+
const reason = err ?? new Error("ws closed before response");
|
|
1945
|
+
for (const { reject } of this.pendingRequests.values()) {
|
|
1946
|
+
reject(reason);
|
|
1947
|
+
}
|
|
1948
|
+
this.pendingRequests.clear();
|
|
1949
|
+
}
|
|
1950
|
+
this.scheduleReconnect(err);
|
|
1951
|
+
});
|
|
1952
|
+
}
|
|
1953
|
+
async flushQueue() {
|
|
1954
|
+
if (!this.current) {
|
|
1955
|
+
return;
|
|
1956
|
+
}
|
|
1957
|
+
const queue = this.outboundQueue;
|
|
1958
|
+
this.outboundQueue = [];
|
|
1959
|
+
for (const msg of queue) {
|
|
1960
|
+
try {
|
|
1961
|
+
await this.current.send(msg);
|
|
1962
|
+
} catch (err) {
|
|
1963
|
+
this.outboundQueue.unshift(msg);
|
|
1964
|
+
this.scheduleReconnect(err);
|
|
1965
|
+
return;
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
scheduleReconnect(err) {
|
|
1970
|
+
if (this.destroyed || this.reconnectInFlight) {
|
|
1971
|
+
return;
|
|
1972
|
+
}
|
|
1973
|
+
this.log(
|
|
1974
|
+
`hydra-acp: connection lost (${err?.message ?? "no error"}); reconnecting...`
|
|
1975
|
+
);
|
|
1976
|
+
if (this.opts.onDisconnect) {
|
|
1977
|
+
try {
|
|
1978
|
+
this.opts.onDisconnect(err);
|
|
1979
|
+
} catch (hookErr) {
|
|
1980
|
+
this.log(
|
|
1981
|
+
`hydra-acp: onDisconnect handler threw: ${hookErr.message}`
|
|
1982
|
+
);
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
this.reconnectInFlight = (async () => {
|
|
1986
|
+
try {
|
|
1987
|
+
await this.connectWithRetry();
|
|
1988
|
+
} catch (final) {
|
|
1989
|
+
for (const handler of this.closeHandlers) {
|
|
1990
|
+
handler(final);
|
|
1991
|
+
}
|
|
1992
|
+
this.destroyed = true;
|
|
1993
|
+
} finally {
|
|
1994
|
+
this.reconnectInFlight = void 0;
|
|
1995
|
+
}
|
|
1996
|
+
})();
|
|
1997
|
+
}
|
|
1998
|
+
log(line) {
|
|
1999
|
+
if (this.opts.log) {
|
|
2000
|
+
this.opts.log(line);
|
|
2001
|
+
return;
|
|
2002
|
+
}
|
|
2003
|
+
process.stderr.write(`${line}
|
|
2004
|
+
`);
|
|
2005
|
+
}
|
|
2006
|
+
};
|
|
2007
|
+
}
|
|
2008
|
+
});
|
|
2009
|
+
|
|
1555
2010
|
// src/tui/history.ts
|
|
1556
|
-
import { promises as
|
|
2011
|
+
import { promises as fs9 } from "fs";
|
|
1557
2012
|
import * as path4 from "path";
|
|
1558
2013
|
async function loadHistory(file) {
|
|
1559
2014
|
let text;
|
|
1560
2015
|
try {
|
|
1561
|
-
text = await
|
|
2016
|
+
text = await fs9.readFile(file, "utf8");
|
|
1562
2017
|
} catch (err) {
|
|
1563
2018
|
if (err.code === "ENOENT") {
|
|
1564
2019
|
return [];
|
|
@@ -1598,9 +2053,9 @@ function appendEntry(history, entry) {
|
|
|
1598
2053
|
return out;
|
|
1599
2054
|
}
|
|
1600
2055
|
async function saveHistory(file, history) {
|
|
1601
|
-
await
|
|
2056
|
+
await fs9.mkdir(path4.dirname(file), { recursive: true });
|
|
1602
2057
|
const lines = history.map((entry) => JSON.stringify(entry));
|
|
1603
|
-
await
|
|
2058
|
+
await fs9.writeFile(file, lines.length > 0 ? lines.join("\n") + "\n" : "");
|
|
1604
2059
|
}
|
|
1605
2060
|
var HISTORY_CAP;
|
|
1606
2061
|
var init_history = __esm({
|
|
@@ -1807,6 +2262,8 @@ var init_picker = __esm({
|
|
|
1807
2262
|
});
|
|
1808
2263
|
|
|
1809
2264
|
// src/tui/screen.ts
|
|
2265
|
+
import stringWidth from "string-width";
|
|
2266
|
+
import wrapAnsi from "wrap-ansi";
|
|
1810
2267
|
function computePromptVisualRows(buffer, room) {
|
|
1811
2268
|
const rows = [];
|
|
1812
2269
|
for (let i = 0; i < buffer.length; i++) {
|
|
@@ -1933,6 +2390,15 @@ function writeStyled(term, text, style) {
|
|
|
1933
2390
|
term.noFormat(text);
|
|
1934
2391
|
}
|
|
1935
2392
|
}
|
|
2393
|
+
function wrapAnsiBody(text, width) {
|
|
2394
|
+
if (width <= 0) {
|
|
2395
|
+
return [text];
|
|
2396
|
+
}
|
|
2397
|
+
if (text.length === 0) {
|
|
2398
|
+
return [""];
|
|
2399
|
+
}
|
|
2400
|
+
return wrapAnsi(text, width, { hard: true, trim: false }).split("\n");
|
|
2401
|
+
}
|
|
1936
2402
|
function wrap(text, width) {
|
|
1937
2403
|
if (width <= 0) {
|
|
1938
2404
|
return [text];
|
|
@@ -2611,8 +3077,24 @@ var init_screen = __esm({
|
|
|
2611
3077
|
const w = this.term.width;
|
|
2612
3078
|
this.term.moveTo(1, 1).eraseLineAfter();
|
|
2613
3079
|
const usage = formatUsage(this.header.usage);
|
|
2614
|
-
const
|
|
2615
|
-
|
|
3080
|
+
const sid = shortId(this.header.sessionId);
|
|
3081
|
+
const title = this.header.title?.trim();
|
|
3082
|
+
const fixed = "hydra \xB7 ".length + this.header.agent.length + " \xB7 ".length + " \xB7 ".length + sid.length + (title ? " \xB7 ".length : 0) + (usage ? usage.length + 3 : 0);
|
|
3083
|
+
const variableRoom = Math.max(8, w - fixed);
|
|
3084
|
+
let cwdRoom;
|
|
3085
|
+
let titleRoom;
|
|
3086
|
+
if (title) {
|
|
3087
|
+
const cwdCap = Math.max(8, Math.floor(variableRoom / 2));
|
|
3088
|
+
cwdRoom = Math.min(this.header.cwd.length, cwdCap);
|
|
3089
|
+
titleRoom = Math.max(8, variableRoom - cwdRoom);
|
|
3090
|
+
} else {
|
|
3091
|
+
titleRoom = 0;
|
|
3092
|
+
cwdRoom = variableRoom;
|
|
3093
|
+
}
|
|
3094
|
+
this.term.bold("hydra")(" \xB7 ").cyan(this.header.agent)(" \xB7 ").dim(truncate(this.header.cwd, cwdRoom))(" \xB7 ").yellow(sid);
|
|
3095
|
+
if (title) {
|
|
3096
|
+
this.term(" \xB7 ").bold(truncate(title, titleRoom));
|
|
3097
|
+
}
|
|
2616
3098
|
if (usage) {
|
|
2617
3099
|
const col = Math.max(1, w - usage.length + 1);
|
|
2618
3100
|
this.term.moveTo(col, 1);
|
|
@@ -2829,6 +3311,8 @@ var init_screen = __esm({
|
|
|
2829
3311
|
if (this.banner.elapsedMs !== void 0 && this.banner.elapsedMs >= 1e3) {
|
|
2830
3312
|
this.term(" ").dim(formatElapsed(this.banner.elapsedMs));
|
|
2831
3313
|
}
|
|
3314
|
+
} else if (this.banner.status === "disconnected") {
|
|
3315
|
+
this.term.brightRed(`${dot} ${this.banner.status}`);
|
|
2832
3316
|
} else {
|
|
2833
3317
|
this.term.brightGreen(`${dot} ${this.banner.status}`);
|
|
2834
3318
|
}
|
|
@@ -2902,7 +3386,7 @@ var init_screen = __esm({
|
|
|
2902
3386
|
for (const line of lines) {
|
|
2903
3387
|
const prefix = line.prefix ?? "";
|
|
2904
3388
|
const room = Math.max(1, width - prefix.length);
|
|
2905
|
-
const chunks = wrap(line.body, room);
|
|
3389
|
+
const chunks = line.ansi ? wrapAnsiBody(line.body, room) : wrap(line.body, room);
|
|
2906
3390
|
for (let i = 0; i < chunks.length; i++) {
|
|
2907
3391
|
const chunk = chunks[i] ?? "";
|
|
2908
3392
|
const wrappedLine = {
|
|
@@ -2918,6 +3402,9 @@ var init_screen = __esm({
|
|
|
2918
3402
|
if (line.fillRow) {
|
|
2919
3403
|
wrappedLine.fillRow = true;
|
|
2920
3404
|
}
|
|
3405
|
+
if (line.ansi) {
|
|
3406
|
+
wrappedLine.ansi = true;
|
|
3407
|
+
}
|
|
2921
3408
|
out.push(wrappedLine);
|
|
2922
3409
|
}
|
|
2923
3410
|
}
|
|
@@ -2928,15 +3415,16 @@ var init_screen = __esm({
|
|
|
2928
3415
|
writeStyled(this.term, line.prefix, line.prefixStyle ?? line.bodyStyle);
|
|
2929
3416
|
}
|
|
2930
3417
|
const remaining = Math.max(0, width - (line.prefix?.length ?? 0));
|
|
2931
|
-
const bodyText = truncate(line.body, remaining);
|
|
3418
|
+
const bodyText = line.ansi ? line.body : truncate(line.body, remaining);
|
|
2932
3419
|
writeStyled(this.term, bodyText, line.bodyStyle);
|
|
2933
3420
|
if (line.fillRow) {
|
|
2934
|
-
const
|
|
3421
|
+
const visible = line.ansi ? stringWidth(bodyText) : bodyText.length;
|
|
3422
|
+
const pad = remaining - visible;
|
|
2935
3423
|
if (pad > 0) {
|
|
2936
3424
|
writeStyled(this.term, " ".repeat(pad), line.bodyStyle);
|
|
2937
3425
|
}
|
|
2938
3426
|
}
|
|
2939
|
-
if (line.body.includes("^")) {
|
|
3427
|
+
if (line.ansi || line.body.includes("^")) {
|
|
2940
3428
|
this.term.styleReset();
|
|
2941
3429
|
}
|
|
2942
3430
|
}
|
|
@@ -3325,10 +3813,19 @@ function mapUpdate(update) {
|
|
|
3325
3813
|
return mapUsage(u);
|
|
3326
3814
|
case "available_commands_update":
|
|
3327
3815
|
return mapAvailableCommands(u);
|
|
3816
|
+
case "session_info_update":
|
|
3817
|
+
return mapSessionInfo(u);
|
|
3328
3818
|
default:
|
|
3329
3819
|
return { kind: "unknown", sessionUpdate: tag, raw: update };
|
|
3330
3820
|
}
|
|
3331
3821
|
}
|
|
3822
|
+
function mapSessionInfo(u) {
|
|
3823
|
+
const title = readString(u, "title");
|
|
3824
|
+
if (title === void 0) {
|
|
3825
|
+
return null;
|
|
3826
|
+
}
|
|
3827
|
+
return { kind: "session-info", title };
|
|
3828
|
+
}
|
|
3332
3829
|
function mapAvailableCommands(u) {
|
|
3333
3830
|
const list = u.availableCommands ?? u.commands;
|
|
3334
3831
|
if (!Array.isArray(list)) {
|
|
@@ -3530,6 +4027,8 @@ var init_render_update = __esm({
|
|
|
3530
4027
|
});
|
|
3531
4028
|
|
|
3532
4029
|
// src/tui/format.ts
|
|
4030
|
+
import chalk from "chalk";
|
|
4031
|
+
import { highlight, supportsLanguage } from "cli-highlight";
|
|
3533
4032
|
function formatEvent(event) {
|
|
3534
4033
|
switch (event.kind) {
|
|
3535
4034
|
case "user-text":
|
|
@@ -3567,6 +4066,8 @@ function formatEvent(event) {
|
|
|
3567
4066
|
return [];
|
|
3568
4067
|
case "available-commands":
|
|
3569
4068
|
return [];
|
|
4069
|
+
case "session-info":
|
|
4070
|
+
return [];
|
|
3570
4071
|
case "unknown":
|
|
3571
4072
|
return [];
|
|
3572
4073
|
}
|
|
@@ -3581,19 +4082,42 @@ function parseAgentMarkdown(text) {
|
|
|
3581
4082
|
const out = [];
|
|
3582
4083
|
const lines = text.split("\n");
|
|
3583
4084
|
let inCode = false;
|
|
4085
|
+
let codeLang = "";
|
|
4086
|
+
let codeBuffer = [];
|
|
4087
|
+
const flushCode = () => {
|
|
4088
|
+
if (codeBuffer.length === 0) {
|
|
4089
|
+
return;
|
|
4090
|
+
}
|
|
4091
|
+
const highlighted = highlightFencedBlock(codeLang, codeBuffer);
|
|
4092
|
+
for (const piece of highlighted) {
|
|
4093
|
+
const entry = {
|
|
4094
|
+
prefix: " ",
|
|
4095
|
+
body: piece.body,
|
|
4096
|
+
bodyStyle: "code",
|
|
4097
|
+
fillRow: true
|
|
4098
|
+
};
|
|
4099
|
+
if (piece.ansi) {
|
|
4100
|
+
entry.ansi = true;
|
|
4101
|
+
}
|
|
4102
|
+
out.push(entry);
|
|
4103
|
+
}
|
|
4104
|
+
codeBuffer = [];
|
|
4105
|
+
codeLang = "";
|
|
4106
|
+
};
|
|
3584
4107
|
for (const line of lines) {
|
|
3585
|
-
const fence = line.match(/^\s*```\s
|
|
4108
|
+
const fence = line.match(/^\s*```\s*(\w*)\s*$/);
|
|
3586
4109
|
if (fence) {
|
|
3587
|
-
|
|
4110
|
+
if (!inCode) {
|
|
4111
|
+
inCode = true;
|
|
4112
|
+
codeLang = fence[1] ?? "";
|
|
4113
|
+
} else {
|
|
4114
|
+
flushCode();
|
|
4115
|
+
inCode = false;
|
|
4116
|
+
}
|
|
3588
4117
|
continue;
|
|
3589
4118
|
}
|
|
3590
4119
|
if (inCode) {
|
|
3591
|
-
|
|
3592
|
-
prefix: " ",
|
|
3593
|
-
body: line,
|
|
3594
|
-
bodyStyle: "code",
|
|
3595
|
-
fillRow: true
|
|
3596
|
-
});
|
|
4120
|
+
codeBuffer.push(line);
|
|
3597
4121
|
continue;
|
|
3598
4122
|
}
|
|
3599
4123
|
const heading = line.match(/^(#{1,6})\s+(.*)$/);
|
|
@@ -3637,8 +4161,34 @@ function parseAgentMarkdown(text) {
|
|
|
3637
4161
|
bodyStyle: "agent"
|
|
3638
4162
|
});
|
|
3639
4163
|
}
|
|
4164
|
+
if (inCode) {
|
|
4165
|
+
flushCode();
|
|
4166
|
+
}
|
|
3640
4167
|
return out;
|
|
3641
4168
|
}
|
|
4169
|
+
function highlightFencedBlock(lang, lines) {
|
|
4170
|
+
if (lang.length === 0 || !supportsLanguage(lang)) {
|
|
4171
|
+
return lines.map((body) => ({ body, ansi: false }));
|
|
4172
|
+
}
|
|
4173
|
+
let highlighted;
|
|
4174
|
+
try {
|
|
4175
|
+
highlighted = highlight(lines.join("\n"), {
|
|
4176
|
+
language: lang,
|
|
4177
|
+
theme: HIGHLIGHT_THEME,
|
|
4178
|
+
ignoreIllegals: true
|
|
4179
|
+
});
|
|
4180
|
+
} catch {
|
|
4181
|
+
return lines.map((body) => ({ body, ansi: false }));
|
|
4182
|
+
}
|
|
4183
|
+
const out = highlighted.split("\n");
|
|
4184
|
+
if (out.length !== lines.length) {
|
|
4185
|
+
return lines.map((body) => ({ body, ansi: false }));
|
|
4186
|
+
}
|
|
4187
|
+
return out.map((body, i) => ({
|
|
4188
|
+
body,
|
|
4189
|
+
ansi: body !== lines[i]
|
|
4190
|
+
}));
|
|
4191
|
+
}
|
|
3642
4192
|
function formatBlock(text, prefix, bodyStyle, prefixStyle, sentBy, fillRow) {
|
|
3643
4193
|
const lines = text.split("\n");
|
|
3644
4194
|
const out = [];
|
|
@@ -3750,15 +4300,40 @@ function toolStatusStyle(status) {
|
|
|
3750
4300
|
return "tool-status-pending";
|
|
3751
4301
|
}
|
|
3752
4302
|
}
|
|
4303
|
+
var highlightChalk, HIGHLIGHT_THEME;
|
|
3753
4304
|
var init_format = __esm({
|
|
3754
4305
|
"src/tui/format.ts"() {
|
|
3755
4306
|
"use strict";
|
|
4307
|
+
highlightChalk = new chalk.Instance({ level: 3 });
|
|
4308
|
+
HIGHLIGHT_THEME = {
|
|
4309
|
+
keyword: highlightChalk.blueBright,
|
|
4310
|
+
built_in: highlightChalk.cyan,
|
|
4311
|
+
type: highlightChalk.cyanBright,
|
|
4312
|
+
literal: highlightChalk.blue,
|
|
4313
|
+
number: highlightChalk.greenBright,
|
|
4314
|
+
string: highlightChalk.yellow,
|
|
4315
|
+
regexp: highlightChalk.red,
|
|
4316
|
+
comment: highlightChalk.gray,
|
|
4317
|
+
function: highlightChalk.yellow,
|
|
4318
|
+
title: highlightChalk.yellow,
|
|
4319
|
+
class: highlightChalk.yellowBright,
|
|
4320
|
+
attr: highlightChalk.cyan,
|
|
4321
|
+
attribute: highlightChalk.cyan,
|
|
4322
|
+
variable: highlightChalk.white,
|
|
4323
|
+
params: highlightChalk.white,
|
|
4324
|
+
meta: highlightChalk.magenta,
|
|
4325
|
+
symbol: highlightChalk.magenta,
|
|
4326
|
+
addition: highlightChalk.greenBright,
|
|
4327
|
+
deletion: highlightChalk.redBright,
|
|
4328
|
+
section: highlightChalk.cyan,
|
|
4329
|
+
tag: highlightChalk.cyan,
|
|
4330
|
+
name: highlightChalk.cyanBright
|
|
4331
|
+
};
|
|
3756
4332
|
}
|
|
3757
4333
|
});
|
|
3758
4334
|
|
|
3759
4335
|
// src/tui/app.ts
|
|
3760
|
-
import
|
|
3761
|
-
import { once } from "events";
|
|
4336
|
+
import { nanoid as nanoid3 } from "nanoid";
|
|
3762
4337
|
import termkit from "terminal-kit";
|
|
3763
4338
|
async function runTuiApp(opts) {
|
|
3764
4339
|
const config = await ensureConfig();
|
|
@@ -3775,9 +4350,31 @@ async function runSession(term, config, opts) {
|
|
|
3775
4350
|
term.grabInput(false);
|
|
3776
4351
|
process.exit(0);
|
|
3777
4352
|
}
|
|
3778
|
-
const
|
|
3779
|
-
const
|
|
4353
|
+
const protocol = config.daemon.tls ? "wss" : "ws";
|
|
4354
|
+
const wsUrl = `${protocol}://${config.daemon.host}:${config.daemon.port}/acp`;
|
|
4355
|
+
const subprotocols = ["acp.v1", `hydra-acp-token.${config.daemon.authToken}`];
|
|
4356
|
+
let onReconnect = null;
|
|
4357
|
+
let onDisconnectHook = null;
|
|
4358
|
+
const stream = new ResilientWsStream({
|
|
4359
|
+
url: wsUrl,
|
|
4360
|
+
subprotocols,
|
|
4361
|
+
onConnect: async (firstConnect) => {
|
|
4362
|
+
if (firstConnect) {
|
|
4363
|
+
return;
|
|
4364
|
+
}
|
|
4365
|
+
if (onReconnect) {
|
|
4366
|
+
await onReconnect();
|
|
4367
|
+
}
|
|
4368
|
+
},
|
|
4369
|
+
onDisconnect: (err) => {
|
|
4370
|
+
if (onDisconnectHook) {
|
|
4371
|
+
onDisconnectHook(err);
|
|
4372
|
+
}
|
|
4373
|
+
},
|
|
4374
|
+
log: () => void 0
|
|
4375
|
+
});
|
|
3780
4376
|
const conn = new JsonRpcConnection(stream);
|
|
4377
|
+
await stream.start();
|
|
3781
4378
|
let bufferedEvents = [];
|
|
3782
4379
|
let applyRenderEvent = null;
|
|
3783
4380
|
const appendRender = (event) => {
|
|
@@ -3941,6 +4538,10 @@ async function runSession(term, config, opts) {
|
|
|
3941
4538
|
let resolvedSessionId = ctx.sessionId;
|
|
3942
4539
|
let resolvedAgentId = ctx.agentId;
|
|
3943
4540
|
let resolvedCwd = ctx.cwd;
|
|
4541
|
+
let resolvedTitle;
|
|
4542
|
+
let initialModel;
|
|
4543
|
+
let initialMode;
|
|
4544
|
+
let initialCommands;
|
|
3944
4545
|
if (ctx.sessionId === "__new__") {
|
|
3945
4546
|
const created = await conn.request("session/new", {
|
|
3946
4547
|
cwd: ctx.cwd,
|
|
@@ -3956,6 +4557,16 @@ async function runSession(term, config, opts) {
|
|
|
3956
4557
|
if (hydraMeta.cwd) {
|
|
3957
4558
|
resolvedCwd = hydraMeta.cwd;
|
|
3958
4559
|
}
|
|
4560
|
+
if (hydraMeta.name) {
|
|
4561
|
+
resolvedTitle = hydraMeta.name;
|
|
4562
|
+
}
|
|
4563
|
+
initialModel = hydraMeta.currentModel;
|
|
4564
|
+
initialMode = hydraMeta.currentMode;
|
|
4565
|
+
if (hydraMeta.availableCommands) {
|
|
4566
|
+
initialCommands = hydraMeta.availableCommands.map(
|
|
4567
|
+
(c) => c.description !== void 0 ? { name: c.name, description: c.description } : { name: c.name }
|
|
4568
|
+
);
|
|
4569
|
+
}
|
|
3959
4570
|
} else {
|
|
3960
4571
|
const attached = await conn.request("session/attach", {
|
|
3961
4572
|
sessionId: ctx.sessionId,
|
|
@@ -3971,8 +4582,17 @@ async function runSession(term, config, opts) {
|
|
|
3971
4582
|
if (hydraMeta.cwd) {
|
|
3972
4583
|
resolvedCwd = hydraMeta.cwd;
|
|
3973
4584
|
}
|
|
4585
|
+
if (hydraMeta.name) {
|
|
4586
|
+
resolvedTitle = hydraMeta.name;
|
|
4587
|
+
}
|
|
4588
|
+
initialModel = hydraMeta.currentModel;
|
|
4589
|
+
initialMode = hydraMeta.currentMode;
|
|
4590
|
+
if (hydraMeta.availableCommands) {
|
|
4591
|
+
initialCommands = hydraMeta.availableCommands.map(
|
|
4592
|
+
(c) => c.description !== void 0 ? { name: c.name, description: c.description } : { name: c.name }
|
|
4593
|
+
);
|
|
4594
|
+
}
|
|
3974
4595
|
}
|
|
3975
|
-
void upstreamSessionId;
|
|
3976
4596
|
const historyFile = paths.tuiHistoryFile();
|
|
3977
4597
|
let history = await loadHistory(historyFile).catch(() => []);
|
|
3978
4598
|
const dispatcher = new InputDispatcher({ history });
|
|
@@ -4010,7 +4630,7 @@ async function runSession(term, config, opts) {
|
|
|
4010
4630
|
{ name: "/demo-plan", description: "Inject synthetic plan events (UI test)" },
|
|
4011
4631
|
{ name: "/demo-tool", description: "Inject a synthetic tool-call sequence (UI test)" }
|
|
4012
4632
|
];
|
|
4013
|
-
let agentCommands = [];
|
|
4633
|
+
let agentCommands = initialCommands ?? [];
|
|
4014
4634
|
const allCommands = () => {
|
|
4015
4635
|
const seen = /* @__PURE__ */ new Set();
|
|
4016
4636
|
const out = [];
|
|
@@ -4134,8 +4754,15 @@ async function runSession(term, config, opts) {
|
|
|
4134
4754
|
screen.setHeader({
|
|
4135
4755
|
agent: headerName,
|
|
4136
4756
|
cwd: resolvedCwd,
|
|
4137
|
-
sessionId: resolvedSessionId
|
|
4757
|
+
sessionId: resolvedSessionId,
|
|
4758
|
+
title: resolvedTitle
|
|
4138
4759
|
});
|
|
4760
|
+
if (initialMode) {
|
|
4761
|
+
screen.appendLines(formatEvent({ kind: "mode-changed", mode: initialMode }));
|
|
4762
|
+
}
|
|
4763
|
+
if (initialModel) {
|
|
4764
|
+
screen.appendLines(formatEvent({ kind: "model-changed", model: initialModel }));
|
|
4765
|
+
}
|
|
4139
4766
|
let finishSession = null;
|
|
4140
4767
|
const sessionDone = new Promise((resolve2) => {
|
|
4141
4768
|
finishSession = resolve2;
|
|
@@ -4221,10 +4848,7 @@ async function runSession(term, config, opts) {
|
|
|
4221
4848
|
process.off("SIGINT", sigintHandler);
|
|
4222
4849
|
screen.stop();
|
|
4223
4850
|
saveHistory(historyFile, history).catch(() => void 0);
|
|
4224
|
-
|
|
4225
|
-
ws.close();
|
|
4226
|
-
} catch {
|
|
4227
|
-
}
|
|
4851
|
+
void stream.close().catch(() => void 0);
|
|
4228
4852
|
};
|
|
4229
4853
|
const stop = (code = 0) => {
|
|
4230
4854
|
teardown();
|
|
@@ -4617,6 +5241,12 @@ async function runSession(term, config, opts) {
|
|
|
4617
5241
|
refreshCompletions();
|
|
4618
5242
|
return;
|
|
4619
5243
|
}
|
|
5244
|
+
if (event.kind === "session-info") {
|
|
5245
|
+
if (event.title !== void 0) {
|
|
5246
|
+
screen.setHeader({ title: event.title });
|
|
5247
|
+
}
|
|
5248
|
+
return;
|
|
5249
|
+
}
|
|
4620
5250
|
if (event.kind === "usage-update") {
|
|
4621
5251
|
let changed = false;
|
|
4622
5252
|
if (event.used !== void 0 && usage.used !== event.used) {
|
|
@@ -4712,6 +5342,95 @@ async function runSession(term, config, opts) {
|
|
|
4712
5342
|
} finally {
|
|
4713
5343
|
screen.resumeRepaint();
|
|
4714
5344
|
}
|
|
5345
|
+
const resetInFlightUiState = () => {
|
|
5346
|
+
if (pendingPermission) {
|
|
5347
|
+
const resolve2 = pendingPermission.resolve;
|
|
5348
|
+
pendingPermission = null;
|
|
5349
|
+
screen.setPermissionPrompt(null);
|
|
5350
|
+
resolve2({ outcome: { outcome: "cancelled" } });
|
|
5351
|
+
}
|
|
5352
|
+
closeAgentText();
|
|
5353
|
+
if (toolsBlockStartedAt !== null) {
|
|
5354
|
+
if (toolCallOrder.length > 0) {
|
|
5355
|
+
toolsBlockEndedAt = Date.now();
|
|
5356
|
+
renderToolsBlock();
|
|
5357
|
+
screen.clearKey("tools");
|
|
5358
|
+
} else {
|
|
5359
|
+
screen.removeBlock("tools");
|
|
5360
|
+
}
|
|
5361
|
+
toolStates.clear();
|
|
5362
|
+
toolCallOrder.length = 0;
|
|
5363
|
+
toolsBlockStartedAt = null;
|
|
5364
|
+
toolsBlockEndedAt = null;
|
|
5365
|
+
toolsExpanded = false;
|
|
5366
|
+
}
|
|
5367
|
+
screen.clearKey("plan");
|
|
5368
|
+
if (pendingTurns > 0) {
|
|
5369
|
+
adjustPendingTurns(-pendingTurns);
|
|
5370
|
+
}
|
|
5371
|
+
};
|
|
5372
|
+
onDisconnectHook = () => {
|
|
5373
|
+
screen.setBanner({ status: "disconnected", elapsedMs: void 0 });
|
|
5374
|
+
};
|
|
5375
|
+
onReconnect = async () => {
|
|
5376
|
+
resetInFlightUiState();
|
|
5377
|
+
const initReq = {
|
|
5378
|
+
jsonrpc: "2.0",
|
|
5379
|
+
id: `tui-reinit-${nanoid3()}`,
|
|
5380
|
+
method: "initialize",
|
|
5381
|
+
params: {
|
|
5382
|
+
protocolVersion: 1,
|
|
5383
|
+
clientCapabilities: {
|
|
5384
|
+
fs: { readTextFile: false, writeTextFile: false },
|
|
5385
|
+
terminal: false
|
|
5386
|
+
},
|
|
5387
|
+
clientInfo: { name: "hydra-acp-tui", version: "0.1.0" }
|
|
5388
|
+
}
|
|
5389
|
+
};
|
|
5390
|
+
try {
|
|
5391
|
+
await stream.request(initReq);
|
|
5392
|
+
} catch {
|
|
5393
|
+
}
|
|
5394
|
+
const attachReq = {
|
|
5395
|
+
jsonrpc: "2.0",
|
|
5396
|
+
id: `tui-reattach-${nanoid3()}`,
|
|
5397
|
+
method: "session/attach",
|
|
5398
|
+
params: {
|
|
5399
|
+
sessionId: resolvedSessionId,
|
|
5400
|
+
historyPolicy: "none",
|
|
5401
|
+
clientInfo: { name: "hydra-acp-tui", version: "0.1.0" },
|
|
5402
|
+
...upstreamSessionId !== void 0 ? {
|
|
5403
|
+
_meta: {
|
|
5404
|
+
[HYDRA_META_KEY]: {
|
|
5405
|
+
resume: {
|
|
5406
|
+
upstreamSessionId,
|
|
5407
|
+
agentId: resolvedAgentId,
|
|
5408
|
+
cwd: resolvedCwd
|
|
5409
|
+
}
|
|
5410
|
+
}
|
|
5411
|
+
}
|
|
5412
|
+
} : {}
|
|
5413
|
+
}
|
|
5414
|
+
};
|
|
5415
|
+
try {
|
|
5416
|
+
const resp = await stream.request(attachReq);
|
|
5417
|
+
if (resp.error) {
|
|
5418
|
+
throw new Error(resp.error.message);
|
|
5419
|
+
}
|
|
5420
|
+
} catch (err) {
|
|
5421
|
+
screen.appendLines([
|
|
5422
|
+
{
|
|
5423
|
+
prefix: " ",
|
|
5424
|
+
body: `reattach failed: ${err.message}`,
|
|
5425
|
+
bodyStyle: "tool-status-fail"
|
|
5426
|
+
}
|
|
5427
|
+
]);
|
|
5428
|
+
}
|
|
5429
|
+
screen.setBanner({
|
|
5430
|
+
status: pendingTurns > 0 ? "running" : "ready",
|
|
5431
|
+
elapsedMs: pendingTurns > 0 ? 0 : void 0
|
|
5432
|
+
});
|
|
5433
|
+
};
|
|
4715
5434
|
conn.onClose((err) => {
|
|
4716
5435
|
if (err) {
|
|
4717
5436
|
term.red(`
|
|
@@ -4777,23 +5496,13 @@ function newCtx(opts, cwd, config) {
|
|
|
4777
5496
|
cwd
|
|
4778
5497
|
};
|
|
4779
5498
|
}
|
|
4780
|
-
async function openWs2(config) {
|
|
4781
|
-
const protocol = config.daemon.tls ? "wss" : "ws";
|
|
4782
|
-
const url = `${protocol}://${config.daemon.host}:${config.daemon.port}/acp`;
|
|
4783
|
-
const ws = new WebSocket2(url, [
|
|
4784
|
-
"acp.v1",
|
|
4785
|
-
`hydra-acp-token.${config.daemon.authToken}`
|
|
4786
|
-
]);
|
|
4787
|
-
await once(ws, "open");
|
|
4788
|
-
return ws;
|
|
4789
|
-
}
|
|
4790
5499
|
var PLAN_PREFIX_TEXT;
|
|
4791
5500
|
var init_app = __esm({
|
|
4792
5501
|
"src/tui/app.ts"() {
|
|
4793
5502
|
"use strict";
|
|
4794
5503
|
init_connection();
|
|
4795
|
-
init_ws_stream();
|
|
4796
5504
|
init_types();
|
|
5505
|
+
init_resilient_ws();
|
|
4797
5506
|
init_config();
|
|
4798
5507
|
init_daemon_bootstrap();
|
|
4799
5508
|
init_paths();
|
|
@@ -4921,7 +5630,7 @@ import { setTimeout as sleep2 } from "timers/promises";
|
|
|
4921
5630
|
|
|
4922
5631
|
// src/daemon/server.ts
|
|
4923
5632
|
init_config();
|
|
4924
|
-
import * as
|
|
5633
|
+
import * as fs7 from "fs";
|
|
4925
5634
|
import * as fsp2 from "fs/promises";
|
|
4926
5635
|
import Fastify from "fastify";
|
|
4927
5636
|
import websocketPlugin from "@fastify/websocket";
|
|
@@ -5238,6 +5947,10 @@ init_paths();
|
|
|
5238
5947
|
import * as fs4 from "fs/promises";
|
|
5239
5948
|
import * as path2 from "path";
|
|
5240
5949
|
import { z as z4 } from "zod";
|
|
5950
|
+
var PersistedAgentCommand = z4.object({
|
|
5951
|
+
name: z4.string(),
|
|
5952
|
+
description: z4.string().optional()
|
|
5953
|
+
});
|
|
5241
5954
|
var SessionRecord = z4.object({
|
|
5242
5955
|
version: z4.literal(1),
|
|
5243
5956
|
sessionId: z4.string(),
|
|
@@ -5246,6 +5959,13 @@ var SessionRecord = z4.object({
|
|
|
5246
5959
|
cwd: z4.string(),
|
|
5247
5960
|
title: z4.string().optional(),
|
|
5248
5961
|
agentArgs: z4.array(z4.string()).optional(),
|
|
5962
|
+
// Snapshot of "what is currently true about this session" carried in
|
|
5963
|
+
// meta.json so a late-attaching or cold-resurrected client can be
|
|
5964
|
+
// told via the attach response _meta without depending on history
|
|
5965
|
+
// replay of a snapshot-shaped notification.
|
|
5966
|
+
currentModel: z4.string().optional(),
|
|
5967
|
+
currentMode: z4.string().optional(),
|
|
5968
|
+
agentCommands: z4.array(PersistedAgentCommand).optional(),
|
|
5249
5969
|
createdAt: z4.string(),
|
|
5250
5970
|
updatedAt: z4.string()
|
|
5251
5971
|
});
|
|
@@ -5258,7 +5978,7 @@ function assertSafeId(id) {
|
|
|
5258
5978
|
var SessionStore = class {
|
|
5259
5979
|
async write(record) {
|
|
5260
5980
|
assertSafeId(record.sessionId);
|
|
5261
|
-
await fs4.mkdir(paths.
|
|
5981
|
+
await fs4.mkdir(paths.sessionDir(record.sessionId), { recursive: true });
|
|
5262
5982
|
const full = { version: 1, ...record };
|
|
5263
5983
|
await fs4.writeFile(
|
|
5264
5984
|
paths.sessionFile(record.sessionId),
|
|
@@ -5298,6 +6018,14 @@ var SessionStore = class {
|
|
|
5298
6018
|
throw err;
|
|
5299
6019
|
}
|
|
5300
6020
|
}
|
|
6021
|
+
try {
|
|
6022
|
+
await fs4.rmdir(paths.sessionDir(sessionId));
|
|
6023
|
+
} catch (err) {
|
|
6024
|
+
const e = err;
|
|
6025
|
+
if (e.code !== "ENOENT" && e.code !== "ENOTEMPTY") {
|
|
6026
|
+
throw err;
|
|
6027
|
+
}
|
|
6028
|
+
}
|
|
5301
6029
|
}
|
|
5302
6030
|
async list() {
|
|
5303
6031
|
let entries;
|
|
@@ -5312,11 +6040,7 @@ var SessionStore = class {
|
|
|
5312
6040
|
}
|
|
5313
6041
|
const records = [];
|
|
5314
6042
|
for (const entry of entries) {
|
|
5315
|
-
|
|
5316
|
-
continue;
|
|
5317
|
-
}
|
|
5318
|
-
const id = entry.slice(0, -".json".length);
|
|
5319
|
-
const record = await this.read(id);
|
|
6043
|
+
const record = await this.read(entry);
|
|
5320
6044
|
if (record) {
|
|
5321
6045
|
records.push(record);
|
|
5322
6046
|
}
|
|
@@ -5333,11 +6057,137 @@ function recordFromMemorySession(args) {
|
|
|
5333
6057
|
cwd: args.cwd,
|
|
5334
6058
|
title: args.title,
|
|
5335
6059
|
agentArgs: args.agentArgs,
|
|
6060
|
+
currentModel: args.currentModel,
|
|
6061
|
+
currentMode: args.currentMode,
|
|
6062
|
+
agentCommands: args.agentCommands,
|
|
5336
6063
|
createdAt: args.createdAt ?? now,
|
|
5337
6064
|
updatedAt: args.updatedAt ?? now
|
|
5338
6065
|
};
|
|
5339
6066
|
}
|
|
5340
6067
|
|
|
6068
|
+
// src/core/history-store.ts
|
|
6069
|
+
init_paths();
|
|
6070
|
+
import * as fs5 from "fs/promises";
|
|
6071
|
+
var SESSION_ID_PATTERN2 = /^[A-Za-z0-9_-]+$/;
|
|
6072
|
+
var MAX_ENTRIES = 1e3;
|
|
6073
|
+
var HistoryStore = class {
|
|
6074
|
+
// Serialize writes per session id so appends and rewrites don't
|
|
6075
|
+
// interleave JSONL lines on disk. The chain swallows errors so one
|
|
6076
|
+
// failed append doesn't poison every subsequent write.
|
|
6077
|
+
writeQueues = /* @__PURE__ */ new Map();
|
|
6078
|
+
async append(sessionId, entry) {
|
|
6079
|
+
if (!SESSION_ID_PATTERN2.test(sessionId)) {
|
|
6080
|
+
return;
|
|
6081
|
+
}
|
|
6082
|
+
return this.enqueue(sessionId, async () => {
|
|
6083
|
+
await fs5.mkdir(paths.sessionDir(sessionId), { recursive: true });
|
|
6084
|
+
const line = JSON.stringify(entry) + "\n";
|
|
6085
|
+
await fs5.appendFile(paths.historyFile(sessionId), line, {
|
|
6086
|
+
encoding: "utf8",
|
|
6087
|
+
mode: 384
|
|
6088
|
+
});
|
|
6089
|
+
});
|
|
6090
|
+
}
|
|
6091
|
+
async rewrite(sessionId, entries) {
|
|
6092
|
+
if (!SESSION_ID_PATTERN2.test(sessionId)) {
|
|
6093
|
+
return;
|
|
6094
|
+
}
|
|
6095
|
+
return this.enqueue(sessionId, async () => {
|
|
6096
|
+
await fs5.mkdir(paths.sessionDir(sessionId), { recursive: true });
|
|
6097
|
+
const body = entries.length === 0 ? "" : entries.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
6098
|
+
await fs5.writeFile(paths.historyFile(sessionId), body, {
|
|
6099
|
+
encoding: "utf8",
|
|
6100
|
+
mode: 384
|
|
6101
|
+
});
|
|
6102
|
+
});
|
|
6103
|
+
}
|
|
6104
|
+
async load(sessionId) {
|
|
6105
|
+
if (!SESSION_ID_PATTERN2.test(sessionId)) {
|
|
6106
|
+
return [];
|
|
6107
|
+
}
|
|
6108
|
+
const pending = this.writeQueues.get(sessionId);
|
|
6109
|
+
if (pending) {
|
|
6110
|
+
await pending;
|
|
6111
|
+
}
|
|
6112
|
+
let raw;
|
|
6113
|
+
try {
|
|
6114
|
+
raw = await fs5.readFile(paths.historyFile(sessionId), "utf8");
|
|
6115
|
+
} catch (err) {
|
|
6116
|
+
const e = err;
|
|
6117
|
+
if (e.code === "ENOENT") {
|
|
6118
|
+
return [];
|
|
6119
|
+
}
|
|
6120
|
+
throw err;
|
|
6121
|
+
}
|
|
6122
|
+
const out = [];
|
|
6123
|
+
for (const line of raw.split("\n")) {
|
|
6124
|
+
if (line.length === 0) {
|
|
6125
|
+
continue;
|
|
6126
|
+
}
|
|
6127
|
+
let parsed;
|
|
6128
|
+
try {
|
|
6129
|
+
parsed = JSON.parse(line);
|
|
6130
|
+
} catch {
|
|
6131
|
+
continue;
|
|
6132
|
+
}
|
|
6133
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
6134
|
+
continue;
|
|
6135
|
+
}
|
|
6136
|
+
const obj = parsed;
|
|
6137
|
+
if (typeof obj.method !== "string") {
|
|
6138
|
+
continue;
|
|
6139
|
+
}
|
|
6140
|
+
if (typeof obj.recordedAt !== "number") {
|
|
6141
|
+
continue;
|
|
6142
|
+
}
|
|
6143
|
+
out.push({
|
|
6144
|
+
method: obj.method,
|
|
6145
|
+
params: obj.params,
|
|
6146
|
+
recordedAt: obj.recordedAt
|
|
6147
|
+
});
|
|
6148
|
+
}
|
|
6149
|
+
if (out.length > MAX_ENTRIES) {
|
|
6150
|
+
return out.slice(-MAX_ENTRIES);
|
|
6151
|
+
}
|
|
6152
|
+
return out;
|
|
6153
|
+
}
|
|
6154
|
+
async delete(sessionId) {
|
|
6155
|
+
if (!SESSION_ID_PATTERN2.test(sessionId)) {
|
|
6156
|
+
return;
|
|
6157
|
+
}
|
|
6158
|
+
return this.enqueue(sessionId, async () => {
|
|
6159
|
+
try {
|
|
6160
|
+
await fs5.unlink(paths.historyFile(sessionId));
|
|
6161
|
+
} catch (err) {
|
|
6162
|
+
const e = err;
|
|
6163
|
+
if (e.code !== "ENOENT") {
|
|
6164
|
+
throw err;
|
|
6165
|
+
}
|
|
6166
|
+
}
|
|
6167
|
+
try {
|
|
6168
|
+
await fs5.rmdir(paths.sessionDir(sessionId));
|
|
6169
|
+
} catch (err) {
|
|
6170
|
+
const e = err;
|
|
6171
|
+
if (e.code !== "ENOENT" && e.code !== "ENOTEMPTY") {
|
|
6172
|
+
throw err;
|
|
6173
|
+
}
|
|
6174
|
+
}
|
|
6175
|
+
});
|
|
6176
|
+
}
|
|
6177
|
+
enqueue(sessionId, task) {
|
|
6178
|
+
const prev = this.writeQueues.get(sessionId) ?? Promise.resolve();
|
|
6179
|
+
const task$ = prev.then(task, task);
|
|
6180
|
+
const settled = task$.catch(() => void 0);
|
|
6181
|
+
this.writeQueues.set(sessionId, settled);
|
|
6182
|
+
void settled.finally(() => {
|
|
6183
|
+
if (this.writeQueues.get(sessionId) === settled) {
|
|
6184
|
+
this.writeQueues.delete(sessionId);
|
|
6185
|
+
}
|
|
6186
|
+
});
|
|
6187
|
+
return task$;
|
|
6188
|
+
}
|
|
6189
|
+
};
|
|
6190
|
+
|
|
5341
6191
|
// src/core/session-manager.ts
|
|
5342
6192
|
init_types();
|
|
5343
6193
|
var SessionManager = class {
|
|
@@ -5345,6 +6195,7 @@ var SessionManager = class {
|
|
|
5345
6195
|
this.registry = registry;
|
|
5346
6196
|
this.spawner = spawner ?? ((opts) => AgentInstance.spawn(opts));
|
|
5347
6197
|
this.store = store ?? new SessionStore();
|
|
6198
|
+
this.histories = new HistoryStore();
|
|
5348
6199
|
this.idleTimeoutMs = options.idleTimeoutMs ?? 0;
|
|
5349
6200
|
}
|
|
5350
6201
|
registry;
|
|
@@ -5352,7 +6203,12 @@ var SessionManager = class {
|
|
|
5352
6203
|
resurrectionInflight = /* @__PURE__ */ new Map();
|
|
5353
6204
|
spawner;
|
|
5354
6205
|
store;
|
|
6206
|
+
histories;
|
|
5355
6207
|
idleTimeoutMs;
|
|
6208
|
+
// Serialize meta.json read-modify-write operations per session id so
|
|
6209
|
+
// concurrent snapshot updates (e.g. an agent emitting model + mode
|
|
6210
|
+
// back-to-back) don't lose writes via interleaved reads.
|
|
6211
|
+
metaWriteQueues = /* @__PURE__ */ new Map();
|
|
5356
6212
|
async create(params) {
|
|
5357
6213
|
const fresh = await this.bootstrapAgent({
|
|
5358
6214
|
agentId: params.agentId,
|
|
@@ -5369,7 +6225,8 @@ var SessionManager = class {
|
|
|
5369
6225
|
title: params.title,
|
|
5370
6226
|
agentArgs: params.agentArgs,
|
|
5371
6227
|
idleTimeoutMs: this.idleTimeoutMs,
|
|
5372
|
-
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] })
|
|
6228
|
+
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
|
|
6229
|
+
historyStore: this.histories
|
|
5373
6230
|
});
|
|
5374
6231
|
await this.attachManagerHooks(session);
|
|
5375
6232
|
return session;
|
|
@@ -5445,7 +6302,12 @@ var SessionManager = class {
|
|
|
5445
6302
|
title: params.title,
|
|
5446
6303
|
agentArgs: params.agentArgs,
|
|
5447
6304
|
idleTimeoutMs: this.idleTimeoutMs,
|
|
5448
|
-
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] })
|
|
6305
|
+
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
|
|
6306
|
+
historyStore: this.histories,
|
|
6307
|
+
seedHistory: params.seedHistory,
|
|
6308
|
+
currentModel: params.currentModel,
|
|
6309
|
+
currentMode: params.currentMode,
|
|
6310
|
+
agentCommands: params.agentCommands
|
|
5449
6311
|
});
|
|
5450
6312
|
await this.attachManagerHooks(session);
|
|
5451
6313
|
return session;
|
|
@@ -5499,6 +6361,7 @@ var SessionManager = class {
|
|
|
5499
6361
|
this.sessions.delete(session.sessionId);
|
|
5500
6362
|
if (deleteRecord) {
|
|
5501
6363
|
void this.store.delete(session.sessionId).catch(() => void 0);
|
|
6364
|
+
void this.histories.delete(session.sessionId).catch(() => void 0);
|
|
5502
6365
|
}
|
|
5503
6366
|
});
|
|
5504
6367
|
session.onTitleChange((title) => {
|
|
@@ -5509,6 +6372,24 @@ var SessionManager = class {
|
|
|
5509
6372
|
() => void 0
|
|
5510
6373
|
);
|
|
5511
6374
|
});
|
|
6375
|
+
session.onModelChange((model) => {
|
|
6376
|
+
void this.persistSnapshot(session.sessionId, { currentModel: model }).catch(
|
|
6377
|
+
() => void 0
|
|
6378
|
+
);
|
|
6379
|
+
});
|
|
6380
|
+
session.onModeChange((mode) => {
|
|
6381
|
+
void this.persistSnapshot(session.sessionId, { currentMode: mode }).catch(
|
|
6382
|
+
() => void 0
|
|
6383
|
+
);
|
|
6384
|
+
});
|
|
6385
|
+
session.onAgentCommandsChange((commands) => {
|
|
6386
|
+
void this.persistSnapshot(session.sessionId, {
|
|
6387
|
+
agentCommands: commands.map((c) => ({
|
|
6388
|
+
name: c.name,
|
|
6389
|
+
...c.description !== void 0 ? { description: c.description } : {}
|
|
6390
|
+
}))
|
|
6391
|
+
}).catch(() => void 0);
|
|
6392
|
+
});
|
|
5512
6393
|
this.sessions.set(session.sessionId, session);
|
|
5513
6394
|
await this.store.write(
|
|
5514
6395
|
recordFromMemorySession({
|
|
@@ -5517,22 +6398,45 @@ var SessionManager = class {
|
|
|
5517
6398
|
agentId: session.agentId,
|
|
5518
6399
|
cwd: session.cwd,
|
|
5519
6400
|
title: session.title,
|
|
5520
|
-
agentArgs: session.agentArgs
|
|
6401
|
+
agentArgs: session.agentArgs,
|
|
6402
|
+
currentModel: session.currentModel,
|
|
6403
|
+
currentMode: session.currentMode
|
|
5521
6404
|
})
|
|
5522
6405
|
).catch(() => void 0);
|
|
5523
6406
|
}
|
|
6407
|
+
// Resolve a session's recorded history without forcing a resurrect.
|
|
6408
|
+
// Returns the in-memory snapshot if the session is hot, falls back
|
|
6409
|
+
// to the on-disk history file otherwise. Returns undefined if the
|
|
6410
|
+
// session id is unknown to both the live map and disk store, so the
|
|
6411
|
+
// caller can distinguish "no history yet" (empty array) from "404".
|
|
6412
|
+
async getHistory(sessionId) {
|
|
6413
|
+
const live = this.sessions.get(sessionId);
|
|
6414
|
+
if (live) {
|
|
6415
|
+
return live.getHistorySnapshot();
|
|
6416
|
+
}
|
|
6417
|
+
const record = await this.store.read(sessionId);
|
|
6418
|
+
if (!record) {
|
|
6419
|
+
return void 0;
|
|
6420
|
+
}
|
|
6421
|
+
return this.histories.load(sessionId).catch(() => []);
|
|
6422
|
+
}
|
|
5524
6423
|
async loadFromDisk(sessionId) {
|
|
5525
6424
|
const record = await this.store.read(sessionId);
|
|
5526
6425
|
if (!record) {
|
|
5527
6426
|
return void 0;
|
|
5528
6427
|
}
|
|
6428
|
+
const seedHistory = await this.histories.load(sessionId).catch(() => []);
|
|
5529
6429
|
return {
|
|
5530
6430
|
hydraSessionId: record.sessionId,
|
|
5531
6431
|
upstreamSessionId: record.upstreamSessionId,
|
|
5532
6432
|
agentId: record.agentId,
|
|
5533
6433
|
cwd: record.cwd,
|
|
5534
6434
|
title: record.title,
|
|
5535
|
-
agentArgs: record.agentArgs
|
|
6435
|
+
agentArgs: record.agentArgs,
|
|
6436
|
+
seedHistory: seedHistory.length > 0 ? seedHistory : void 0,
|
|
6437
|
+
currentModel: record.currentModel,
|
|
6438
|
+
currentMode: record.currentMode,
|
|
6439
|
+
agentCommands: record.agentCommands
|
|
5536
6440
|
};
|
|
5537
6441
|
}
|
|
5538
6442
|
get(sessionId) {
|
|
@@ -5620,14 +6524,16 @@ var SessionManager = class {
|
|
|
5620
6524
|
// record's title in sync with what was broadcast to clients so a
|
|
5621
6525
|
// daemon restart (and later resurrect) restores the same title.
|
|
5622
6526
|
async persistTitle(sessionId, title) {
|
|
5623
|
-
|
|
5624
|
-
|
|
5625
|
-
|
|
5626
|
-
|
|
5627
|
-
|
|
5628
|
-
|
|
5629
|
-
|
|
5630
|
-
|
|
6527
|
+
await this.enqueueMetaWrite(sessionId, async () => {
|
|
6528
|
+
const record = await this.store.read(sessionId);
|
|
6529
|
+
if (!record) {
|
|
6530
|
+
return;
|
|
6531
|
+
}
|
|
6532
|
+
await this.store.write({
|
|
6533
|
+
...record,
|
|
6534
|
+
title,
|
|
6535
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6536
|
+
});
|
|
5631
6537
|
});
|
|
5632
6538
|
}
|
|
5633
6539
|
// Persist an agent swap from /hydra switch. The on-disk record's
|
|
@@ -5635,17 +6541,52 @@ var SessionManager = class {
|
|
|
5635
6541
|
// later resurrect) brings the session back up on the agent the user
|
|
5636
6542
|
// most recently switched to, not the one it was originally created on.
|
|
5637
6543
|
async persistAgentChange(sessionId, agentId, upstreamSessionId) {
|
|
5638
|
-
|
|
5639
|
-
|
|
5640
|
-
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
|
|
5644
|
-
|
|
5645
|
-
|
|
5646
|
-
|
|
6544
|
+
await this.enqueueMetaWrite(sessionId, async () => {
|
|
6545
|
+
const record = await this.store.read(sessionId);
|
|
6546
|
+
if (!record) {
|
|
6547
|
+
return;
|
|
6548
|
+
}
|
|
6549
|
+
await this.store.write({
|
|
6550
|
+
...record,
|
|
6551
|
+
agentId,
|
|
6552
|
+
upstreamSessionId,
|
|
6553
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6554
|
+
});
|
|
6555
|
+
});
|
|
6556
|
+
}
|
|
6557
|
+
// Update one or more snapshot fields (model, mode, commands) in
|
|
6558
|
+
// meta.json. Used so cold-resurrect can deliver the latest snapshot
|
|
6559
|
+
// to attaching clients via the attach response _meta. No-op if the
|
|
6560
|
+
// session record has gone away (race with deleteRecord).
|
|
6561
|
+
async persistSnapshot(sessionId, update) {
|
|
6562
|
+
await this.enqueueMetaWrite(sessionId, async () => {
|
|
6563
|
+
const record = await this.store.read(sessionId);
|
|
6564
|
+
if (!record) {
|
|
6565
|
+
return;
|
|
6566
|
+
}
|
|
6567
|
+
await this.store.write({
|
|
6568
|
+
...record,
|
|
6569
|
+
...update.currentModel !== void 0 ? { currentModel: update.currentModel } : {},
|
|
6570
|
+
...update.currentMode !== void 0 ? { currentMode: update.currentMode } : {},
|
|
6571
|
+
...update.agentCommands !== void 0 ? { agentCommands: update.agentCommands } : {},
|
|
6572
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6573
|
+
});
|
|
5647
6574
|
});
|
|
5648
6575
|
}
|
|
6576
|
+
// Serialize meta.json writes per session id so concurrent
|
|
6577
|
+
// read-modify-write operations don't interleave reads.
|
|
6578
|
+
enqueueMetaWrite(sessionId, task) {
|
|
6579
|
+
const prev = this.metaWriteQueues.get(sessionId) ?? Promise.resolve();
|
|
6580
|
+
const next = prev.then(task, task);
|
|
6581
|
+
const settled = next.catch(() => void 0);
|
|
6582
|
+
this.metaWriteQueues.set(sessionId, settled);
|
|
6583
|
+
void settled.finally(() => {
|
|
6584
|
+
if (this.metaWriteQueues.get(sessionId) === settled) {
|
|
6585
|
+
this.metaWriteQueues.delete(sessionId);
|
|
6586
|
+
}
|
|
6587
|
+
});
|
|
6588
|
+
return next;
|
|
6589
|
+
}
|
|
5649
6590
|
async closeAll() {
|
|
5650
6591
|
const sessions = [...this.sessions.values()];
|
|
5651
6592
|
await Promise.allSettled(sessions.map((s) => s.close()));
|
|
@@ -5656,7 +6597,7 @@ var SessionManager = class {
|
|
|
5656
6597
|
// src/core/extensions.ts
|
|
5657
6598
|
init_paths();
|
|
5658
6599
|
import { spawn as spawn2 } from "child_process";
|
|
5659
|
-
import * as
|
|
6600
|
+
import * as fs6 from "fs";
|
|
5660
6601
|
import * as fsp from "fs/promises";
|
|
5661
6602
|
import * as path3 from "path";
|
|
5662
6603
|
var RESTART_BASE_MS = 1e3;
|
|
@@ -5939,7 +6880,7 @@ var ExtensionManager = class {
|
|
|
5939
6880
|
}
|
|
5940
6881
|
const ext = entry.config;
|
|
5941
6882
|
const command = ext.command.length > 0 ? ext.command : [ext.name];
|
|
5942
|
-
const logStream =
|
|
6883
|
+
const logStream = fs6.createWriteStream(paths.extensionLogFile(ext.name), {
|
|
5943
6884
|
flags: "a"
|
|
5944
6885
|
});
|
|
5945
6886
|
logStream.write(
|
|
@@ -5989,7 +6930,7 @@ var ExtensionManager = class {
|
|
|
5989
6930
|
}
|
|
5990
6931
|
if (typeof child.pid === "number") {
|
|
5991
6932
|
try {
|
|
5992
|
-
|
|
6933
|
+
fs6.writeFileSync(paths.extensionPidFile(ext.name), `${child.pid}
|
|
5993
6934
|
`, {
|
|
5994
6935
|
encoding: "utf8",
|
|
5995
6936
|
mode: 384
|
|
@@ -6014,7 +6955,7 @@ var ExtensionManager = class {
|
|
|
6014
6955
|
});
|
|
6015
6956
|
child.on("exit", (code, signal) => {
|
|
6016
6957
|
try {
|
|
6017
|
-
|
|
6958
|
+
fs6.unlinkSync(paths.extensionPidFile(ext.name));
|
|
6018
6959
|
} catch {
|
|
6019
6960
|
}
|
|
6020
6961
|
logStream.write(
|
|
@@ -6130,8 +7071,7 @@ init_config();
|
|
|
6130
7071
|
function registerSessionRoutes(app, manager, defaults) {
|
|
6131
7072
|
app.get("/v1/sessions", async (request) => {
|
|
6132
7073
|
const query = request.query;
|
|
6133
|
-
const
|
|
6134
|
-
const sessions = await manager.list({ cwd: query?.cwd, all });
|
|
7074
|
+
const sessions = await manager.list({ cwd: query?.cwd });
|
|
6135
7075
|
return { sessions };
|
|
6136
7076
|
});
|
|
6137
7077
|
app.post("/v1/sessions", async (request, reply) => {
|
|
@@ -6169,6 +7109,50 @@ function registerSessionRoutes(app, manager, defaults) {
|
|
|
6169
7109
|
}
|
|
6170
7110
|
reply.code(204).send();
|
|
6171
7111
|
});
|
|
7112
|
+
app.get("/v1/sessions/:id/history", async (request, reply) => {
|
|
7113
|
+
const raw = request.params.id;
|
|
7114
|
+
const query = request.query;
|
|
7115
|
+
const follow = query?.follow === "1" || query?.follow === "true";
|
|
7116
|
+
const id = await manager.resolveCanonicalId(raw) ?? raw;
|
|
7117
|
+
const live = manager.get(id);
|
|
7118
|
+
let snapshot;
|
|
7119
|
+
let unsubscribe;
|
|
7120
|
+
if (live) {
|
|
7121
|
+
snapshot = live.getHistorySnapshot();
|
|
7122
|
+
if (follow) {
|
|
7123
|
+
unsubscribe = live.onBroadcast((entry) => {
|
|
7124
|
+
if (reply.raw.writableEnded) {
|
|
7125
|
+
return;
|
|
7126
|
+
}
|
|
7127
|
+
reply.raw.write(JSON.stringify(entry) + "\n");
|
|
7128
|
+
});
|
|
7129
|
+
}
|
|
7130
|
+
} else {
|
|
7131
|
+
const cold = await manager.getHistory(id);
|
|
7132
|
+
if (cold === void 0) {
|
|
7133
|
+
reply.code(404).send({ error: "session not found" });
|
|
7134
|
+
return reply;
|
|
7135
|
+
}
|
|
7136
|
+
snapshot = cold;
|
|
7137
|
+
}
|
|
7138
|
+
reply.raw.setHeader("Content-Type", "application/x-ndjson");
|
|
7139
|
+
reply.raw.setHeader("Cache-Control", "no-cache");
|
|
7140
|
+
reply.raw.statusCode = 200;
|
|
7141
|
+
for (const entry of snapshot ?? []) {
|
|
7142
|
+
reply.raw.write(JSON.stringify(entry) + "\n");
|
|
7143
|
+
}
|
|
7144
|
+
if (!unsubscribe) {
|
|
7145
|
+
reply.raw.end();
|
|
7146
|
+
return reply;
|
|
7147
|
+
}
|
|
7148
|
+
request.raw.on("close", () => {
|
|
7149
|
+
unsubscribe?.();
|
|
7150
|
+
if (!reply.raw.writableEnded) {
|
|
7151
|
+
reply.raw.end();
|
|
7152
|
+
}
|
|
7153
|
+
});
|
|
7154
|
+
return reply;
|
|
7155
|
+
});
|
|
6172
7156
|
}
|
|
6173
7157
|
|
|
6174
7158
|
// src/daemon/routes/agents.ts
|
|
@@ -6559,6 +7543,16 @@ function buildResponseMeta(session) {
|
|
|
6559
7543
|
if (session.agentArgs && session.agentArgs.length > 0) {
|
|
6560
7544
|
ours.agentArgs = session.agentArgs;
|
|
6561
7545
|
}
|
|
7546
|
+
if (session.currentModel !== void 0) {
|
|
7547
|
+
ours.currentModel = session.currentModel;
|
|
7548
|
+
}
|
|
7549
|
+
if (session.currentMode !== void 0) {
|
|
7550
|
+
ours.currentMode = session.currentMode;
|
|
7551
|
+
}
|
|
7552
|
+
const commands = session.mergedAvailableCommands();
|
|
7553
|
+
if (commands.length > 0) {
|
|
7554
|
+
ours.availableCommands = commands;
|
|
7555
|
+
}
|
|
6562
7556
|
return mergeMeta(session.agentMeta, ours);
|
|
6563
7557
|
}
|
|
6564
7558
|
function buildInitializeResult() {
|
|
@@ -6679,7 +7673,7 @@ async function startDaemon(config) {
|
|
|
6679
7673
|
await manager.closeAll();
|
|
6680
7674
|
await app.close();
|
|
6681
7675
|
try {
|
|
6682
|
-
|
|
7676
|
+
fs7.unlinkSync(paths.pidFile());
|
|
6683
7677
|
} catch {
|
|
6684
7678
|
}
|
|
6685
7679
|
try {
|
|
@@ -6718,7 +7712,7 @@ function ensureLoopbackOrTls(config) {
|
|
|
6718
7712
|
init_daemon_bootstrap();
|
|
6719
7713
|
|
|
6720
7714
|
// src/cli/commands/log-tail.ts
|
|
6721
|
-
import * as
|
|
7715
|
+
import * as fs8 from "fs";
|
|
6722
7716
|
import * as fsp3 from "fs/promises";
|
|
6723
7717
|
async function runLogTail(logPath, argv, notFoundMessage) {
|
|
6724
7718
|
const opts = parseLogTailFlags(argv);
|
|
@@ -6742,7 +7736,7 @@ async function runLogTail(logPath, argv, notFoundMessage) {
|
|
|
6742
7736
|
process.stdout.write(`-- following ${logPath} --
|
|
6743
7737
|
`);
|
|
6744
7738
|
let pending = false;
|
|
6745
|
-
const watcher =
|
|
7739
|
+
const watcher = fs8.watch(logPath, () => {
|
|
6746
7740
|
if (pending) {
|
|
6747
7741
|
return;
|
|
6748
7742
|
}
|
|
@@ -7500,226 +8494,7 @@ function maxLen3(headerCell, values) {
|
|
|
7500
8494
|
// src/shim/proxy.ts
|
|
7501
8495
|
init_config();
|
|
7502
8496
|
init_daemon_bootstrap();
|
|
7503
|
-
|
|
7504
|
-
// src/shim/resilient-ws.ts
|
|
7505
|
-
init_ws_stream();
|
|
7506
|
-
init_types();
|
|
7507
|
-
import { setTimeout as sleep3 } from "timers/promises";
|
|
7508
|
-
import { WebSocket } from "ws";
|
|
7509
|
-
var BACKOFF_INITIAL_MS = 200;
|
|
7510
|
-
var BACKOFF_MAX_MS = 5e3;
|
|
7511
|
-
var BACKOFF_MULTIPLIER = 2;
|
|
7512
|
-
var MAX_RECONNECT_ATTEMPTS = 60;
|
|
7513
|
-
var ResilientWsStream = class {
|
|
7514
|
-
constructor(opts) {
|
|
7515
|
-
this.opts = opts;
|
|
7516
|
-
}
|
|
7517
|
-
opts;
|
|
7518
|
-
current;
|
|
7519
|
-
outboundQueue = [];
|
|
7520
|
-
messageHandlers = [];
|
|
7521
|
-
closeHandlers = [];
|
|
7522
|
-
destroyed = false;
|
|
7523
|
-
firstConnect = true;
|
|
7524
|
-
reconnectInFlight;
|
|
7525
|
-
connectGate;
|
|
7526
|
-
releaseConnectGate;
|
|
7527
|
-
pendingRequests = /* @__PURE__ */ new Map();
|
|
7528
|
-
async start() {
|
|
7529
|
-
await this.connectWithRetry();
|
|
7530
|
-
}
|
|
7531
|
-
onMessage(handler) {
|
|
7532
|
-
this.messageHandlers.push(handler);
|
|
7533
|
-
}
|
|
7534
|
-
onClose(handler) {
|
|
7535
|
-
this.closeHandlers.push(handler);
|
|
7536
|
-
}
|
|
7537
|
-
async send(message) {
|
|
7538
|
-
if (this.destroyed) {
|
|
7539
|
-
throw new Error("resilient ws stream is destroyed");
|
|
7540
|
-
}
|
|
7541
|
-
if (this.connectGate || !this.current) {
|
|
7542
|
-
this.outboundQueue.push(message);
|
|
7543
|
-
return;
|
|
7544
|
-
}
|
|
7545
|
-
try {
|
|
7546
|
-
await this.current.send(message);
|
|
7547
|
-
} catch (err) {
|
|
7548
|
-
this.outboundQueue.push(message);
|
|
7549
|
-
this.scheduleReconnect(err);
|
|
7550
|
-
}
|
|
7551
|
-
}
|
|
7552
|
-
// Send a request directly and resolve when the matching response arrives
|
|
7553
|
-
// on the same connection. Used by onConnect handlers to await replay-attach
|
|
7554
|
-
// responses before letting the outbound queue drain. Bypasses the
|
|
7555
|
-
// connectGate intentionally.
|
|
7556
|
-
async request(message) {
|
|
7557
|
-
if (this.destroyed) {
|
|
7558
|
-
throw new Error("resilient ws stream is destroyed");
|
|
7559
|
-
}
|
|
7560
|
-
if (!this.current) {
|
|
7561
|
-
throw new Error("resilient ws stream not connected");
|
|
7562
|
-
}
|
|
7563
|
-
const id = message.id;
|
|
7564
|
-
const promise = new Promise((resolve2, reject) => {
|
|
7565
|
-
this.pendingRequests.set(id, { resolve: resolve2, reject });
|
|
7566
|
-
});
|
|
7567
|
-
try {
|
|
7568
|
-
await this.current.send(message);
|
|
7569
|
-
} catch (err) {
|
|
7570
|
-
this.pendingRequests.delete(id);
|
|
7571
|
-
throw err;
|
|
7572
|
-
}
|
|
7573
|
-
return promise;
|
|
7574
|
-
}
|
|
7575
|
-
async close() {
|
|
7576
|
-
this.destroyed = true;
|
|
7577
|
-
if (this.current) {
|
|
7578
|
-
await this.current.close().catch(() => void 0);
|
|
7579
|
-
}
|
|
7580
|
-
for (const handler of this.closeHandlers) {
|
|
7581
|
-
handler();
|
|
7582
|
-
}
|
|
7583
|
-
}
|
|
7584
|
-
async connectWithRetry() {
|
|
7585
|
-
let attempt = 0;
|
|
7586
|
-
let backoff = BACKOFF_INITIAL_MS;
|
|
7587
|
-
while (!this.destroyed) {
|
|
7588
|
-
try {
|
|
7589
|
-
const stream = await openWs(this.opts.url, this.opts.subprotocols);
|
|
7590
|
-
this.bindStream(stream);
|
|
7591
|
-
const wasFirst = this.firstConnect;
|
|
7592
|
-
this.firstConnect = false;
|
|
7593
|
-
this.connectGate = new Promise((resolve2) => {
|
|
7594
|
-
this.releaseConnectGate = resolve2;
|
|
7595
|
-
});
|
|
7596
|
-
try {
|
|
7597
|
-
if (this.opts.onConnect) {
|
|
7598
|
-
try {
|
|
7599
|
-
await this.opts.onConnect(wasFirst);
|
|
7600
|
-
} catch (err) {
|
|
7601
|
-
this.log(
|
|
7602
|
-
`hydra-acp: post-connect handler failed: ${err.message}`
|
|
7603
|
-
);
|
|
7604
|
-
}
|
|
7605
|
-
}
|
|
7606
|
-
} finally {
|
|
7607
|
-
this.releaseConnectGate?.();
|
|
7608
|
-
this.releaseConnectGate = void 0;
|
|
7609
|
-
this.connectGate = void 0;
|
|
7610
|
-
}
|
|
7611
|
-
await this.flushQueue();
|
|
7612
|
-
return;
|
|
7613
|
-
} catch (err) {
|
|
7614
|
-
attempt += 1;
|
|
7615
|
-
if (this.opts.onConnectFailure) {
|
|
7616
|
-
this.opts.onConnectFailure(err);
|
|
7617
|
-
}
|
|
7618
|
-
if (attempt >= MAX_RECONNECT_ATTEMPTS) {
|
|
7619
|
-
throw new Error(
|
|
7620
|
-
`hydra-acp: gave up reconnecting after ${attempt} attempts: ${err.message}`
|
|
7621
|
-
);
|
|
7622
|
-
}
|
|
7623
|
-
this.log(
|
|
7624
|
-
`hydra-acp: connect attempt ${attempt} failed (${err.message}); retrying in ${backoff}ms`
|
|
7625
|
-
);
|
|
7626
|
-
await sleep3(backoff);
|
|
7627
|
-
backoff = Math.min(backoff * BACKOFF_MULTIPLIER, BACKOFF_MAX_MS);
|
|
7628
|
-
}
|
|
7629
|
-
}
|
|
7630
|
-
}
|
|
7631
|
-
bindStream(stream) {
|
|
7632
|
-
this.current = stream;
|
|
7633
|
-
stream.onMessage((msg) => {
|
|
7634
|
-
if (isResponse(msg)) {
|
|
7635
|
-
const pending = this.pendingRequests.get(msg.id);
|
|
7636
|
-
if (pending) {
|
|
7637
|
-
this.pendingRequests.delete(msg.id);
|
|
7638
|
-
pending.resolve(msg);
|
|
7639
|
-
}
|
|
7640
|
-
}
|
|
7641
|
-
for (const handler of this.messageHandlers) {
|
|
7642
|
-
handler(msg);
|
|
7643
|
-
}
|
|
7644
|
-
});
|
|
7645
|
-
stream.onClose((err) => {
|
|
7646
|
-
if (this.destroyed) {
|
|
7647
|
-
return;
|
|
7648
|
-
}
|
|
7649
|
-
this.current = void 0;
|
|
7650
|
-
if (this.pendingRequests.size > 0) {
|
|
7651
|
-
const reason = err ?? new Error("ws closed before response");
|
|
7652
|
-
for (const { reject } of this.pendingRequests.values()) {
|
|
7653
|
-
reject(reason);
|
|
7654
|
-
}
|
|
7655
|
-
this.pendingRequests.clear();
|
|
7656
|
-
}
|
|
7657
|
-
this.scheduleReconnect(err);
|
|
7658
|
-
});
|
|
7659
|
-
}
|
|
7660
|
-
async flushQueue() {
|
|
7661
|
-
if (!this.current) {
|
|
7662
|
-
return;
|
|
7663
|
-
}
|
|
7664
|
-
const queue = this.outboundQueue;
|
|
7665
|
-
this.outboundQueue = [];
|
|
7666
|
-
for (const msg of queue) {
|
|
7667
|
-
try {
|
|
7668
|
-
await this.current.send(msg);
|
|
7669
|
-
} catch (err) {
|
|
7670
|
-
this.outboundQueue.unshift(msg);
|
|
7671
|
-
this.scheduleReconnect(err);
|
|
7672
|
-
return;
|
|
7673
|
-
}
|
|
7674
|
-
}
|
|
7675
|
-
}
|
|
7676
|
-
scheduleReconnect(err) {
|
|
7677
|
-
if (this.destroyed || this.reconnectInFlight) {
|
|
7678
|
-
return;
|
|
7679
|
-
}
|
|
7680
|
-
this.log(
|
|
7681
|
-
`hydra-acp: connection lost (${err?.message ?? "no error"}); reconnecting...`
|
|
7682
|
-
);
|
|
7683
|
-
this.reconnectInFlight = (async () => {
|
|
7684
|
-
try {
|
|
7685
|
-
await this.connectWithRetry();
|
|
7686
|
-
} catch (final) {
|
|
7687
|
-
for (const handler of this.closeHandlers) {
|
|
7688
|
-
handler(final);
|
|
7689
|
-
}
|
|
7690
|
-
this.destroyed = true;
|
|
7691
|
-
} finally {
|
|
7692
|
-
this.reconnectInFlight = void 0;
|
|
7693
|
-
}
|
|
7694
|
-
})();
|
|
7695
|
-
}
|
|
7696
|
-
log(line) {
|
|
7697
|
-
if (this.opts.log) {
|
|
7698
|
-
this.opts.log(line);
|
|
7699
|
-
return;
|
|
7700
|
-
}
|
|
7701
|
-
process.stderr.write(`${line}
|
|
7702
|
-
`);
|
|
7703
|
-
}
|
|
7704
|
-
};
|
|
7705
|
-
function isResponse(msg) {
|
|
7706
|
-
return !("method" in msg) && "id" in msg && msg.id !== void 0;
|
|
7707
|
-
}
|
|
7708
|
-
async function openWs(url, subprotocols) {
|
|
7709
|
-
return new Promise((resolve2, reject) => {
|
|
7710
|
-
const ws = new WebSocket(url, subprotocols);
|
|
7711
|
-
const onOpen = () => {
|
|
7712
|
-
ws.off("error", onError);
|
|
7713
|
-
resolve2(wsToMessageStream(ws));
|
|
7714
|
-
};
|
|
7715
|
-
const onError = (err) => {
|
|
7716
|
-
ws.off("open", onOpen);
|
|
7717
|
-
reject(err);
|
|
7718
|
-
};
|
|
7719
|
-
ws.once("open", onOpen);
|
|
7720
|
-
ws.once("error", onError);
|
|
7721
|
-
});
|
|
7722
|
-
}
|
|
8497
|
+
init_resilient_ws();
|
|
7723
8498
|
|
|
7724
8499
|
// src/shim/session-tracker.ts
|
|
7725
8500
|
init_types();
|