@hydra-acp/cli 0.1.3 → 0.1.4
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 +25 -1
- package/dist/cli.js +1078 -266
- package/dist/index.d.ts +176 -7
- package/dist/index.js +660 -129
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -89,8 +89,7 @@ async function ensureConfig() {
|
|
|
89
89
|
if (e.code !== "ENOENT") {
|
|
90
90
|
throw err;
|
|
91
91
|
}
|
|
92
|
-
const config =
|
|
93
|
-
await writeConfig(config);
|
|
92
|
+
const config = await writeMinimalInitConfig();
|
|
94
93
|
process.stderr.write(
|
|
95
94
|
`hydra-acp: initialized ${paths.config()} with a fresh auth token.
|
|
96
95
|
`
|
|
@@ -99,9 +98,23 @@ async function ensureConfig() {
|
|
|
99
98
|
}
|
|
100
99
|
return loadConfig();
|
|
101
100
|
}
|
|
102
|
-
async function
|
|
101
|
+
async function writeMinimalInitConfig(authToken) {
|
|
102
|
+
const token = authToken ?? generateAuthToken();
|
|
103
|
+
const minimal = { daemon: { authToken: token } };
|
|
103
104
|
await fs.mkdir(paths.home(), { recursive: true });
|
|
104
|
-
await fs.writeFile(paths.config(), JSON.stringify(
|
|
105
|
+
await fs.writeFile(paths.config(), JSON.stringify(minimal, null, 2) + "\n", {
|
|
106
|
+
encoding: "utf8",
|
|
107
|
+
mode: 384
|
|
108
|
+
});
|
|
109
|
+
return HydraConfig.parse(minimal);
|
|
110
|
+
}
|
|
111
|
+
async function updateConfigField(mutate) {
|
|
112
|
+
const path6 = paths.config();
|
|
113
|
+
const text = await fs.readFile(path6, "utf8");
|
|
114
|
+
const raw = JSON.parse(text);
|
|
115
|
+
mutate(raw);
|
|
116
|
+
HydraConfig.parse(raw);
|
|
117
|
+
await fs.writeFile(path6, JSON.stringify(raw, null, 2) + "\n", {
|
|
105
118
|
encoding: "utf8",
|
|
106
119
|
mode: 384
|
|
107
120
|
});
|
|
@@ -115,13 +128,6 @@ function generateAuthToken() {
|
|
|
115
128
|
}
|
|
116
129
|
return `hydra_token_${hex}`;
|
|
117
130
|
}
|
|
118
|
-
function defaultConfig() {
|
|
119
|
-
return HydraConfig.parse({
|
|
120
|
-
daemon: {
|
|
121
|
-
authToken: generateAuthToken()
|
|
122
|
-
}
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
131
|
function expandHome(p) {
|
|
126
132
|
if (p === "~" || p === "$HOME") {
|
|
127
133
|
return homedir2();
|
|
@@ -150,7 +156,7 @@ var init_config = __esm({
|
|
|
150
156
|
authToken: z.string().min(16),
|
|
151
157
|
logLevel: z.enum(["debug", "info", "warn", "error"]).default("info"),
|
|
152
158
|
tls: TlsConfig.optional(),
|
|
153
|
-
sessionIdleTimeoutSeconds: z.number().int().nonnegative().default(
|
|
159
|
+
sessionIdleTimeoutSeconds: z.number().int().nonnegative().default(3600)
|
|
154
160
|
});
|
|
155
161
|
RegistryConfig = z.object({
|
|
156
162
|
url: z.string().url().default(REGISTRY_URL_DEFAULT),
|
|
@@ -277,7 +283,8 @@ var init_types = __esm({
|
|
|
277
283
|
SessionNotFound: -32001,
|
|
278
284
|
PermissionDenied: -32002,
|
|
279
285
|
AlreadyAttached: -32003,
|
|
280
|
-
AgentNotInstalled: -32005
|
|
286
|
+
AgentNotInstalled: -32005,
|
|
287
|
+
BundleAlreadyImported: -32010
|
|
281
288
|
};
|
|
282
289
|
InitializeParams = z3.object({
|
|
283
290
|
protocolVersion: z3.number().optional(),
|
|
@@ -401,9 +408,9 @@ var init_connection = __esm({
|
|
|
401
408
|
}
|
|
402
409
|
const id = nanoid();
|
|
403
410
|
const message = { jsonrpc: "2.0", id, method, params };
|
|
404
|
-
const response = new Promise((
|
|
411
|
+
const response = new Promise((resolve4, reject) => {
|
|
405
412
|
this.pending.set(id, {
|
|
406
|
-
resolve: (result) =>
|
|
413
|
+
resolve: (result) => resolve4(result),
|
|
407
414
|
reject
|
|
408
415
|
});
|
|
409
416
|
this.stream.send(message).catch((err) => {
|
|
@@ -627,7 +634,7 @@ function firstLine(text, max) {
|
|
|
627
634
|
}
|
|
628
635
|
return void 0;
|
|
629
636
|
}
|
|
630
|
-
var HYDRA_ID_ALPHABET, generateHydraId, HYDRA_SESSION_PREFIX, Session, STATE_UPDATE_KINDS;
|
|
637
|
+
var HYDRA_ID_ALPHABET, generateHydraId, HYDRA_SESSION_PREFIX, MAX_HISTORY_ENTRIES, COMPACT_EVERY, Session, STATE_UPDATE_KINDS;
|
|
631
638
|
var init_session = __esm({
|
|
632
639
|
"src/core/session.ts"() {
|
|
633
640
|
"use strict";
|
|
@@ -636,6 +643,8 @@ var init_session = __esm({
|
|
|
636
643
|
HYDRA_ID_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
637
644
|
generateHydraId = customAlphabet(HYDRA_ID_ALPHABET, 16);
|
|
638
645
|
HYDRA_SESSION_PREFIX = "hydra_session_";
|
|
646
|
+
MAX_HISTORY_ENTRIES = 1e3;
|
|
647
|
+
COMPACT_EVERY = 200;
|
|
639
648
|
Session = class {
|
|
640
649
|
sessionId;
|
|
641
650
|
cwd;
|
|
@@ -655,8 +664,8 @@ var init_session = __esm({
|
|
|
655
664
|
currentModel;
|
|
656
665
|
currentMode;
|
|
657
666
|
updatedAt;
|
|
667
|
+
createdAt;
|
|
658
668
|
clients = /* @__PURE__ */ new Map();
|
|
659
|
-
history = [];
|
|
660
669
|
historyStore;
|
|
661
670
|
promptQueue = [];
|
|
662
671
|
promptInFlight = false;
|
|
@@ -672,6 +681,15 @@ var init_session = __esm({
|
|
|
672
681
|
// True once we've observed our first session/prompt; gates the
|
|
673
682
|
// first-prompt-seeded title so subsequent prompts don't churn it.
|
|
674
683
|
firstPromptSeeded = false;
|
|
684
|
+
// Wall-clock when the active prompt started, undefined when idle.
|
|
685
|
+
// Bumped by broadcastPromptReceived, cleared by broadcastTurnComplete.
|
|
686
|
+
// Drives the mid-turn elapsed counter delivered to fresh attachers.
|
|
687
|
+
promptStartedAt;
|
|
688
|
+
// Counts appends since the last compaction. When it hits COMPACT_EVERY
|
|
689
|
+
// we ask the history store to trim the file to the most recent
|
|
690
|
+
// MAX_HISTORY_ENTRIES. Keeps file growth bounded without per-append
|
|
691
|
+
// file-size checks.
|
|
692
|
+
appendCount = 0;
|
|
675
693
|
// Permission requests that have been broadcast to one or more
|
|
676
694
|
// clients but have not yet resolved. Replayed to clients that
|
|
677
695
|
// attach mid-flight so a late joiner sees the prompt instead of an
|
|
@@ -686,6 +704,12 @@ var init_session = __esm({
|
|
|
686
704
|
internalPromptCapture;
|
|
687
705
|
idleTimeoutMs;
|
|
688
706
|
idleTimer;
|
|
707
|
+
// Time of the last recordable broadcast (or session creation, if
|
|
708
|
+
// none yet). Drives the inactivity-based idle close; deliberately
|
|
709
|
+
// does NOT include snapshot state pings (model/mode/title/commands)
|
|
710
|
+
// or attach/detach, which would otherwise let passive observers
|
|
711
|
+
// and noisy state churn keep a quiet session alive forever.
|
|
712
|
+
lastRecordedAt;
|
|
689
713
|
spawnReplacementAgent;
|
|
690
714
|
agentChangeHandlers = [];
|
|
691
715
|
// Last available_commands_update we observed from the agent. Stored
|
|
@@ -720,11 +744,11 @@ var init_session = __esm({
|
|
|
720
744
|
this.firstPromptSeeded = true;
|
|
721
745
|
}
|
|
722
746
|
this.historyStore = init.historyStore;
|
|
723
|
-
if (init.seedHistory && init.seedHistory.length > 0) {
|
|
724
|
-
this.history = [...init.seedHistory];
|
|
725
|
-
}
|
|
726
747
|
this.updatedAt = Date.now();
|
|
748
|
+
this.createdAt = init.createdAt ?? this.updatedAt;
|
|
749
|
+
this.lastRecordedAt = this.updatedAt;
|
|
727
750
|
this.wireAgent(this.agent);
|
|
751
|
+
this.scheduleIdleCheck();
|
|
728
752
|
}
|
|
729
753
|
broadcastMergedCommands() {
|
|
730
754
|
const merged = [
|
|
@@ -784,34 +808,20 @@ var init_session = __esm({
|
|
|
784
808
|
return this.clients.size;
|
|
785
809
|
}
|
|
786
810
|
// Wall-clock when the in-flight agent turn began, or undefined when
|
|
787
|
-
// idle.
|
|
788
|
-
//
|
|
789
|
-
//
|
|
790
|
-
// so a fresh client reattaching mid-turn boots up with the busy
|
|
791
|
-
// banner showing real elapsed time.
|
|
811
|
+
// idle. Tracked in-memory by broadcastPromptReceived/broadcastTurnComplete
|
|
812
|
+
// so the daemon can hand a fresh attacher mid-turn the right elapsed
|
|
813
|
+
// time without scanning history.
|
|
792
814
|
get turnStartedAt() {
|
|
793
|
-
|
|
794
|
-
const entry = this.history[i];
|
|
795
|
-
if (!entry) {
|
|
796
|
-
continue;
|
|
797
|
-
}
|
|
798
|
-
const params = entry.params;
|
|
799
|
-
const kind = params?.update?.sessionUpdate;
|
|
800
|
-
if (kind === "turn_complete") {
|
|
801
|
-
return void 0;
|
|
802
|
-
}
|
|
803
|
-
if (kind === "prompt_received") {
|
|
804
|
-
return entry.recordedAt;
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
return void 0;
|
|
815
|
+
return this.promptStartedAt;
|
|
808
816
|
}
|
|
809
|
-
//
|
|
810
|
-
//
|
|
811
|
-
//
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
817
|
+
// Read the persisted history from disk. Returns [] if no history
|
|
818
|
+
// file exists (fresh session, never prompted). Used by attach() and
|
|
819
|
+
// the HTTP /history endpoint.
|
|
820
|
+
async getHistorySnapshot() {
|
|
821
|
+
if (!this.historyStore) {
|
|
822
|
+
return [];
|
|
823
|
+
}
|
|
824
|
+
return this.historyStore.load(this.sessionId).catch(() => []);
|
|
815
825
|
}
|
|
816
826
|
// Subscribe to recordable broadcast entries — fires once per entry
|
|
817
827
|
// that lands in history (so snapshot-shaped session_info/model/mode/
|
|
@@ -827,6 +837,10 @@ var init_session = __esm({
|
|
|
827
837
|
}
|
|
828
838
|
};
|
|
829
839
|
}
|
|
840
|
+
// Register a client and (asynchronously) load the replay slice it
|
|
841
|
+
// should receive. Validation errors throw synchronously so callers
|
|
842
|
+
// can rely on either the registration being in effect or having
|
|
843
|
+
// thrown; the disk-load is the only async work.
|
|
830
844
|
attach(client, historyPolicy) {
|
|
831
845
|
if (this.closed) {
|
|
832
846
|
throw withCode(
|
|
@@ -842,14 +856,10 @@ var init_session = __esm({
|
|
|
842
856
|
}
|
|
843
857
|
this.clients.set(client.clientId, client);
|
|
844
858
|
this.updatedAt = Date.now();
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
return [];
|
|
848
|
-
}
|
|
849
|
-
if (historyPolicy === "pending_only") {
|
|
850
|
-
return [];
|
|
859
|
+
if (historyPolicy === "none" || historyPolicy === "pending_only") {
|
|
860
|
+
return Promise.resolve([]);
|
|
851
861
|
}
|
|
852
|
-
return
|
|
862
|
+
return this.getHistorySnapshot();
|
|
853
863
|
}
|
|
854
864
|
// Dispatch in-flight permission requests to a freshly-attached
|
|
855
865
|
// client. Called by the daemon's WS handler *after* it finishes
|
|
@@ -863,7 +873,6 @@ var init_session = __esm({
|
|
|
863
873
|
detach(clientId) {
|
|
864
874
|
if (this.clients.delete(clientId)) {
|
|
865
875
|
this.updatedAt = Date.now();
|
|
866
|
-
this.maybeStartIdleTimer();
|
|
867
876
|
}
|
|
868
877
|
}
|
|
869
878
|
async prompt(clientId, params) {
|
|
@@ -909,6 +918,7 @@ var init_session = __esm({
|
|
|
909
918
|
if (client.clientInfo?.version) {
|
|
910
919
|
sentBy.version = client.clientInfo.version;
|
|
911
920
|
}
|
|
921
|
+
this.promptStartedAt = Date.now();
|
|
912
922
|
this.recordAndBroadcast(
|
|
913
923
|
"session/update",
|
|
914
924
|
{
|
|
@@ -945,6 +955,7 @@ var init_session = __esm({
|
|
|
945
955
|
if (stopReason !== void 0) {
|
|
946
956
|
update.stopReason = stopReason;
|
|
947
957
|
}
|
|
958
|
+
this.promptStartedAt = void 0;
|
|
948
959
|
this.recordAndBroadcast(
|
|
949
960
|
"session/update",
|
|
950
961
|
{
|
|
@@ -1130,6 +1141,12 @@ var init_session = __esm({
|
|
|
1130
1141
|
mergedAvailableCommands() {
|
|
1131
1142
|
return [...hydraCommandsAsAdvertised(), ...this.agentAdvertisedCommands];
|
|
1132
1143
|
}
|
|
1144
|
+
// The agent's own advertised commands (not merged with hydra verbs).
|
|
1145
|
+
// Used by SessionManager to persist into meta.json so cold resurrect
|
|
1146
|
+
// can re-deliver via the attach response _meta.
|
|
1147
|
+
agentOnlyAdvertisedCommands() {
|
|
1148
|
+
return [...this.agentAdvertisedCommands];
|
|
1149
|
+
}
|
|
1133
1150
|
// Pick up an agent-emitted session_info_update and store its title
|
|
1134
1151
|
// as our canonical record. The notification is also forwarded to
|
|
1135
1152
|
// clients via the surrounding recordAndBroadcast call. Authoritative
|
|
@@ -1261,7 +1278,7 @@ var init_session = __esm({
|
|
|
1261
1278
|
const spawnAgent = this.spawnReplacementAgent;
|
|
1262
1279
|
return this.enqueuePrompt(async () => {
|
|
1263
1280
|
const oldAgentId = this.agentId;
|
|
1264
|
-
const transcript = this.buildSwitchTranscript(oldAgentId);
|
|
1281
|
+
const transcript = await this.buildSwitchTranscript(oldAgentId);
|
|
1265
1282
|
const fresh = await spawnAgent({
|
|
1266
1283
|
agentId: newAgentId,
|
|
1267
1284
|
cwd: this.cwd,
|
|
@@ -1293,15 +1310,20 @@ var init_session = __esm({
|
|
|
1293
1310
|
return { stopReason: "end_turn" };
|
|
1294
1311
|
});
|
|
1295
1312
|
}
|
|
1296
|
-
// Walk
|
|
1297
|
-
//
|
|
1298
|
-
//
|
|
1299
|
-
//
|
|
1300
|
-
//
|
|
1301
|
-
//
|
|
1302
|
-
|
|
1313
|
+
// Walk the persisted history and produce a labeled transcript suitable
|
|
1314
|
+
// for handing to a fresh agent. Includes user prompts, agent replies,
|
|
1315
|
+
// and tool-call outcomes; skips hydra-synthesized markers (so multi-hop
|
|
1316
|
+
// switches don't accumulate banners) and other update kinds we don't
|
|
1317
|
+
// think the next agent benefits from re-seeing (plans, thoughts,
|
|
1318
|
+
// mode/model/usage).
|
|
1319
|
+
//
|
|
1320
|
+
// The header text defaults to the agent-swap framing; callers like
|
|
1321
|
+
// seedFromImport pass a custom header when the new agent is taking
|
|
1322
|
+
// over an imported session rather than swapping mid-conversation.
|
|
1323
|
+
async buildSwitchTranscript(prevAgentId, headerOverride) {
|
|
1303
1324
|
const lines = [];
|
|
1304
|
-
|
|
1325
|
+
const history = await this.getHistorySnapshot();
|
|
1326
|
+
for (const note of history) {
|
|
1305
1327
|
if (note.method !== "session/update") {
|
|
1306
1328
|
continue;
|
|
1307
1329
|
}
|
|
@@ -1355,29 +1377,53 @@ var init_session = __esm({
|
|
|
1355
1377
|
if (current) {
|
|
1356
1378
|
coalesced.push(`<${current.speaker}>: ${current.text.trim()}`);
|
|
1357
1379
|
}
|
|
1380
|
+
const intro = headerOverride?.intro ?? `You are taking over this conversation from ${prevAgentId}. Below is the transcript so far.`;
|
|
1381
|
+
const followup = headerOverride?.followup ?? `Each line is prefixed with its speaker. Continue from where ${prevAgentId} left off, responding to the user's most recent message.`;
|
|
1358
1382
|
return [
|
|
1359
|
-
|
|
1360
|
-
|
|
1383
|
+
intro,
|
|
1384
|
+
followup,
|
|
1361
1385
|
"",
|
|
1362
1386
|
"--- begin transcript ---",
|
|
1363
1387
|
...coalesced,
|
|
1364
1388
|
"--- end transcript ---"
|
|
1365
1389
|
].join("\n");
|
|
1366
1390
|
}
|
|
1391
|
+
// Replay the persisted history into a freshly-spawned agent so an
|
|
1392
|
+
// imported session has context. Called by SessionManager.doResurrect
|
|
1393
|
+
// on the first wake-up of a session whose meta.json has an empty
|
|
1394
|
+
// upstreamSessionId (the import marker). Wrapped in enqueuePrompt so
|
|
1395
|
+
// any user prompts arriving mid-seed queue behind it (mirrors the
|
|
1396
|
+
// /hydra switch path so the agent isn't asked to respond to a user
|
|
1397
|
+
// turn before it has absorbed the imported transcript). Best-effort:
|
|
1398
|
+
// if the agent fails to absorb the transcript we still leave the
|
|
1399
|
+
// session usable — the user just continues without context.
|
|
1400
|
+
async seedFromImport() {
|
|
1401
|
+
await this.enqueuePrompt(async () => {
|
|
1402
|
+
const transcript = await this.buildSwitchTranscript(this.agentId, {
|
|
1403
|
+
intro: "You are continuing a conversation that was imported from another hydra. Below is the transcript so far.",
|
|
1404
|
+
followup: "Each line is prefixed with its speaker. Treat this as context for the next user message; do not re-respond to earlier turns."
|
|
1405
|
+
});
|
|
1406
|
+
if (!transcript) {
|
|
1407
|
+
return void 0;
|
|
1408
|
+
}
|
|
1409
|
+
await this.runInternalPrompt(transcript).catch(() => void 0);
|
|
1410
|
+
return void 0;
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1367
1413
|
// Tell every attached client (a) the agent identity has changed
|
|
1368
|
-
// (session_info_update
|
|
1369
|
-
//
|
|
1370
|
-
//
|
|
1371
|
-
//
|
|
1372
|
-
//
|
|
1414
|
+
// (session_info_update carrying agentId inside _meta["hydra-acp"] —
|
|
1415
|
+
// the ACP schema for session_info_update is just title/updatedAt/_meta,
|
|
1416
|
+
// so non-hydra clients harmlessly ignore the extension; hydra-aware
|
|
1417
|
+
// ones read it and relabel) and (b) drop a visible banner into the
|
|
1418
|
+
// transcript so users see the switch rather than just suddenly getting
|
|
1419
|
+
// answers from a different agent. Both updates carry synthetic=true
|
|
1373
1420
|
// so a future /hydra switch's transcript builder filters them out.
|
|
1374
1421
|
broadcastAgentSwitch(oldAgentId, newAgentId) {
|
|
1375
1422
|
this.recordAndBroadcast("session/update", {
|
|
1376
1423
|
sessionId: this.sessionId,
|
|
1377
1424
|
update: {
|
|
1378
1425
|
sessionUpdate: "session_info_update",
|
|
1379
|
-
agentId: newAgentId
|
|
1380
|
-
_meta: { "hydra-acp": { synthetic: true } }
|
|
1426
|
+
_meta: { "hydra-acp": { synthetic: true, agentId: newAgentId } }
|
|
1381
1427
|
}
|
|
1382
1428
|
});
|
|
1383
1429
|
this.recordAndBroadcast("session/update", {
|
|
@@ -1408,22 +1454,55 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
1408
1454
|
handler(opts);
|
|
1409
1455
|
}
|
|
1410
1456
|
}
|
|
1411
|
-
|
|
1412
|
-
|
|
1457
|
+
// Last meaningful activity timestamp. Bumped only by recordable
|
|
1458
|
+
// broadcasts in recordAndBroadcast — the same signal historyMtimeIso
|
|
1459
|
+
// uses for the picker. Initialized at construction (and seeded from
|
|
1460
|
+
// the newest entry on resurrect) so the inactivity window starts
|
|
1461
|
+
// ticking from a sensible floor when there's no history yet.
|
|
1462
|
+
get lastActivityAt() {
|
|
1463
|
+
return this.lastRecordedAt;
|
|
1464
|
+
}
|
|
1465
|
+
// (Re-)arm the idle timer to fire when the inactivity window
|
|
1466
|
+
// elapses past lastActivityAt. Called once at construction and after
|
|
1467
|
+
// every recorded broadcast. The previous design gated on
|
|
1468
|
+
// clients.size === 0; we drop that gate because extensions
|
|
1469
|
+
// (slack/notifier/approver/browser) hold persistent attaches that
|
|
1470
|
+
// would otherwise keep a quiet session alive forever.
|
|
1471
|
+
scheduleIdleCheck() {
|
|
1472
|
+
if (this.closed || this.idleTimeoutMs <= 0) {
|
|
1413
1473
|
return;
|
|
1414
1474
|
}
|
|
1475
|
+
const dueAt = this.lastActivityAt + this.idleTimeoutMs;
|
|
1476
|
+
this.armIdleTimer(Math.max(0, dueAt - Date.now()));
|
|
1477
|
+
}
|
|
1478
|
+
armIdleTimer(delay) {
|
|
1415
1479
|
if (this.idleTimer) {
|
|
1416
|
-
|
|
1480
|
+
clearTimeout(this.idleTimer);
|
|
1417
1481
|
}
|
|
1418
1482
|
this.idleTimer = setTimeout(() => {
|
|
1419
1483
|
this.idleTimer = void 0;
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
}, this.idleTimeoutMs);
|
|
1484
|
+
this.checkIdle();
|
|
1485
|
+
}, delay);
|
|
1423
1486
|
if (typeof this.idleTimer.unref === "function") {
|
|
1424
1487
|
this.idleTimer.unref();
|
|
1425
1488
|
}
|
|
1426
1489
|
}
|
|
1490
|
+
checkIdle() {
|
|
1491
|
+
if (this.closed || this.idleTimeoutMs <= 0) {
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1494
|
+
if (this.turnStartedAt !== void 0 || this.inFlightPermissions.size > 0) {
|
|
1495
|
+
this.armIdleTimer(this.idleTimeoutMs);
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
const idle = Date.now() - this.lastActivityAt;
|
|
1499
|
+
if (idle < this.idleTimeoutMs) {
|
|
1500
|
+
this.armIdleTimer(this.idleTimeoutMs - idle);
|
|
1501
|
+
return;
|
|
1502
|
+
}
|
|
1503
|
+
const opts = this.firstPromptSeeded ? { deleteRecord: false, regenTitle: true } : { deleteRecord: true };
|
|
1504
|
+
void this.close(opts).catch(() => void 0);
|
|
1505
|
+
}
|
|
1427
1506
|
cancelIdleTimer() {
|
|
1428
1507
|
if (this.idleTimer) {
|
|
1429
1508
|
clearTimeout(this.idleTimer);
|
|
@@ -1448,17 +1527,14 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
1448
1527
|
params: rewritten,
|
|
1449
1528
|
recordedAt: Date.now()
|
|
1450
1529
|
};
|
|
1451
|
-
this.
|
|
1452
|
-
|
|
1453
|
-
if (this.history.length > 1e3) {
|
|
1454
|
-
this.history = this.history.slice(-500);
|
|
1455
|
-
trimmed = true;
|
|
1456
|
-
}
|
|
1530
|
+
this.lastRecordedAt = entry.recordedAt;
|
|
1531
|
+
this.appendCount += 1;
|
|
1457
1532
|
if (this.historyStore) {
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1533
|
+
const store = this.historyStore;
|
|
1534
|
+
void store.append(this.sessionId, entry).catch(() => void 0);
|
|
1535
|
+
if (this.appendCount >= COMPACT_EVERY) {
|
|
1536
|
+
this.appendCount = 0;
|
|
1537
|
+
void store.compact(this.sessionId, MAX_HISTORY_ENTRIES).catch(
|
|
1462
1538
|
() => void 0
|
|
1463
1539
|
);
|
|
1464
1540
|
}
|
|
@@ -1469,6 +1545,7 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
1469
1545
|
} catch {
|
|
1470
1546
|
}
|
|
1471
1547
|
}
|
|
1548
|
+
this.scheduleIdleCheck();
|
|
1472
1549
|
}
|
|
1473
1550
|
this.updatedAt = Date.now();
|
|
1474
1551
|
for (const client of this.clients.values()) {
|
|
@@ -1487,7 +1564,7 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
1487
1564
|
);
|
|
1488
1565
|
}
|
|
1489
1566
|
const clientParams = this.rewriteForClient(params);
|
|
1490
|
-
return new Promise((
|
|
1567
|
+
return new Promise((resolve4, reject) => {
|
|
1491
1568
|
let settled = false;
|
|
1492
1569
|
const outbound = [];
|
|
1493
1570
|
const entry = { addClient: sendTo };
|
|
@@ -1522,7 +1599,7 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
1522
1599
|
result
|
|
1523
1600
|
}).catch(() => void 0);
|
|
1524
1601
|
}
|
|
1525
|
-
|
|
1602
|
+
resolve4(result);
|
|
1526
1603
|
});
|
|
1527
1604
|
}).catch((err) => {
|
|
1528
1605
|
settle(() => reject(err));
|
|
@@ -1534,11 +1611,11 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
1534
1611
|
});
|
|
1535
1612
|
}
|
|
1536
1613
|
async enqueuePrompt(task) {
|
|
1537
|
-
return new Promise((
|
|
1614
|
+
return new Promise((resolve4, reject) => {
|
|
1538
1615
|
const run = async () => {
|
|
1539
1616
|
try {
|
|
1540
1617
|
const result = await task();
|
|
1541
|
-
|
|
1618
|
+
resolve4(result);
|
|
1542
1619
|
} catch (err) {
|
|
1543
1620
|
reject(err);
|
|
1544
1621
|
}
|
|
@@ -1573,6 +1650,64 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
1573
1650
|
}
|
|
1574
1651
|
});
|
|
1575
1652
|
|
|
1653
|
+
// src/tui/history.ts
|
|
1654
|
+
import { promises as fs6 } from "fs";
|
|
1655
|
+
import * as path3 from "path";
|
|
1656
|
+
async function loadHistory(file) {
|
|
1657
|
+
let text;
|
|
1658
|
+
try {
|
|
1659
|
+
text = await fs6.readFile(file, "utf8");
|
|
1660
|
+
} catch (err) {
|
|
1661
|
+
if (err.code === "ENOENT") {
|
|
1662
|
+
return [];
|
|
1663
|
+
}
|
|
1664
|
+
throw err;
|
|
1665
|
+
}
|
|
1666
|
+
return parseHistory(text);
|
|
1667
|
+
}
|
|
1668
|
+
function parseHistory(text) {
|
|
1669
|
+
const out = [];
|
|
1670
|
+
for (const rawLine of text.split("\n")) {
|
|
1671
|
+
if (rawLine.length === 0) {
|
|
1672
|
+
continue;
|
|
1673
|
+
}
|
|
1674
|
+
try {
|
|
1675
|
+
const decoded = JSON.parse(rawLine);
|
|
1676
|
+
if (typeof decoded === "string") {
|
|
1677
|
+
out.push(decoded);
|
|
1678
|
+
}
|
|
1679
|
+
} catch {
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
return out;
|
|
1683
|
+
}
|
|
1684
|
+
function appendEntry(history, entry) {
|
|
1685
|
+
const trimmed = entry.replace(/\n+$/, "");
|
|
1686
|
+
if (trimmed.length === 0) {
|
|
1687
|
+
return history;
|
|
1688
|
+
}
|
|
1689
|
+
if (history.length > 0 && history[history.length - 1] === trimmed) {
|
|
1690
|
+
return history;
|
|
1691
|
+
}
|
|
1692
|
+
const out = history.concat(trimmed);
|
|
1693
|
+
if (out.length > HISTORY_CAP) {
|
|
1694
|
+
return out.slice(out.length - HISTORY_CAP);
|
|
1695
|
+
}
|
|
1696
|
+
return out;
|
|
1697
|
+
}
|
|
1698
|
+
async function saveHistory(file, history) {
|
|
1699
|
+
await fs6.mkdir(path3.dirname(file), { recursive: true });
|
|
1700
|
+
const lines = history.map((entry) => JSON.stringify(entry));
|
|
1701
|
+
await fs6.writeFile(file, lines.length > 0 ? lines.join("\n") + "\n" : "");
|
|
1702
|
+
}
|
|
1703
|
+
var HISTORY_CAP;
|
|
1704
|
+
var init_history = __esm({
|
|
1705
|
+
"src/tui/history.ts"() {
|
|
1706
|
+
"use strict";
|
|
1707
|
+
HISTORY_CAP = 500;
|
|
1708
|
+
}
|
|
1709
|
+
});
|
|
1710
|
+
|
|
1576
1711
|
// src/acp/ws-stream.ts
|
|
1577
1712
|
function wsToMessageStream(ws) {
|
|
1578
1713
|
const messageHandlers = [];
|
|
@@ -1618,13 +1753,13 @@ function wsToMessageStream(ws) {
|
|
|
1618
1753
|
throw new Error("ws is closed");
|
|
1619
1754
|
}
|
|
1620
1755
|
const text = JSON.stringify(message);
|
|
1621
|
-
await new Promise((
|
|
1756
|
+
await new Promise((resolve4, reject) => {
|
|
1622
1757
|
ws.send(text, (err) => {
|
|
1623
1758
|
if (err) {
|
|
1624
1759
|
reject(err);
|
|
1625
1760
|
return;
|
|
1626
1761
|
}
|
|
1627
|
-
|
|
1762
|
+
resolve4();
|
|
1628
1763
|
});
|
|
1629
1764
|
});
|
|
1630
1765
|
},
|
|
@@ -1845,6 +1980,8 @@ var init_session_row = __esm({
|
|
|
1845
1980
|
});
|
|
1846
1981
|
|
|
1847
1982
|
// src/cli/commands/sessions.ts
|
|
1983
|
+
import * as fs11 from "fs/promises";
|
|
1984
|
+
import * as path5 from "path";
|
|
1848
1985
|
async function runSessionsList(opts = {}) {
|
|
1849
1986
|
const config = await loadConfig();
|
|
1850
1987
|
const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
@@ -1933,6 +2070,111 @@ async function runSessionsRm(id) {
|
|
|
1933
2070
|
process.stdout.write(`Removed ${id}
|
|
1934
2071
|
`);
|
|
1935
2072
|
}
|
|
2073
|
+
async function runSessionsExport(id, outPath) {
|
|
2074
|
+
if (!id) {
|
|
2075
|
+
process.stderr.write(
|
|
2076
|
+
"Usage: hydra-acp sessions export <session-id> [--out <file>]\n"
|
|
2077
|
+
);
|
|
2078
|
+
process.exit(2);
|
|
2079
|
+
}
|
|
2080
|
+
const config = await loadConfig();
|
|
2081
|
+
const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
2082
|
+
const response = await fetch(
|
|
2083
|
+
`${baseUrl}/v1/sessions/${encodeURIComponent(id)}/export`,
|
|
2084
|
+
{
|
|
2085
|
+
headers: { Authorization: `Bearer ${config.daemon.authToken}` }
|
|
2086
|
+
}
|
|
2087
|
+
);
|
|
2088
|
+
if (!response.ok) {
|
|
2089
|
+
const text = await response.text().catch(() => "");
|
|
2090
|
+
process.stderr.write(`Daemon returned HTTP ${response.status}: ${text}
|
|
2091
|
+
`);
|
|
2092
|
+
process.exit(1);
|
|
2093
|
+
}
|
|
2094
|
+
const body = await response.text();
|
|
2095
|
+
if (!outPath) {
|
|
2096
|
+
process.stdout.write(body);
|
|
2097
|
+
if (!body.endsWith("\n")) {
|
|
2098
|
+
process.stdout.write("\n");
|
|
2099
|
+
}
|
|
2100
|
+
return;
|
|
2101
|
+
}
|
|
2102
|
+
const resolved = outPath === "." ? deriveFilenameFrom(response, id) : outPath;
|
|
2103
|
+
await fs11.mkdir(path5.dirname(path5.resolve(resolved)), { recursive: true });
|
|
2104
|
+
await fs11.writeFile(resolved, body, { encoding: "utf8", mode: 384 });
|
|
2105
|
+
process.stdout.write(`Wrote ${resolved}
|
|
2106
|
+
`);
|
|
2107
|
+
}
|
|
2108
|
+
async function runSessionsImport(file, opts = {}) {
|
|
2109
|
+
if (!file) {
|
|
2110
|
+
process.stderr.write(
|
|
2111
|
+
"Usage: hydra-acp sessions import <file>|- [--replace]\n"
|
|
2112
|
+
);
|
|
2113
|
+
process.exit(2);
|
|
2114
|
+
}
|
|
2115
|
+
let body;
|
|
2116
|
+
if (file === "-") {
|
|
2117
|
+
body = await readStdin();
|
|
2118
|
+
} else {
|
|
2119
|
+
body = await fs11.readFile(file, "utf8");
|
|
2120
|
+
}
|
|
2121
|
+
let bundle;
|
|
2122
|
+
try {
|
|
2123
|
+
bundle = JSON.parse(body);
|
|
2124
|
+
} catch (err) {
|
|
2125
|
+
process.stderr.write(`Failed to parse bundle: ${err.message}
|
|
2126
|
+
`);
|
|
2127
|
+
process.exit(1);
|
|
2128
|
+
}
|
|
2129
|
+
const config = await loadConfig();
|
|
2130
|
+
const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
2131
|
+
const response = await fetch(`${baseUrl}/v1/sessions/import`, {
|
|
2132
|
+
method: "POST",
|
|
2133
|
+
headers: {
|
|
2134
|
+
"Content-Type": "application/json",
|
|
2135
|
+
Authorization: `Bearer ${config.daemon.authToken}`
|
|
2136
|
+
},
|
|
2137
|
+
body: JSON.stringify({ bundle, replace: opts.replace === true })
|
|
2138
|
+
});
|
|
2139
|
+
if (response.status === 409) {
|
|
2140
|
+
const detail = await response.json().catch(() => ({}));
|
|
2141
|
+
process.stderr.write(
|
|
2142
|
+
`Bundle already imported as ${detail.existingSessionId ?? "unknown"}. Use --replace to overwrite.
|
|
2143
|
+
`
|
|
2144
|
+
);
|
|
2145
|
+
process.exit(1);
|
|
2146
|
+
}
|
|
2147
|
+
if (!response.ok) {
|
|
2148
|
+
const text = await response.text().catch(() => "");
|
|
2149
|
+
process.stderr.write(`Daemon returned HTTP ${response.status}: ${text}
|
|
2150
|
+
`);
|
|
2151
|
+
process.exit(1);
|
|
2152
|
+
}
|
|
2153
|
+
const result = await response.json();
|
|
2154
|
+
process.stdout.write(
|
|
2155
|
+
result.replaced ? `Replaced ${result.sessionId} (from ${result.importedFromSessionId})
|
|
2156
|
+
` : `Imported as ${result.sessionId} (from ${result.importedFromSessionId})
|
|
2157
|
+
`
|
|
2158
|
+
);
|
|
2159
|
+
}
|
|
2160
|
+
async function readStdin() {
|
|
2161
|
+
const chunks = [];
|
|
2162
|
+
for await (const chunk of process.stdin) {
|
|
2163
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
2164
|
+
}
|
|
2165
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
2166
|
+
}
|
|
2167
|
+
function deriveFilenameFrom(response, id) {
|
|
2168
|
+
const cd = response.headers.get("content-disposition");
|
|
2169
|
+
if (cd) {
|
|
2170
|
+
const match = cd.match(/filename="([^"]+)"/);
|
|
2171
|
+
if (match) {
|
|
2172
|
+
return match[1];
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
2176
|
+
return `hydra-${id}-${stamp}.hydra`;
|
|
2177
|
+
}
|
|
1936
2178
|
function httpBase(host, port, tls) {
|
|
1937
2179
|
const protocol = tls ? "https" : "http";
|
|
1938
2180
|
return `${protocol}://${host}:${port}`;
|
|
@@ -1952,11 +2194,11 @@ function isResponse(msg) {
|
|
|
1952
2194
|
return !("method" in msg) && "id" in msg && msg.id !== void 0;
|
|
1953
2195
|
}
|
|
1954
2196
|
async function openWs(url, subprotocols) {
|
|
1955
|
-
return new Promise((
|
|
2197
|
+
return new Promise((resolve4, reject) => {
|
|
1956
2198
|
const ws = new WebSocket(url, subprotocols);
|
|
1957
2199
|
const onOpen = () => {
|
|
1958
2200
|
ws.off("error", onError);
|
|
1959
|
-
|
|
2201
|
+
resolve4(wsToMessageStream(ws));
|
|
1960
2202
|
};
|
|
1961
2203
|
const onError = (err) => {
|
|
1962
2204
|
ws.off("open", onOpen);
|
|
@@ -2027,8 +2269,8 @@ var init_resilient_ws = __esm({
|
|
|
2027
2269
|
throw new Error("resilient ws stream not connected");
|
|
2028
2270
|
}
|
|
2029
2271
|
const id = message.id;
|
|
2030
|
-
const promise = new Promise((
|
|
2031
|
-
this.pendingRequests.set(id, { resolve:
|
|
2272
|
+
const promise = new Promise((resolve4, reject) => {
|
|
2273
|
+
this.pendingRequests.set(id, { resolve: resolve4, reject });
|
|
2032
2274
|
});
|
|
2033
2275
|
try {
|
|
2034
2276
|
await this.current.send(message);
|
|
@@ -2056,8 +2298,8 @@ var init_resilient_ws = __esm({
|
|
|
2056
2298
|
this.bindStream(stream);
|
|
2057
2299
|
const wasFirst = this.firstConnect;
|
|
2058
2300
|
this.firstConnect = false;
|
|
2059
|
-
this.connectGate = new Promise((
|
|
2060
|
-
this.releaseConnectGate =
|
|
2301
|
+
this.connectGate = new Promise((resolve4) => {
|
|
2302
|
+
this.releaseConnectGate = resolve4;
|
|
2061
2303
|
});
|
|
2062
2304
|
try {
|
|
2063
2305
|
if (this.opts.onConnect) {
|
|
@@ -2180,64 +2422,6 @@ var init_resilient_ws = __esm({
|
|
|
2180
2422
|
}
|
|
2181
2423
|
});
|
|
2182
2424
|
|
|
2183
|
-
// src/tui/history.ts
|
|
2184
|
-
import { promises as fs10 } from "fs";
|
|
2185
|
-
import * as path4 from "path";
|
|
2186
|
-
async function loadHistory(file) {
|
|
2187
|
-
let text;
|
|
2188
|
-
try {
|
|
2189
|
-
text = await fs10.readFile(file, "utf8");
|
|
2190
|
-
} catch (err) {
|
|
2191
|
-
if (err.code === "ENOENT") {
|
|
2192
|
-
return [];
|
|
2193
|
-
}
|
|
2194
|
-
throw err;
|
|
2195
|
-
}
|
|
2196
|
-
return parseHistory(text);
|
|
2197
|
-
}
|
|
2198
|
-
function parseHistory(text) {
|
|
2199
|
-
const out = [];
|
|
2200
|
-
for (const rawLine of text.split("\n")) {
|
|
2201
|
-
if (rawLine.length === 0) {
|
|
2202
|
-
continue;
|
|
2203
|
-
}
|
|
2204
|
-
try {
|
|
2205
|
-
const decoded = JSON.parse(rawLine);
|
|
2206
|
-
if (typeof decoded === "string") {
|
|
2207
|
-
out.push(decoded);
|
|
2208
|
-
}
|
|
2209
|
-
} catch {
|
|
2210
|
-
}
|
|
2211
|
-
}
|
|
2212
|
-
return out;
|
|
2213
|
-
}
|
|
2214
|
-
function appendEntry(history, entry) {
|
|
2215
|
-
const trimmed = entry.replace(/\n+$/, "");
|
|
2216
|
-
if (trimmed.length === 0) {
|
|
2217
|
-
return history;
|
|
2218
|
-
}
|
|
2219
|
-
if (history.length > 0 && history[history.length - 1] === trimmed) {
|
|
2220
|
-
return history;
|
|
2221
|
-
}
|
|
2222
|
-
const out = history.concat(trimmed);
|
|
2223
|
-
if (out.length > HISTORY_CAP) {
|
|
2224
|
-
return out.slice(out.length - HISTORY_CAP);
|
|
2225
|
-
}
|
|
2226
|
-
return out;
|
|
2227
|
-
}
|
|
2228
|
-
async function saveHistory(file, history) {
|
|
2229
|
-
await fs10.mkdir(path4.dirname(file), { recursive: true });
|
|
2230
|
-
const lines = history.map((entry) => JSON.stringify(entry));
|
|
2231
|
-
await fs10.writeFile(file, lines.length > 0 ? lines.join("\n") + "\n" : "");
|
|
2232
|
-
}
|
|
2233
|
-
var HISTORY_CAP;
|
|
2234
|
-
var init_history = __esm({
|
|
2235
|
-
"src/tui/history.ts"() {
|
|
2236
|
-
"use strict";
|
|
2237
|
-
HISTORY_CAP = 500;
|
|
2238
|
-
}
|
|
2239
|
-
});
|
|
2240
|
-
|
|
2241
2425
|
// src/tui/discovery.ts
|
|
2242
2426
|
async function listSessions(config, opts = {}, fetchImpl = fetch) {
|
|
2243
2427
|
const base = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
@@ -2468,7 +2652,7 @@ async function pickSession(term, opts) {
|
|
|
2468
2652
|
};
|
|
2469
2653
|
renderFromScratch();
|
|
2470
2654
|
term.hideCursor();
|
|
2471
|
-
return await new Promise((
|
|
2655
|
+
return await new Promise((resolve4) => {
|
|
2472
2656
|
let resolved = false;
|
|
2473
2657
|
const onResize = () => {
|
|
2474
2658
|
if (resolved) {
|
|
@@ -2590,6 +2774,11 @@ async function pickSession(term, opts) {
|
|
|
2590
2774
|
}
|
|
2591
2775
|
clearTransient();
|
|
2592
2776
|
if (data?.isCharacter) {
|
|
2777
|
+
if (name === "r" || name === "R") {
|
|
2778
|
+
const currentId = selectedIdx > 0 ? visible[selectedIdx - 1]?.sessionId : void 0;
|
|
2779
|
+
void refresh(currentId);
|
|
2780
|
+
return;
|
|
2781
|
+
}
|
|
2593
2782
|
if ((name === "k" || name === "K") && selectedIdx > 0) {
|
|
2594
2783
|
const session = visible[selectedIdx - 1];
|
|
2595
2784
|
if (!session) {
|
|
@@ -2650,12 +2839,12 @@ async function pickSession(term, opts) {
|
|
|
2650
2839
|
case "KP_ENTER": {
|
|
2651
2840
|
cleanup();
|
|
2652
2841
|
if (selectedIdx === 0) {
|
|
2653
|
-
|
|
2842
|
+
resolve4({ kind: "new" });
|
|
2654
2843
|
return;
|
|
2655
2844
|
}
|
|
2656
2845
|
const session = visible[selectedIdx - 1];
|
|
2657
2846
|
if (!session) {
|
|
2658
|
-
|
|
2847
|
+
resolve4({ kind: "abort" });
|
|
2659
2848
|
return;
|
|
2660
2849
|
}
|
|
2661
2850
|
const result = {
|
|
@@ -2665,13 +2854,13 @@ async function pickSession(term, opts) {
|
|
|
2665
2854
|
if (session.agentId !== void 0) {
|
|
2666
2855
|
result.agentId = session.agentId;
|
|
2667
2856
|
}
|
|
2668
|
-
|
|
2857
|
+
resolve4(result);
|
|
2669
2858
|
return;
|
|
2670
2859
|
}
|
|
2671
2860
|
case "ESCAPE":
|
|
2672
2861
|
case "CTRL_C":
|
|
2673
2862
|
cleanup();
|
|
2674
|
-
|
|
2863
|
+
resolve4({ kind: "abort" });
|
|
2675
2864
|
return;
|
|
2676
2865
|
}
|
|
2677
2866
|
};
|
|
@@ -2853,6 +3042,12 @@ function wrap(text, width) {
|
|
|
2853
3042
|
if (text.length === 0) {
|
|
2854
3043
|
return [""];
|
|
2855
3044
|
}
|
|
3045
|
+
if (!NON_ASCII.test(text)) {
|
|
3046
|
+
return wrapAscii(text, width);
|
|
3047
|
+
}
|
|
3048
|
+
return wrapVisible(text, width);
|
|
3049
|
+
}
|
|
3050
|
+
function wrapAscii(text, width) {
|
|
2856
3051
|
const out = [];
|
|
2857
3052
|
let remaining = text;
|
|
2858
3053
|
while (remaining.length > width) {
|
|
@@ -2875,14 +3070,78 @@ function wrap(text, width) {
|
|
|
2875
3070
|
out.push(remaining);
|
|
2876
3071
|
return out;
|
|
2877
3072
|
}
|
|
3073
|
+
function wrapVisible(text, width) {
|
|
3074
|
+
const out = [];
|
|
3075
|
+
const graphemes = [];
|
|
3076
|
+
for (const { segment } of SEGMENTER.segment(text)) {
|
|
3077
|
+
graphemes.push({ seg: segment, w: stringWidth(segment) });
|
|
3078
|
+
}
|
|
3079
|
+
let i = 0;
|
|
3080
|
+
while (i < graphemes.length) {
|
|
3081
|
+
let chunk = "";
|
|
3082
|
+
let chunkW = 0;
|
|
3083
|
+
let lastSpaceI = -1;
|
|
3084
|
+
let chunkAtLastSpace = "";
|
|
3085
|
+
while (i < graphemes.length) {
|
|
3086
|
+
const g = graphemes[i];
|
|
3087
|
+
if (chunkW + g.w > width) {
|
|
3088
|
+
break;
|
|
3089
|
+
}
|
|
3090
|
+
if (g.seg === " ") {
|
|
3091
|
+
lastSpaceI = i;
|
|
3092
|
+
chunkAtLastSpace = chunk;
|
|
3093
|
+
}
|
|
3094
|
+
chunk += g.seg;
|
|
3095
|
+
chunkW += g.w;
|
|
3096
|
+
i += 1;
|
|
3097
|
+
}
|
|
3098
|
+
if (i >= graphemes.length) {
|
|
3099
|
+
out.push(chunk);
|
|
3100
|
+
break;
|
|
3101
|
+
}
|
|
3102
|
+
if (lastSpaceI >= 0) {
|
|
3103
|
+
out.push(chunkAtLastSpace);
|
|
3104
|
+
i = lastSpaceI + 1;
|
|
3105
|
+
} else if (chunk.length === 0) {
|
|
3106
|
+
out.push(graphemes[i].seg);
|
|
3107
|
+
i += 1;
|
|
3108
|
+
} else {
|
|
3109
|
+
out.push(chunk);
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
3112
|
+
return out;
|
|
3113
|
+
}
|
|
2878
3114
|
function truncate(text, max) {
|
|
2879
|
-
if (
|
|
3115
|
+
if (max <= 0) {
|
|
3116
|
+
return "";
|
|
3117
|
+
}
|
|
3118
|
+
if (text.length <= max && !NON_ASCII.test(text)) {
|
|
3119
|
+
return text;
|
|
3120
|
+
}
|
|
3121
|
+
const visible = stringWidth(text);
|
|
3122
|
+
if (visible <= max) {
|
|
2880
3123
|
return text;
|
|
2881
3124
|
}
|
|
2882
3125
|
if (max <= 1) {
|
|
2883
|
-
return text
|
|
3126
|
+
return takeByWidth(text, max);
|
|
2884
3127
|
}
|
|
2885
|
-
return text
|
|
3128
|
+
return takeByWidth(text, max - 1) + "\u2026";
|
|
3129
|
+
}
|
|
3130
|
+
function takeByWidth(text, budget) {
|
|
3131
|
+
if (budget <= 0) {
|
|
3132
|
+
return "";
|
|
3133
|
+
}
|
|
3134
|
+
let out = "";
|
|
3135
|
+
let used = 0;
|
|
3136
|
+
for (const { segment } of SEGMENTER.segment(text)) {
|
|
3137
|
+
const w = stringWidth(segment);
|
|
3138
|
+
if (used + w > budget) {
|
|
3139
|
+
break;
|
|
3140
|
+
}
|
|
3141
|
+
out += segment;
|
|
3142
|
+
used += w;
|
|
3143
|
+
}
|
|
3144
|
+
return out;
|
|
2886
3145
|
}
|
|
2887
3146
|
function firstLine2(text) {
|
|
2888
3147
|
const idx = text.indexOf("\n");
|
|
@@ -2997,7 +3256,7 @@ function mapKeyName(name) {
|
|
|
2997
3256
|
return null;
|
|
2998
3257
|
}
|
|
2999
3258
|
}
|
|
3000
|
-
var HEADER_ROWS, BANNER_ROWS, SEPARATOR_ROWS, MAX_PROMPT_ROWS, MAX_QUEUED_ROWS, MAX_PERMISSION_ROWS, MAX_COMPLETION_ROWS, CONFIRM_PROMPT_ROWS, DEFAULT_CONTENT_REPAINT_THROTTLE_MS, DEFAULT_MAX_SCROLLBACK_LINES, Screen, shortId;
|
|
3259
|
+
var HEADER_ROWS, BANNER_ROWS, SEPARATOR_ROWS, MAX_PROMPT_ROWS, MAX_QUEUED_ROWS, MAX_PERMISSION_ROWS, MAX_COMPLETION_ROWS, CONFIRM_PROMPT_ROWS, DEFAULT_CONTENT_REPAINT_THROTTLE_MS, DEFAULT_MAX_SCROLLBACK_LINES, Screen, NON_ASCII, SEGMENTER, shortId;
|
|
3001
3260
|
var init_screen = __esm({
|
|
3002
3261
|
"src/tui/screen.ts"() {
|
|
3003
3262
|
"use strict";
|
|
@@ -3100,6 +3359,11 @@ var init_screen = __esm({
|
|
|
3100
3359
|
}
|
|
3101
3360
|
this.started = true;
|
|
3102
3361
|
this.term.fullscreen(true);
|
|
3362
|
+
this.lastFrameRows.clear();
|
|
3363
|
+
this.lastFrameW = 0;
|
|
3364
|
+
this.lastFrameH = 0;
|
|
3365
|
+
this.lastWindowTitle = null;
|
|
3366
|
+
process.stdout.write("\x1B[?7l");
|
|
3103
3367
|
this.term.grabInput({ mouse: "button" });
|
|
3104
3368
|
this.term.hideCursor(false);
|
|
3105
3369
|
this.term.on("key", this.keyHandler);
|
|
@@ -3119,6 +3383,7 @@ var init_screen = __esm({
|
|
|
3119
3383
|
this.term.off("resize", this.resizeHandler);
|
|
3120
3384
|
this.term.grabInput(false);
|
|
3121
3385
|
this.term.hideCursor(false);
|
|
3386
|
+
process.stdout.write("\x1B[?7h");
|
|
3122
3387
|
this.term.fullscreen(false);
|
|
3123
3388
|
this.term("\n");
|
|
3124
3389
|
}
|
|
@@ -3430,6 +3695,22 @@ var init_screen = __esm({
|
|
|
3430
3695
|
redraw() {
|
|
3431
3696
|
this.repaint();
|
|
3432
3697
|
}
|
|
3698
|
+
// Forced clean-slate repaint. Drops the per-row signature cache, the
|
|
3699
|
+
// window-title cache, the wrap cache, and clears the terminal before
|
|
3700
|
+
// painting. Wired to ^L so the user can recover when something has
|
|
3701
|
+
// corrupted the visible state and the per-row sig check otherwise
|
|
3702
|
+
// short-circuits the re-emit.
|
|
3703
|
+
fullRedraw() {
|
|
3704
|
+
this.lastFrameRows.clear();
|
|
3705
|
+
this.lastFrameW = 0;
|
|
3706
|
+
this.lastFrameH = 0;
|
|
3707
|
+
this.lastWindowTitle = null;
|
|
3708
|
+
this.wrapCache.clear();
|
|
3709
|
+
this.wrapCacheWidth = 0;
|
|
3710
|
+
process.stdout.write("\x1B[?7l");
|
|
3711
|
+
this.term.clear();
|
|
3712
|
+
this.repaint();
|
|
3713
|
+
}
|
|
3433
3714
|
// While paused, append* methods buffer state but don't repaint. Calls are
|
|
3434
3715
|
// counter-based so they nest safely. Resume triggers exactly one repaint
|
|
3435
3716
|
// if any was requested while paused.
|
|
@@ -4083,6 +4364,8 @@ var init_screen = __esm({
|
|
|
4083
4364
|
}
|
|
4084
4365
|
}
|
|
4085
4366
|
};
|
|
4367
|
+
NON_ASCII = /[^\x20-\x7e]/;
|
|
4368
|
+
SEGMENTER = new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
4086
4369
|
shortId = stripHydraSessionPrefix;
|
|
4087
4370
|
}
|
|
4088
4371
|
});
|
|
@@ -4500,10 +4783,28 @@ function mapUpdate(update) {
|
|
|
4500
4783
|
}
|
|
4501
4784
|
function mapSessionInfo(u) {
|
|
4502
4785
|
const title = readString(u, "title");
|
|
4503
|
-
|
|
4786
|
+
const meta = u._meta;
|
|
4787
|
+
let agentId;
|
|
4788
|
+
if (meta && typeof meta === "object" && !Array.isArray(meta)) {
|
|
4789
|
+
const ns = meta["hydra-acp"];
|
|
4790
|
+
if (ns && typeof ns === "object" && !Array.isArray(ns)) {
|
|
4791
|
+
const candidate = ns.agentId;
|
|
4792
|
+
if (typeof candidate === "string") {
|
|
4793
|
+
agentId = candidate;
|
|
4794
|
+
}
|
|
4795
|
+
}
|
|
4796
|
+
}
|
|
4797
|
+
if (title === void 0 && agentId === void 0) {
|
|
4504
4798
|
return null;
|
|
4505
4799
|
}
|
|
4506
|
-
|
|
4800
|
+
const event = { kind: "session-info" };
|
|
4801
|
+
if (title !== void 0) {
|
|
4802
|
+
event.title = title;
|
|
4803
|
+
}
|
|
4804
|
+
if (agentId !== void 0) {
|
|
4805
|
+
event.agentId = agentId;
|
|
4806
|
+
}
|
|
4807
|
+
return event;
|
|
4507
4808
|
}
|
|
4508
4809
|
function mapAvailableCommands(u) {
|
|
4509
4810
|
const list = u.availableCommands ?? u.commands;
|
|
@@ -4907,8 +5208,9 @@ function formatToolLine(state) {
|
|
|
4907
5208
|
title = `${initial} \xB7 ${latest}`;
|
|
4908
5209
|
}
|
|
4909
5210
|
return {
|
|
4910
|
-
prefix:
|
|
4911
|
-
|
|
5211
|
+
prefix: ` ${toolStatusIcon(state.status)} `,
|
|
5212
|
+
prefixStyle: toolIconStyle(state.status),
|
|
5213
|
+
body: title,
|
|
4912
5214
|
bodyStyle: toolStatusStyle(state.status)
|
|
4913
5215
|
};
|
|
4914
5216
|
}
|
|
@@ -4933,6 +5235,22 @@ function toolStatusIcon(status) {
|
|
|
4933
5235
|
return "\u25D0";
|
|
4934
5236
|
}
|
|
4935
5237
|
}
|
|
5238
|
+
function toolIconStyle(status) {
|
|
5239
|
+
switch (status) {
|
|
5240
|
+
case "completed":
|
|
5241
|
+
case "succeeded":
|
|
5242
|
+
case "ok":
|
|
5243
|
+
return "tool-status-ok";
|
|
5244
|
+
case "failed":
|
|
5245
|
+
case "error":
|
|
5246
|
+
case "rejected":
|
|
5247
|
+
return "tool-status-fail";
|
|
5248
|
+
case "cancelled":
|
|
5249
|
+
return "tool-status-cancelled";
|
|
5250
|
+
default:
|
|
5251
|
+
return "tool-status-running";
|
|
5252
|
+
}
|
|
5253
|
+
}
|
|
4936
5254
|
function formatPlan(event) {
|
|
4937
5255
|
if (event.entries.length === 0) {
|
|
4938
5256
|
return [
|
|
@@ -5019,12 +5337,18 @@ async function runTuiApp(opts) {
|
|
|
5019
5337
|
const config = await ensureConfig();
|
|
5020
5338
|
await ensureDaemonReachable(config);
|
|
5021
5339
|
const term = termkit.terminal;
|
|
5340
|
+
const exitHint = {};
|
|
5022
5341
|
let nextOpts = opts;
|
|
5023
5342
|
while (nextOpts !== null) {
|
|
5024
|
-
nextOpts = await runSession(term, config, nextOpts);
|
|
5343
|
+
nextOpts = await runSession(term, config, nextOpts, exitHint);
|
|
5344
|
+
}
|
|
5345
|
+
if (exitHint.sessionId) {
|
|
5346
|
+
const short = stripHydraSessionPrefix(exitHint.sessionId);
|
|
5347
|
+
process.stdout.write(`To resume: hydra-acp tui --resume ${short}
|
|
5348
|
+
`);
|
|
5025
5349
|
}
|
|
5026
5350
|
}
|
|
5027
|
-
async function runSession(term, config, opts) {
|
|
5351
|
+
async function runSession(term, config, opts, exitHint) {
|
|
5028
5352
|
const ctx = await resolveSession(term, config, opts);
|
|
5029
5353
|
if (!ctx) {
|
|
5030
5354
|
term.grabInput(false);
|
|
@@ -5132,10 +5456,10 @@ async function runSession(term, config, opts) {
|
|
|
5132
5456
|
if (pendingPermission.toolCallId && toolCallId && pendingPermission.toolCallId !== toolCallId) {
|
|
5133
5457
|
return;
|
|
5134
5458
|
}
|
|
5135
|
-
const
|
|
5459
|
+
const resolve4 = pendingPermission.resolve;
|
|
5136
5460
|
pendingPermission = null;
|
|
5137
5461
|
screen.setPermissionPrompt(null);
|
|
5138
|
-
|
|
5462
|
+
resolve4(result ?? { outcome: { outcome: "cancelled" } });
|
|
5139
5463
|
};
|
|
5140
5464
|
const maybeDismissPermissionByToolUpdate = (update) => {
|
|
5141
5465
|
if (!pendingPermission?.toolCallId) {
|
|
@@ -5168,14 +5492,14 @@ async function runSession(term, config, opts) {
|
|
|
5168
5492
|
if (!pendingPermission) {
|
|
5169
5493
|
return;
|
|
5170
5494
|
}
|
|
5171
|
-
const { options, resolve:
|
|
5495
|
+
const { options, resolve: resolve4 } = pendingPermission;
|
|
5172
5496
|
pendingPermission = null;
|
|
5173
5497
|
screen.setPermissionPrompt(null);
|
|
5174
5498
|
if (optionId === null) {
|
|
5175
|
-
|
|
5499
|
+
resolve4({ outcome: { outcome: "cancelled" } });
|
|
5176
5500
|
return;
|
|
5177
5501
|
}
|
|
5178
|
-
|
|
5502
|
+
resolve4({ outcome: { outcome: "selected", optionId } });
|
|
5179
5503
|
void options;
|
|
5180
5504
|
};
|
|
5181
5505
|
conn.onRequest("session/request_permission", async (params) => {
|
|
@@ -5193,12 +5517,12 @@ async function runSession(term, config, opts) {
|
|
|
5193
5517
|
]);
|
|
5194
5518
|
return { outcome: { outcome: "cancelled" } };
|
|
5195
5519
|
}
|
|
5196
|
-
return new Promise((
|
|
5520
|
+
return new Promise((resolve4) => {
|
|
5197
5521
|
pendingPermission = {
|
|
5198
5522
|
title,
|
|
5199
5523
|
options,
|
|
5200
5524
|
selectedIndex: 0,
|
|
5201
|
-
resolve:
|
|
5525
|
+
resolve: resolve4,
|
|
5202
5526
|
toolCallId
|
|
5203
5527
|
};
|
|
5204
5528
|
refreshPermissionPrompt();
|
|
@@ -5236,6 +5560,7 @@ async function runSession(term, config, opts) {
|
|
|
5236
5560
|
...opts.name ? { _meta: { [HYDRA_META_KEY]: { name: opts.name } } } : {}
|
|
5237
5561
|
});
|
|
5238
5562
|
resolvedSessionId = created.sessionId;
|
|
5563
|
+
exitHint.sessionId = resolvedSessionId;
|
|
5239
5564
|
const hydraMeta = extractHydraMeta(created._meta ?? void 0);
|
|
5240
5565
|
upstreamSessionId = hydraMeta.upstreamSessionId;
|
|
5241
5566
|
if (hydraMeta.agentId) {
|
|
@@ -5262,6 +5587,7 @@ async function runSession(term, config, opts) {
|
|
|
5262
5587
|
clientInfo: { name: "hydra-acp-tui", version: "0.1.0" }
|
|
5263
5588
|
});
|
|
5264
5589
|
resolvedSessionId = attached.sessionId;
|
|
5590
|
+
exitHint.sessionId = resolvedSessionId;
|
|
5265
5591
|
const hydraMeta = extractHydraMeta(attached._meta ?? void 0);
|
|
5266
5592
|
upstreamSessionId = hydraMeta.upstreamSessionId;
|
|
5267
5593
|
if (hydraMeta.agentId) {
|
|
@@ -5458,8 +5784,8 @@ async function runSession(term, config, opts) {
|
|
|
5458
5784
|
screen.appendLines(formatEvent({ kind: "model-changed", model: initialModel }));
|
|
5459
5785
|
}
|
|
5460
5786
|
let finishSession = null;
|
|
5461
|
-
const sessionDone = new Promise((
|
|
5462
|
-
finishSession =
|
|
5787
|
+
const sessionDone = new Promise((resolve4) => {
|
|
5788
|
+
finishSession = resolve4;
|
|
5463
5789
|
});
|
|
5464
5790
|
const cancelRemoteTurn = () => {
|
|
5465
5791
|
conn.notify("session/cancel", { sessionId: resolvedSessionId }).catch(() => void 0);
|
|
@@ -5631,7 +5957,7 @@ async function runSession(term, config, opts) {
|
|
|
5631
5957
|
screen.setBanner({});
|
|
5632
5958
|
return;
|
|
5633
5959
|
case "redraw":
|
|
5634
|
-
screen.
|
|
5960
|
+
screen.fullRedraw();
|
|
5635
5961
|
return;
|
|
5636
5962
|
case "switch-session":
|
|
5637
5963
|
void switchSession();
|
|
@@ -5957,6 +6283,10 @@ async function runSession(term, config, opts) {
|
|
|
5957
6283
|
if (event.title !== void 0) {
|
|
5958
6284
|
screen.setHeader({ title: event.title });
|
|
5959
6285
|
}
|
|
6286
|
+
if (event.agentId !== void 0 && event.agentId !== resolvedAgentId) {
|
|
6287
|
+
resolvedAgentId = event.agentId;
|
|
6288
|
+
screen.setHeader({ agent: event.agentId });
|
|
6289
|
+
}
|
|
5960
6290
|
return;
|
|
5961
6291
|
}
|
|
5962
6292
|
if (event.kind === "usage-update") {
|
|
@@ -6077,10 +6407,10 @@ async function runSession(term, config, opts) {
|
|
|
6077
6407
|
}
|
|
6078
6408
|
const resetInFlightUiState = () => {
|
|
6079
6409
|
if (pendingPermission) {
|
|
6080
|
-
const
|
|
6410
|
+
const resolve4 = pendingPermission.resolve;
|
|
6081
6411
|
pendingPermission = null;
|
|
6082
6412
|
screen.setPermissionPrompt(null);
|
|
6083
|
-
|
|
6413
|
+
resolve4({ outcome: { outcome: "cancelled" } });
|
|
6084
6414
|
}
|
|
6085
6415
|
closeAgentText();
|
|
6086
6416
|
if (toolsBlockStartedAt !== null) {
|
|
@@ -6268,6 +6598,7 @@ var init_app = __esm({
|
|
|
6268
6598
|
init_resilient_ws();
|
|
6269
6599
|
init_config();
|
|
6270
6600
|
init_daemon_bootstrap();
|
|
6601
|
+
init_session();
|
|
6271
6602
|
init_paths();
|
|
6272
6603
|
init_history();
|
|
6273
6604
|
init_discovery();
|
|
@@ -6293,6 +6624,11 @@ var init_tui = __esm({
|
|
|
6293
6624
|
}
|
|
6294
6625
|
});
|
|
6295
6626
|
|
|
6627
|
+
// src/cli.ts
|
|
6628
|
+
import { readFileSync } from "fs";
|
|
6629
|
+
import { fileURLToPath } from "url";
|
|
6630
|
+
import { dirname as dirname3, resolve as resolve3 } from "path";
|
|
6631
|
+
|
|
6296
6632
|
// src/cli/parse-args.ts
|
|
6297
6633
|
function parseArgs(argv) {
|
|
6298
6634
|
const positional = [];
|
|
@@ -6362,8 +6698,7 @@ async function runInit(flags) {
|
|
|
6362
6698
|
existing = void 0;
|
|
6363
6699
|
}
|
|
6364
6700
|
if (!existing) {
|
|
6365
|
-
const config =
|
|
6366
|
-
await writeConfig(config);
|
|
6701
|
+
const config = await writeMinimalInitConfig();
|
|
6367
6702
|
process.stdout.write(
|
|
6368
6703
|
`Initialized ${paths.config()}
|
|
6369
6704
|
Auth token: ${config.daemon.authToken}
|
|
@@ -6372,11 +6707,14 @@ Auth token: ${config.daemon.authToken}
|
|
|
6372
6707
|
return;
|
|
6373
6708
|
}
|
|
6374
6709
|
if (flagBool(flags, "rotate-token")) {
|
|
6375
|
-
|
|
6376
|
-
await
|
|
6710
|
+
const newToken = generateAuthToken();
|
|
6711
|
+
await updateConfigField((raw) => {
|
|
6712
|
+
const daemon = raw.daemon ??= {};
|
|
6713
|
+
daemon.authToken = newToken;
|
|
6714
|
+
});
|
|
6377
6715
|
process.stdout.write(
|
|
6378
6716
|
`Rotated token in ${paths.config()}
|
|
6379
|
-
New token: ${
|
|
6717
|
+
New token: ${newToken}
|
|
6380
6718
|
`
|
|
6381
6719
|
);
|
|
6382
6720
|
return;
|
|
@@ -6394,7 +6732,7 @@ import { setTimeout as sleep2 } from "timers/promises";
|
|
|
6394
6732
|
|
|
6395
6733
|
// src/daemon/server.ts
|
|
6396
6734
|
init_config();
|
|
6397
|
-
import * as
|
|
6735
|
+
import * as fs9 from "fs";
|
|
6398
6736
|
import * as fsp2 from "fs/promises";
|
|
6399
6737
|
import Fastify from "fastify";
|
|
6400
6738
|
import websocketPlugin from "@fastify/websocket";
|
|
@@ -6567,7 +6905,8 @@ function planSpawn(agent, extraArgs = []) {
|
|
|
6567
6905
|
}
|
|
6568
6906
|
|
|
6569
6907
|
// src/core/session-manager.ts
|
|
6570
|
-
import * as
|
|
6908
|
+
import * as fs7 from "fs/promises";
|
|
6909
|
+
import { customAlphabet as customAlphabet3 } from "nanoid";
|
|
6571
6910
|
|
|
6572
6911
|
// src/core/agent-instance.ts
|
|
6573
6912
|
import { spawn } from "child_process";
|
|
@@ -6626,13 +6965,13 @@ function ndjsonStreamFromStdio(stdout, stdin) {
|
|
|
6626
6965
|
throw new Error("stream is closed");
|
|
6627
6966
|
}
|
|
6628
6967
|
const line = JSON.stringify(message) + "\n";
|
|
6629
|
-
await new Promise((
|
|
6968
|
+
await new Promise((resolve4, reject) => {
|
|
6630
6969
|
stdin.write(line, (err) => {
|
|
6631
6970
|
if (err) {
|
|
6632
6971
|
reject(err);
|
|
6633
6972
|
return;
|
|
6634
6973
|
}
|
|
6635
|
-
|
|
6974
|
+
resolve4();
|
|
6636
6975
|
});
|
|
6637
6976
|
});
|
|
6638
6977
|
},
|
|
@@ -6713,7 +7052,14 @@ init_session();
|
|
|
6713
7052
|
init_paths();
|
|
6714
7053
|
import * as fs4 from "fs/promises";
|
|
6715
7054
|
import * as path2 from "path";
|
|
7055
|
+
import { customAlphabet as customAlphabet2 } from "nanoid";
|
|
6716
7056
|
import { z as z4 } from "zod";
|
|
7057
|
+
var HYDRA_ID_ALPHABET2 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
7058
|
+
var generateRawId = customAlphabet2(HYDRA_ID_ALPHABET2, 16);
|
|
7059
|
+
var HYDRA_LINEAGE_PREFIX = "hydra_lineage_";
|
|
7060
|
+
function generateLineageId() {
|
|
7061
|
+
return `${HYDRA_LINEAGE_PREFIX}${generateRawId()}`;
|
|
7062
|
+
}
|
|
6717
7063
|
var PersistedAgentCommand = z4.object({
|
|
6718
7064
|
name: z4.string(),
|
|
6719
7065
|
description: z4.string().optional()
|
|
@@ -6721,7 +7067,20 @@ var PersistedAgentCommand = z4.object({
|
|
|
6721
7067
|
var SessionRecord = z4.object({
|
|
6722
7068
|
version: z4.literal(1),
|
|
6723
7069
|
sessionId: z4.string(),
|
|
7070
|
+
// Optional for back-compat with records written before this field
|
|
7071
|
+
// existed; mergeForPersistence generates one on next write so any
|
|
7072
|
+
// touched session converges to having a lineageId. A record that
|
|
7073
|
+
// never gets written again (truly cold and untouched) just won't
|
|
7074
|
+
// participate in lineage-based dedup, which is correct — it was
|
|
7075
|
+
// never exported, so no incoming bundle can claim its lineage.
|
|
7076
|
+
lineageId: z4.string().optional(),
|
|
6724
7077
|
upstreamSessionId: z4.string(),
|
|
7078
|
+
// When non-empty, marks a session that was created by import and is
|
|
7079
|
+
// waiting for its first attach to bootstrap a fresh upstream agent
|
|
7080
|
+
// and replay the imported history as a takeover transcript. The
|
|
7081
|
+
// origin's local id at export time, kept for debuggability and as a
|
|
7082
|
+
// breadcrumb in `sessions list` (informational, not used for routing).
|
|
7083
|
+
importedFromSessionId: z4.string().optional(),
|
|
6725
7084
|
agentId: z4.string(),
|
|
6726
7085
|
cwd: z4.string(),
|
|
6727
7086
|
title: z4.string().optional(),
|
|
@@ -6794,6 +7153,25 @@ var SessionStore = class {
|
|
|
6794
7153
|
}
|
|
6795
7154
|
}
|
|
6796
7155
|
}
|
|
7156
|
+
// Find a persisted session by lineageId. Used by SessionManager.import
|
|
7157
|
+
// to detect bundles that have already been imported (lineageId match)
|
|
7158
|
+
// so we can either error out or, with replace:true, overwrite.
|
|
7159
|
+
// Returns undefined if no record has that lineageId. Records that
|
|
7160
|
+
// pre-date the lineageId field simply don't match — which is
|
|
7161
|
+
// correct: they were never exported, so no incoming bundle can
|
|
7162
|
+
// legitimately claim their lineage.
|
|
7163
|
+
async findByLineageId(lineageId) {
|
|
7164
|
+
if (lineageId.length === 0) {
|
|
7165
|
+
return void 0;
|
|
7166
|
+
}
|
|
7167
|
+
const all = await this.list().catch(() => []);
|
|
7168
|
+
for (const record of all) {
|
|
7169
|
+
if (record.lineageId === lineageId) {
|
|
7170
|
+
return record;
|
|
7171
|
+
}
|
|
7172
|
+
}
|
|
7173
|
+
return void 0;
|
|
7174
|
+
}
|
|
6797
7175
|
async list() {
|
|
6798
7176
|
let entries;
|
|
6799
7177
|
try {
|
|
@@ -6819,7 +7197,9 @@ function recordFromMemorySession(args) {
|
|
|
6819
7197
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6820
7198
|
return {
|
|
6821
7199
|
sessionId: args.sessionId,
|
|
7200
|
+
lineageId: args.lineageId,
|
|
6822
7201
|
upstreamSessionId: args.upstreamSessionId,
|
|
7202
|
+
importedFromSessionId: args.importedFromSessionId,
|
|
6823
7203
|
agentId: args.agentId,
|
|
6824
7204
|
cwd: args.cwd,
|
|
6825
7205
|
title: args.title,
|
|
@@ -6868,6 +7248,36 @@ var HistoryStore = class {
|
|
|
6868
7248
|
});
|
|
6869
7249
|
});
|
|
6870
7250
|
}
|
|
7251
|
+
// Trim the on-disk history file to the most recent maxEntries lines.
|
|
7252
|
+
// Runs through the same per-session write queue as append/rewrite so
|
|
7253
|
+
// it's safe to invoke alongside ongoing writes; a no-op if the file is
|
|
7254
|
+
// already at or below the cap.
|
|
7255
|
+
async compact(sessionId, maxEntries) {
|
|
7256
|
+
if (!SESSION_ID_PATTERN2.test(sessionId)) {
|
|
7257
|
+
return;
|
|
7258
|
+
}
|
|
7259
|
+
return this.enqueue(sessionId, async () => {
|
|
7260
|
+
let raw;
|
|
7261
|
+
try {
|
|
7262
|
+
raw = await fs5.readFile(paths.historyFile(sessionId), "utf8");
|
|
7263
|
+
} catch (err) {
|
|
7264
|
+
const e = err;
|
|
7265
|
+
if (e.code === "ENOENT") {
|
|
7266
|
+
return;
|
|
7267
|
+
}
|
|
7268
|
+
throw err;
|
|
7269
|
+
}
|
|
7270
|
+
const lines = raw.split("\n").filter((l) => l.length > 0);
|
|
7271
|
+
if (lines.length <= maxEntries) {
|
|
7272
|
+
return;
|
|
7273
|
+
}
|
|
7274
|
+
const trimmed = lines.slice(-maxEntries);
|
|
7275
|
+
await fs5.writeFile(paths.historyFile(sessionId), trimmed.join("\n") + "\n", {
|
|
7276
|
+
encoding: "utf8",
|
|
7277
|
+
mode: 384
|
|
7278
|
+
});
|
|
7279
|
+
});
|
|
7280
|
+
}
|
|
6871
7281
|
async load(sessionId) {
|
|
6872
7282
|
if (!SESSION_ID_PATTERN2.test(sessionId)) {
|
|
6873
7283
|
return [];
|
|
@@ -6957,7 +7367,10 @@ var HistoryStore = class {
|
|
|
6957
7367
|
|
|
6958
7368
|
// src/core/session-manager.ts
|
|
6959
7369
|
init_paths();
|
|
7370
|
+
init_history();
|
|
6960
7371
|
init_types();
|
|
7372
|
+
var HYDRA_ID_ALPHABET3 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
7373
|
+
var generateRawSessionId = customAlphabet3(HYDRA_ID_ALPHABET3, 16);
|
|
6961
7374
|
var SessionManager = class {
|
|
6962
7375
|
constructor(registry, spawner, store, options = {}) {
|
|
6963
7376
|
this.registry = registry;
|
|
@@ -7036,6 +7449,9 @@ var SessionManager = class {
|
|
|
7036
7449
|
err.code = JsonRpcErrorCodes.AgentNotInstalled;
|
|
7037
7450
|
throw err;
|
|
7038
7451
|
}
|
|
7452
|
+
if (params.upstreamSessionId === "") {
|
|
7453
|
+
return this.doResurrectFromImport(params);
|
|
7454
|
+
}
|
|
7039
7455
|
const plan = planSpawn(agentDef, params.agentArgs ?? []);
|
|
7040
7456
|
const agent = this.spawner({
|
|
7041
7457
|
agentId: params.agentId,
|
|
@@ -7072,15 +7488,55 @@ var SessionManager = class {
|
|
|
7072
7488
|
idleTimeoutMs: this.idleTimeoutMs,
|
|
7073
7489
|
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
|
|
7074
7490
|
historyStore: this.histories,
|
|
7075
|
-
seedHistory: params.seedHistory,
|
|
7076
7491
|
currentModel: params.currentModel,
|
|
7077
7492
|
currentMode: params.currentMode,
|
|
7078
7493
|
agentCommands: params.agentCommands,
|
|
7079
|
-
|
|
7494
|
+
// Only gate the first-prompt title heuristic when we actually have
|
|
7495
|
+
// a title to preserve. A title-less session (lost to a write race
|
|
7496
|
+
// or never seeded) should re-derive from the next prompt rather
|
|
7497
|
+
// than stay stuck.
|
|
7498
|
+
firstPromptSeeded: !!params.title,
|
|
7499
|
+
createdAt: params.createdAt ? new Date(params.createdAt).getTime() : void 0
|
|
7080
7500
|
});
|
|
7081
7501
|
await this.attachManagerHooks(session);
|
|
7082
7502
|
return session;
|
|
7083
7503
|
}
|
|
7504
|
+
// First-attach path for a session that was created via import(). The
|
|
7505
|
+
// on-disk meta.json carries upstreamSessionId="" as the import
|
|
7506
|
+
// marker; bootstrap a fresh agent (gets a real upstream id) and kick
|
|
7507
|
+
// off seedFromImport so the agent absorbs the historical transcript.
|
|
7508
|
+
// attachManagerHooks rewrites meta.json with the new upstreamSessionId,
|
|
7509
|
+
// so subsequent resurrects of this session use the normal session/load
|
|
7510
|
+
// path.
|
|
7511
|
+
async doResurrectFromImport(params) {
|
|
7512
|
+
const fresh = await this.bootstrapAgent({
|
|
7513
|
+
agentId: params.agentId,
|
|
7514
|
+
cwd: params.cwd,
|
|
7515
|
+
agentArgs: params.agentArgs,
|
|
7516
|
+
mcpServers: []
|
|
7517
|
+
});
|
|
7518
|
+
const session = new Session({
|
|
7519
|
+
sessionId: params.hydraSessionId,
|
|
7520
|
+
cwd: params.cwd,
|
|
7521
|
+
agentId: params.agentId,
|
|
7522
|
+
agent: fresh.agent,
|
|
7523
|
+
upstreamSessionId: fresh.upstreamSessionId,
|
|
7524
|
+
agentMeta: fresh.agentMeta,
|
|
7525
|
+
title: params.title,
|
|
7526
|
+
agentArgs: params.agentArgs,
|
|
7527
|
+
idleTimeoutMs: this.idleTimeoutMs,
|
|
7528
|
+
spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
|
|
7529
|
+
historyStore: this.histories,
|
|
7530
|
+
currentModel: params.currentModel,
|
|
7531
|
+
currentMode: params.currentMode,
|
|
7532
|
+
agentCommands: params.agentCommands,
|
|
7533
|
+
firstPromptSeeded: !!params.title,
|
|
7534
|
+
createdAt: params.createdAt ? new Date(params.createdAt).getTime() : void 0
|
|
7535
|
+
});
|
|
7536
|
+
await this.attachManagerHooks(session);
|
|
7537
|
+
void session.seedFromImport().catch(() => void 0);
|
|
7538
|
+
return session;
|
|
7539
|
+
}
|
|
7084
7540
|
// Bootstrap a fresh agent process: registry resolve → spawn → initialize
|
|
7085
7541
|
// → session/new. Shared by create() and the /hydra switch path so both
|
|
7086
7542
|
// go through the same env / capabilities / error-handling.
|
|
@@ -7160,28 +7616,20 @@ var SessionManager = class {
|
|
|
7160
7616
|
}).catch(() => void 0);
|
|
7161
7617
|
});
|
|
7162
7618
|
this.sessions.set(session.sessionId, session);
|
|
7163
|
-
await this.
|
|
7164
|
-
|
|
7165
|
-
|
|
7166
|
-
|
|
7167
|
-
|
|
7168
|
-
cwd: session.cwd,
|
|
7169
|
-
title: session.title,
|
|
7170
|
-
agentArgs: session.agentArgs,
|
|
7171
|
-
currentModel: session.currentModel,
|
|
7172
|
-
currentMode: session.currentMode
|
|
7173
|
-
})
|
|
7174
|
-
).catch(() => void 0);
|
|
7619
|
+
await this.enqueueMetaWrite(session.sessionId, async () => {
|
|
7620
|
+
const existing = await this.store.read(session.sessionId);
|
|
7621
|
+
const merged = mergeForPersistence(session, existing);
|
|
7622
|
+
await this.store.write(merged);
|
|
7623
|
+
}).catch(() => void 0);
|
|
7175
7624
|
}
|
|
7176
7625
|
// Resolve a session's recorded history without forcing a resurrect.
|
|
7177
|
-
//
|
|
7178
|
-
//
|
|
7179
|
-
//
|
|
7180
|
-
//
|
|
7626
|
+
// Always loads from disk — that's the source of truth whether the
|
|
7627
|
+
// session is hot or cold. Returns undefined if the session id is
|
|
7628
|
+
// unknown to both the live map and disk store, so the caller can
|
|
7629
|
+
// distinguish "no history yet" (empty array) from "404".
|
|
7181
7630
|
async getHistory(sessionId) {
|
|
7182
|
-
|
|
7183
|
-
|
|
7184
|
-
return live.getHistorySnapshot();
|
|
7631
|
+
if (this.sessions.has(sessionId)) {
|
|
7632
|
+
return this.histories.load(sessionId).catch(() => []);
|
|
7185
7633
|
}
|
|
7186
7634
|
const record = await this.store.read(sessionId);
|
|
7187
7635
|
if (!record) {
|
|
@@ -7194,20 +7642,41 @@ var SessionManager = class {
|
|
|
7194
7642
|
if (!record) {
|
|
7195
7643
|
return void 0;
|
|
7196
7644
|
}
|
|
7197
|
-
|
|
7645
|
+
let title = record.title;
|
|
7646
|
+
if (!title) {
|
|
7647
|
+
title = await this.deriveTitleFromHistory(sessionId);
|
|
7648
|
+
}
|
|
7198
7649
|
return {
|
|
7199
7650
|
hydraSessionId: record.sessionId,
|
|
7200
7651
|
upstreamSessionId: record.upstreamSessionId,
|
|
7201
7652
|
agentId: record.agentId,
|
|
7202
7653
|
cwd: record.cwd,
|
|
7203
|
-
title
|
|
7654
|
+
title,
|
|
7204
7655
|
agentArgs: record.agentArgs,
|
|
7205
|
-
seedHistory: seedHistory.length > 0 ? seedHistory : void 0,
|
|
7206
7656
|
currentModel: record.currentModel,
|
|
7207
7657
|
currentMode: record.currentMode,
|
|
7208
|
-
agentCommands: record.agentCommands
|
|
7658
|
+
agentCommands: record.agentCommands,
|
|
7659
|
+
createdAt: record.createdAt
|
|
7209
7660
|
};
|
|
7210
7661
|
}
|
|
7662
|
+
// Best-effort: peek at the persisted history's first prompt and use
|
|
7663
|
+
// its first line (capped to 200 chars) as a session title. Returns
|
|
7664
|
+
// undefined if no usable prompt is found or any I/O fails.
|
|
7665
|
+
async deriveTitleFromHistory(sessionId) {
|
|
7666
|
+
const history = await this.histories.load(sessionId).catch(() => []);
|
|
7667
|
+
for (const entry of history) {
|
|
7668
|
+
const params = entry.params;
|
|
7669
|
+
if (params?.update?.sessionUpdate !== "prompt_received") {
|
|
7670
|
+
continue;
|
|
7671
|
+
}
|
|
7672
|
+
const text = extractPromptText(params.update.prompt);
|
|
7673
|
+
const line = firstLine(text, 200);
|
|
7674
|
+
if (line) {
|
|
7675
|
+
return line;
|
|
7676
|
+
}
|
|
7677
|
+
}
|
|
7678
|
+
return void 0;
|
|
7679
|
+
}
|
|
7211
7680
|
get(sessionId) {
|
|
7212
7681
|
return this.sessions.get(sessionId);
|
|
7213
7682
|
}
|
|
@@ -7282,6 +7751,111 @@ var SessionManager = class {
|
|
|
7282
7751
|
entries.sort((a, b) => a.updatedAt < b.updatedAt ? 1 : -1);
|
|
7283
7752
|
return entries;
|
|
7284
7753
|
}
|
|
7754
|
+
// Build an export bundle for a session, reading meta + history from
|
|
7755
|
+
// disk. Backfills lineageId if the on-disk record pre-dates that
|
|
7756
|
+
// field. Returns undefined if the session doesn't exist. Callers
|
|
7757
|
+
// populate the bundle's exportedFrom metadata themselves.
|
|
7758
|
+
async exportBundle(sessionId) {
|
|
7759
|
+
const record = await this.store.read(sessionId);
|
|
7760
|
+
if (!record) {
|
|
7761
|
+
return void 0;
|
|
7762
|
+
}
|
|
7763
|
+
let withLineage;
|
|
7764
|
+
if (record.lineageId) {
|
|
7765
|
+
withLineage = record;
|
|
7766
|
+
} else {
|
|
7767
|
+
const lineageId = generateLineageId();
|
|
7768
|
+
const backfilled = { ...record, lineageId };
|
|
7769
|
+
await this.enqueueMetaWrite(sessionId, async () => {
|
|
7770
|
+
const latest = await this.store.read(sessionId);
|
|
7771
|
+
if (!latest) {
|
|
7772
|
+
return;
|
|
7773
|
+
}
|
|
7774
|
+
if (latest.lineageId) {
|
|
7775
|
+
return;
|
|
7776
|
+
}
|
|
7777
|
+
await this.store.write({ ...latest, lineageId });
|
|
7778
|
+
}).catch(() => void 0);
|
|
7779
|
+
withLineage = backfilled;
|
|
7780
|
+
}
|
|
7781
|
+
const history = await this.histories.load(sessionId).catch(() => []);
|
|
7782
|
+
const promptHistory = await loadPromptHistorySafely(sessionId);
|
|
7783
|
+
return { record: withLineage, history, promptHistory };
|
|
7784
|
+
}
|
|
7785
|
+
// Create a local session from an imported bundle. Without `replace`,
|
|
7786
|
+
// a bundle with a lineageId we already have on disk throws
|
|
7787
|
+
// BundleAlreadyImported citing the existing local id. With
|
|
7788
|
+
// `replace: true`, the existing record is overwritten in-place (its
|
|
7789
|
+
// local sessionId is preserved so bookmarks/Slack thread links still
|
|
7790
|
+
// resolve), and any live in-memory session is closed so the next
|
|
7791
|
+
// attach triggers the import-reseed path.
|
|
7792
|
+
async importBundle(bundle, opts = {}) {
|
|
7793
|
+
const existing = await this.store.findByLineageId(bundle.session.lineageId);
|
|
7794
|
+
if (existing) {
|
|
7795
|
+
if (!opts.replace) {
|
|
7796
|
+
const err = new Error(
|
|
7797
|
+
`bundle already imported as ${existing.sessionId}`
|
|
7798
|
+
);
|
|
7799
|
+
err.code = JsonRpcErrorCodes.BundleAlreadyImported;
|
|
7800
|
+
err.existingSessionId = existing.sessionId;
|
|
7801
|
+
throw err;
|
|
7802
|
+
}
|
|
7803
|
+
const live = this.sessions.get(existing.sessionId);
|
|
7804
|
+
if (live) {
|
|
7805
|
+
await live.close({ deleteRecord: false }).catch(() => void 0);
|
|
7806
|
+
}
|
|
7807
|
+
await this.writeImportedRecord({
|
|
7808
|
+
sessionId: existing.sessionId,
|
|
7809
|
+
bundle,
|
|
7810
|
+
preservedCreatedAt: existing.createdAt
|
|
7811
|
+
});
|
|
7812
|
+
return {
|
|
7813
|
+
sessionId: existing.sessionId,
|
|
7814
|
+
importedFromSessionId: bundle.session.sessionId,
|
|
7815
|
+
replaced: true
|
|
7816
|
+
};
|
|
7817
|
+
}
|
|
7818
|
+
const newId = `${HYDRA_SESSION_PREFIX}${generateRawSessionId()}`;
|
|
7819
|
+
await this.writeImportedRecord({ sessionId: newId, bundle });
|
|
7820
|
+
return {
|
|
7821
|
+
sessionId: newId,
|
|
7822
|
+
importedFromSessionId: bundle.session.sessionId,
|
|
7823
|
+
replaced: false
|
|
7824
|
+
};
|
|
7825
|
+
}
|
|
7826
|
+
// Write the imported bundle's history.jsonl, prompt-history (if
|
|
7827
|
+
// present), and meta.json. upstreamSessionId is left empty as the
|
|
7828
|
+
// marker that the first attach should bootstrap a fresh agent and
|
|
7829
|
+
// run seedFromImport rather than calling session/load.
|
|
7830
|
+
async writeImportedRecord(args) {
|
|
7831
|
+
await this.histories.rewrite(
|
|
7832
|
+
args.sessionId,
|
|
7833
|
+
args.bundle.history
|
|
7834
|
+
);
|
|
7835
|
+
if (args.bundle.promptHistory && args.bundle.promptHistory.length > 0) {
|
|
7836
|
+
await saveHistory(
|
|
7837
|
+
paths.tuiHistoryFile(args.sessionId),
|
|
7838
|
+
args.bundle.promptHistory
|
|
7839
|
+
).catch(() => void 0);
|
|
7840
|
+
}
|
|
7841
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
7842
|
+
await this.enqueueMetaWrite(args.sessionId, async () => {
|
|
7843
|
+
await this.store.write({
|
|
7844
|
+
sessionId: args.sessionId,
|
|
7845
|
+
lineageId: args.bundle.session.lineageId,
|
|
7846
|
+
upstreamSessionId: "",
|
|
7847
|
+
importedFromSessionId: args.bundle.session.sessionId,
|
|
7848
|
+
agentId: args.bundle.session.agentId,
|
|
7849
|
+
cwd: args.bundle.session.cwd,
|
|
7850
|
+
title: args.bundle.session.title,
|
|
7851
|
+
currentModel: args.bundle.session.currentModel,
|
|
7852
|
+
currentMode: args.bundle.session.currentMode,
|
|
7853
|
+
agentCommands: args.bundle.session.agentCommands,
|
|
7854
|
+
createdAt: args.preservedCreatedAt ?? now,
|
|
7855
|
+
updatedAt: now
|
|
7856
|
+
});
|
|
7857
|
+
});
|
|
7858
|
+
}
|
|
7285
7859
|
async deleteRecord(sessionId) {
|
|
7286
7860
|
const record = await this.store.read(sessionId);
|
|
7287
7861
|
if (!record) {
|
|
@@ -7367,10 +7941,64 @@ var SessionManager = class {
|
|
|
7367
7941
|
await Promise.allSettled(sessions.map((s) => s.close()));
|
|
7368
7942
|
this.sessions.clear();
|
|
7369
7943
|
}
|
|
7944
|
+
// Wait for every pending meta.json write to settle. Daemon shutdown
|
|
7945
|
+
// hooks call this so a SIGTERM doesn't kill the process mid-write
|
|
7946
|
+
// and lose a freshly-set title (or model/mode/commands).
|
|
7947
|
+
async flushMetaWrites() {
|
|
7948
|
+
const pending = [...this.metaWriteQueues.values()];
|
|
7949
|
+
if (pending.length === 0) {
|
|
7950
|
+
return;
|
|
7951
|
+
}
|
|
7952
|
+
await Promise.allSettled(pending);
|
|
7953
|
+
}
|
|
7370
7954
|
};
|
|
7955
|
+
function mergeForPersistence(session, existing) {
|
|
7956
|
+
const persistedCommands = session.mergedAvailableCommands().length > 0 ? session.agentOnlyAdvertisedCommands().map((c) => {
|
|
7957
|
+
if (c.description !== void 0) {
|
|
7958
|
+
return { name: c.name, description: c.description };
|
|
7959
|
+
}
|
|
7960
|
+
return { name: c.name };
|
|
7961
|
+
}) : void 0;
|
|
7962
|
+
const agentCommands = persistedCommands ?? existing?.agentCommands;
|
|
7963
|
+
return recordFromMemorySession({
|
|
7964
|
+
sessionId: session.sessionId,
|
|
7965
|
+
lineageId: existing?.lineageId ?? generateLineageId(),
|
|
7966
|
+
upstreamSessionId: session.upstreamSessionId,
|
|
7967
|
+
importedFromSessionId: existing?.importedFromSessionId,
|
|
7968
|
+
agentId: session.agentId,
|
|
7969
|
+
cwd: session.cwd,
|
|
7970
|
+
title: session.title,
|
|
7971
|
+
agentArgs: session.agentArgs,
|
|
7972
|
+
currentModel: session.currentModel ?? existing?.currentModel,
|
|
7973
|
+
currentMode: session.currentMode ?? existing?.currentMode,
|
|
7974
|
+
agentCommands,
|
|
7975
|
+
createdAt: existing?.createdAt ?? new Date(session.createdAt).toISOString()
|
|
7976
|
+
});
|
|
7977
|
+
}
|
|
7978
|
+
async function loadPromptHistorySafely(sessionId) {
|
|
7979
|
+
try {
|
|
7980
|
+
const raw = await fs7.readFile(paths.tuiHistoryFile(sessionId), "utf8");
|
|
7981
|
+
const out = [];
|
|
7982
|
+
for (const line of raw.split("\n")) {
|
|
7983
|
+
if (line.length === 0) {
|
|
7984
|
+
continue;
|
|
7985
|
+
}
|
|
7986
|
+
try {
|
|
7987
|
+
const decoded = JSON.parse(line);
|
|
7988
|
+
if (typeof decoded === "string") {
|
|
7989
|
+
out.push(decoded);
|
|
7990
|
+
}
|
|
7991
|
+
} catch {
|
|
7992
|
+
}
|
|
7993
|
+
}
|
|
7994
|
+
return out;
|
|
7995
|
+
} catch {
|
|
7996
|
+
return [];
|
|
7997
|
+
}
|
|
7998
|
+
}
|
|
7371
7999
|
async function historyMtimeIso(sessionId) {
|
|
7372
8000
|
try {
|
|
7373
|
-
const st = await
|
|
8001
|
+
const st = await fs7.stat(paths.historyFile(sessionId));
|
|
7374
8002
|
return new Date(st.mtimeMs).toISOString();
|
|
7375
8003
|
} catch {
|
|
7376
8004
|
return void 0;
|
|
@@ -7380,9 +8008,9 @@ async function historyMtimeIso(sessionId) {
|
|
|
7380
8008
|
// src/core/extensions.ts
|
|
7381
8009
|
init_paths();
|
|
7382
8010
|
import { spawn as spawn2 } from "child_process";
|
|
7383
|
-
import * as
|
|
8011
|
+
import * as fs8 from "fs";
|
|
7384
8012
|
import * as fsp from "fs/promises";
|
|
7385
|
-
import * as
|
|
8013
|
+
import * as path4 from "path";
|
|
7386
8014
|
var RESTART_BASE_MS = 1e3;
|
|
7387
8015
|
var RESTART_CAP_MS = 6e4;
|
|
7388
8016
|
var STOP_GRACE_MS = 3e3;
|
|
@@ -7429,9 +8057,9 @@ var ExtensionManager = class {
|
|
|
7429
8057
|
} catch {
|
|
7430
8058
|
}
|
|
7431
8059
|
tasks.push(
|
|
7432
|
-
new Promise((
|
|
8060
|
+
new Promise((resolve4) => {
|
|
7433
8061
|
if (child.exitCode !== null || child.signalCode !== null) {
|
|
7434
|
-
|
|
8062
|
+
resolve4();
|
|
7435
8063
|
return;
|
|
7436
8064
|
}
|
|
7437
8065
|
const timer = setTimeout(() => {
|
|
@@ -7439,11 +8067,11 @@ var ExtensionManager = class {
|
|
|
7439
8067
|
child.kill("SIGKILL");
|
|
7440
8068
|
} catch {
|
|
7441
8069
|
}
|
|
7442
|
-
|
|
8070
|
+
resolve4();
|
|
7443
8071
|
}, STOP_GRACE_MS);
|
|
7444
8072
|
child.on("exit", () => {
|
|
7445
8073
|
clearTimeout(timer);
|
|
7446
|
-
|
|
8074
|
+
resolve4();
|
|
7447
8075
|
});
|
|
7448
8076
|
})
|
|
7449
8077
|
);
|
|
@@ -7551,8 +8179,8 @@ var ExtensionManager = class {
|
|
|
7551
8179
|
if (child.exitCode !== null || child.signalCode !== null) {
|
|
7552
8180
|
return;
|
|
7553
8181
|
}
|
|
7554
|
-
const exited = new Promise((
|
|
7555
|
-
entry.exitWaiters.push(
|
|
8182
|
+
const exited = new Promise((resolve4) => {
|
|
8183
|
+
entry.exitWaiters.push(resolve4);
|
|
7556
8184
|
});
|
|
7557
8185
|
try {
|
|
7558
8186
|
child.kill("SIGTERM");
|
|
@@ -7624,7 +8252,7 @@ var ExtensionManager = class {
|
|
|
7624
8252
|
if (!entry.endsWith(".pid")) {
|
|
7625
8253
|
continue;
|
|
7626
8254
|
}
|
|
7627
|
-
const pidPath =
|
|
8255
|
+
const pidPath = path4.join(paths.extensionsDir(), entry);
|
|
7628
8256
|
let pid;
|
|
7629
8257
|
try {
|
|
7630
8258
|
const raw = await fsp.readFile(pidPath, "utf8");
|
|
@@ -7663,7 +8291,7 @@ var ExtensionManager = class {
|
|
|
7663
8291
|
}
|
|
7664
8292
|
const ext = entry.config;
|
|
7665
8293
|
const command = ext.command.length > 0 ? ext.command : [ext.name];
|
|
7666
|
-
const logStream =
|
|
8294
|
+
const logStream = fs8.createWriteStream(paths.extensionLogFile(ext.name), {
|
|
7667
8295
|
flags: "a"
|
|
7668
8296
|
});
|
|
7669
8297
|
logStream.write(
|
|
@@ -7713,7 +8341,7 @@ var ExtensionManager = class {
|
|
|
7713
8341
|
}
|
|
7714
8342
|
if (typeof child.pid === "number") {
|
|
7715
8343
|
try {
|
|
7716
|
-
|
|
8344
|
+
fs8.writeFileSync(paths.extensionPidFile(ext.name), `${child.pid}
|
|
7717
8345
|
`, {
|
|
7718
8346
|
encoding: "utf8",
|
|
7719
8347
|
mode: 384
|
|
@@ -7738,7 +8366,7 @@ var ExtensionManager = class {
|
|
|
7738
8366
|
});
|
|
7739
8367
|
child.on("exit", (code, signal) => {
|
|
7740
8368
|
try {
|
|
7741
|
-
|
|
8369
|
+
fs8.unlinkSync(paths.extensionPidFile(ext.name));
|
|
7742
8370
|
} catch {
|
|
7743
8371
|
}
|
|
7744
8372
|
logStream.write(
|
|
@@ -7749,8 +8377,8 @@ var ExtensionManager = class {
|
|
|
7749
8377
|
entry.pid = void 0;
|
|
7750
8378
|
entry.lastExitCode = typeof code === "number" ? code : void 0;
|
|
7751
8379
|
const waiters = entry.exitWaiters.splice(0);
|
|
7752
|
-
for (const
|
|
7753
|
-
|
|
8380
|
+
for (const resolve4 of waiters) {
|
|
8381
|
+
resolve4();
|
|
7754
8382
|
}
|
|
7755
8383
|
if (this.stopping || entry.manuallyStopped) {
|
|
7756
8384
|
try {
|
|
@@ -7851,6 +8479,76 @@ function constantTimeEqual(a, b) {
|
|
|
7851
8479
|
|
|
7852
8480
|
// src/daemon/routes/sessions.ts
|
|
7853
8481
|
init_config();
|
|
8482
|
+
import * as os2 from "os";
|
|
8483
|
+
|
|
8484
|
+
// src/core/bundle.ts
|
|
8485
|
+
import { z as z5 } from "zod";
|
|
8486
|
+
var HistoryEntrySchema = z5.object({
|
|
8487
|
+
method: z5.string(),
|
|
8488
|
+
params: z5.unknown(),
|
|
8489
|
+
recordedAt: z5.number()
|
|
8490
|
+
});
|
|
8491
|
+
var BundleSession = z5.object({
|
|
8492
|
+
// The exporter's local id. Regenerated fresh on import (sessionId is
|
|
8493
|
+
// the local namespace; lineageId is what survives across hops).
|
|
8494
|
+
sessionId: z5.string(),
|
|
8495
|
+
// Required on bundles — the export path backfills if the source
|
|
8496
|
+
// record was written before lineageId existed.
|
|
8497
|
+
lineageId: z5.string(),
|
|
8498
|
+
agentId: z5.string(),
|
|
8499
|
+
cwd: z5.string(),
|
|
8500
|
+
title: z5.string().optional(),
|
|
8501
|
+
currentModel: z5.string().optional(),
|
|
8502
|
+
currentMode: z5.string().optional(),
|
|
8503
|
+
agentCommands: z5.array(PersistedAgentCommand).optional(),
|
|
8504
|
+
createdAt: z5.string(),
|
|
8505
|
+
updatedAt: z5.string()
|
|
8506
|
+
});
|
|
8507
|
+
var Bundle = z5.object({
|
|
8508
|
+
version: z5.literal(1),
|
|
8509
|
+
exportedAt: z5.string(),
|
|
8510
|
+
exportedFrom: z5.object({
|
|
8511
|
+
hydraVersion: z5.string(),
|
|
8512
|
+
machine: z5.string()
|
|
8513
|
+
}),
|
|
8514
|
+
session: BundleSession,
|
|
8515
|
+
history: z5.array(HistoryEntrySchema),
|
|
8516
|
+
promptHistory: z5.array(z5.string()).optional()
|
|
8517
|
+
});
|
|
8518
|
+
function encodeBundle(params) {
|
|
8519
|
+
const bundle = {
|
|
8520
|
+
version: 1,
|
|
8521
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8522
|
+
exportedFrom: {
|
|
8523
|
+
hydraVersion: params.hydraVersion,
|
|
8524
|
+
machine: params.machine
|
|
8525
|
+
},
|
|
8526
|
+
session: {
|
|
8527
|
+
sessionId: params.record.sessionId,
|
|
8528
|
+
lineageId: params.record.lineageId,
|
|
8529
|
+
agentId: params.record.agentId,
|
|
8530
|
+
cwd: params.record.cwd,
|
|
8531
|
+
...params.record.title !== void 0 ? { title: params.record.title } : {},
|
|
8532
|
+
...params.record.currentModel !== void 0 ? { currentModel: params.record.currentModel } : {},
|
|
8533
|
+
...params.record.currentMode !== void 0 ? { currentMode: params.record.currentMode } : {},
|
|
8534
|
+
...params.record.agentCommands !== void 0 ? { agentCommands: params.record.agentCommands } : {},
|
|
8535
|
+
createdAt: params.record.createdAt,
|
|
8536
|
+
updatedAt: params.record.updatedAt
|
|
8537
|
+
},
|
|
8538
|
+
history: params.history
|
|
8539
|
+
};
|
|
8540
|
+
if (params.promptHistory !== void 0) {
|
|
8541
|
+
bundle.promptHistory = params.promptHistory;
|
|
8542
|
+
}
|
|
8543
|
+
return bundle;
|
|
8544
|
+
}
|
|
8545
|
+
function decodeBundle(raw) {
|
|
8546
|
+
return Bundle.parse(raw);
|
|
8547
|
+
}
|
|
8548
|
+
|
|
8549
|
+
// src/daemon/routes/sessions.ts
|
|
8550
|
+
init_types();
|
|
8551
|
+
var HYDRA_VERSION = "0.1.0";
|
|
7854
8552
|
function registerSessionRoutes(app, manager, defaults) {
|
|
7855
8553
|
app.get("/v1/sessions", async (request) => {
|
|
7856
8554
|
const query = request.query;
|
|
@@ -7908,6 +8606,61 @@ function registerSessionRoutes(app, manager, defaults) {
|
|
|
7908
8606
|
}
|
|
7909
8607
|
reply.code(204).send();
|
|
7910
8608
|
});
|
|
8609
|
+
app.get("/v1/sessions/:id/export", async (request, reply) => {
|
|
8610
|
+
const raw = request.params.id;
|
|
8611
|
+
const id = await manager.resolveCanonicalId(raw) ?? raw;
|
|
8612
|
+
const exported = await manager.exportBundle(id);
|
|
8613
|
+
if (!exported) {
|
|
8614
|
+
reply.code(404).send({ error: "session not found" });
|
|
8615
|
+
return;
|
|
8616
|
+
}
|
|
8617
|
+
const bundle = encodeBundle({
|
|
8618
|
+
record: exported.record,
|
|
8619
|
+
history: exported.history,
|
|
8620
|
+
promptHistory: exported.promptHistory.length > 0 ? exported.promptHistory : void 0,
|
|
8621
|
+
hydraVersion: HYDRA_VERSION,
|
|
8622
|
+
machine: os2.hostname()
|
|
8623
|
+
});
|
|
8624
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
8625
|
+
reply.header(
|
|
8626
|
+
"Content-Disposition",
|
|
8627
|
+
`attachment; filename="hydra-${id}-${stamp}.hydra"`
|
|
8628
|
+
);
|
|
8629
|
+
reply.code(200).send(bundle);
|
|
8630
|
+
});
|
|
8631
|
+
app.post("/v1/sessions/import", async (request, reply) => {
|
|
8632
|
+
const body = request.body ?? {};
|
|
8633
|
+
if (body.bundle === void 0) {
|
|
8634
|
+
reply.code(400).send({ error: "missing bundle" });
|
|
8635
|
+
return;
|
|
8636
|
+
}
|
|
8637
|
+
let bundle;
|
|
8638
|
+
try {
|
|
8639
|
+
bundle = decodeBundle(body.bundle);
|
|
8640
|
+
} catch (err) {
|
|
8641
|
+
reply.code(400).send({
|
|
8642
|
+
error: "invalid bundle",
|
|
8643
|
+
details: err.message
|
|
8644
|
+
});
|
|
8645
|
+
return;
|
|
8646
|
+
}
|
|
8647
|
+
try {
|
|
8648
|
+
const result = await manager.importBundle(bundle, {
|
|
8649
|
+
replace: body.replace === true
|
|
8650
|
+
});
|
|
8651
|
+
reply.code(201).send(result);
|
|
8652
|
+
} catch (err) {
|
|
8653
|
+
const e = err;
|
|
8654
|
+
if (e.code === JsonRpcErrorCodes.BundleAlreadyImported) {
|
|
8655
|
+
reply.code(409).send({
|
|
8656
|
+
error: "bundle already imported",
|
|
8657
|
+
existingSessionId: e.existingSessionId
|
|
8658
|
+
});
|
|
8659
|
+
return;
|
|
8660
|
+
}
|
|
8661
|
+
reply.code(500).send({ error: e.message });
|
|
8662
|
+
}
|
|
8663
|
+
});
|
|
7911
8664
|
app.get("/v1/sessions/:id/history", async (request, reply) => {
|
|
7912
8665
|
const raw = request.params.id;
|
|
7913
8666
|
const query = request.query;
|
|
@@ -7916,16 +8669,22 @@ function registerSessionRoutes(app, manager, defaults) {
|
|
|
7916
8669
|
const live = manager.get(id);
|
|
7917
8670
|
let snapshot;
|
|
7918
8671
|
let unsubscribe;
|
|
8672
|
+
let snapshotDone = false;
|
|
8673
|
+
const pending = [];
|
|
7919
8674
|
if (live) {
|
|
7920
|
-
snapshot = live.getHistorySnapshot();
|
|
7921
8675
|
if (follow) {
|
|
7922
8676
|
unsubscribe = live.onBroadcast((entry) => {
|
|
7923
8677
|
if (reply.raw.writableEnded) {
|
|
7924
8678
|
return;
|
|
7925
8679
|
}
|
|
7926
|
-
|
|
8680
|
+
if (snapshotDone) {
|
|
8681
|
+
reply.raw.write(JSON.stringify(entry) + "\n");
|
|
8682
|
+
} else {
|
|
8683
|
+
pending.push(entry);
|
|
8684
|
+
}
|
|
7927
8685
|
});
|
|
7928
8686
|
}
|
|
8687
|
+
snapshot = await live.getHistorySnapshot();
|
|
7929
8688
|
} else {
|
|
7930
8689
|
const cold = await manager.getHistory(id);
|
|
7931
8690
|
if (cold === void 0) {
|
|
@@ -7937,9 +8696,23 @@ function registerSessionRoutes(app, manager, defaults) {
|
|
|
7937
8696
|
reply.raw.setHeader("Content-Type", "application/x-ndjson");
|
|
7938
8697
|
reply.raw.setHeader("Cache-Control", "no-cache");
|
|
7939
8698
|
reply.raw.statusCode = 200;
|
|
8699
|
+
const snapshotKeys = /* @__PURE__ */ new Set();
|
|
7940
8700
|
for (const entry of snapshot ?? []) {
|
|
7941
8701
|
reply.raw.write(JSON.stringify(entry) + "\n");
|
|
8702
|
+
const e = entry;
|
|
8703
|
+
if (typeof e.recordedAt === "number") {
|
|
8704
|
+
snapshotKeys.add(String(e.recordedAt));
|
|
8705
|
+
}
|
|
8706
|
+
}
|
|
8707
|
+
for (const entry of pending) {
|
|
8708
|
+
const e = entry;
|
|
8709
|
+
const key = typeof e.recordedAt === "number" ? String(e.recordedAt) : "";
|
|
8710
|
+
if (key && snapshotKeys.has(key)) {
|
|
8711
|
+
continue;
|
|
8712
|
+
}
|
|
8713
|
+
reply.raw.write(JSON.stringify(entry) + "\n");
|
|
7942
8714
|
}
|
|
8715
|
+
snapshotDone = true;
|
|
7943
8716
|
if (!unsubscribe) {
|
|
7944
8717
|
reply.raw.end();
|
|
7945
8718
|
return reply;
|
|
@@ -8114,7 +8887,7 @@ init_connection();
|
|
|
8114
8887
|
init_ws_stream();
|
|
8115
8888
|
init_types();
|
|
8116
8889
|
import { nanoid as nanoid2 } from "nanoid";
|
|
8117
|
-
var
|
|
8890
|
+
var HYDRA_VERSION2 = "0.1.0";
|
|
8118
8891
|
var HYDRA_PROTOCOL_VERSION = 1;
|
|
8119
8892
|
function registerAcpWsEndpoint(app, deps) {
|
|
8120
8893
|
app.get("/acp", { websocket: true }, (socket, request) => {
|
|
@@ -8160,7 +8933,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
8160
8933
|
agentArgs: hydraMeta.agentArgs
|
|
8161
8934
|
});
|
|
8162
8935
|
const client = bindClientToSession(connection, session, state);
|
|
8163
|
-
session.attach(client, "full");
|
|
8936
|
+
await session.attach(client, "full");
|
|
8164
8937
|
state.attached.set(session.sessionId, {
|
|
8165
8938
|
sessionId: session.sessionId,
|
|
8166
8939
|
clientId: client.clientId
|
|
@@ -8179,14 +8952,22 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
8179
8952
|
const lookupId = hydraHints ? params.sessionId : await deps.manager.resolveCanonicalId(params.sessionId) ?? params.sessionId;
|
|
8180
8953
|
let session = deps.manager.get(lookupId);
|
|
8181
8954
|
if (!session) {
|
|
8182
|
-
|
|
8183
|
-
|
|
8184
|
-
|
|
8185
|
-
|
|
8186
|
-
|
|
8187
|
-
|
|
8188
|
-
|
|
8189
|
-
|
|
8955
|
+
const fromDisk = await deps.manager.loadFromDisk(lookupId);
|
|
8956
|
+
let resurrectParams = fromDisk;
|
|
8957
|
+
if (hydraHints) {
|
|
8958
|
+
resurrectParams = {
|
|
8959
|
+
hydraSessionId: params.sessionId,
|
|
8960
|
+
upstreamSessionId: hydraHints.upstreamSessionId,
|
|
8961
|
+
agentId: hydraHints.agentId,
|
|
8962
|
+
cwd: hydraHints.cwd,
|
|
8963
|
+
title: hydraHints.title ?? fromDisk?.title,
|
|
8964
|
+
agentArgs: hydraHints.agentArgs ?? fromDisk?.agentArgs,
|
|
8965
|
+
currentModel: fromDisk?.currentModel,
|
|
8966
|
+
currentMode: fromDisk?.currentMode,
|
|
8967
|
+
agentCommands: fromDisk?.agentCommands,
|
|
8968
|
+
createdAt: fromDisk?.createdAt
|
|
8969
|
+
};
|
|
8970
|
+
}
|
|
8190
8971
|
if (!resurrectParams) {
|
|
8191
8972
|
const err = new Error(
|
|
8192
8973
|
`session ${params.sessionId} not found and no resume hints provided`
|
|
@@ -8202,13 +8983,13 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
8202
8983
|
state,
|
|
8203
8984
|
params.clientInfo
|
|
8204
8985
|
);
|
|
8205
|
-
const replay = session.attach(client, params.historyPolicy);
|
|
8986
|
+
const replay = await session.attach(client, params.historyPolicy);
|
|
8206
8987
|
state.attached.set(session.sessionId, {
|
|
8207
8988
|
sessionId: session.sessionId,
|
|
8208
8989
|
clientId: client.clientId
|
|
8209
8990
|
});
|
|
8210
8991
|
app.log.info(
|
|
8211
|
-
`session/attach OK sessionId=${session.sessionId} clientId=${client.clientId} attachedCount=${state.attached.size}`
|
|
8992
|
+
`session/attach OK sessionId=${session.sessionId} clientId=${client.clientId} attachedCount=${state.attached.size} replayed=${replay.length}`
|
|
8212
8993
|
);
|
|
8213
8994
|
for (const note of replay) {
|
|
8214
8995
|
await connection.notify(note.method, note.params);
|
|
@@ -8304,7 +9085,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
8304
9085
|
session = await deps.manager.resurrect(fromDisk);
|
|
8305
9086
|
}
|
|
8306
9087
|
const client = bindClientToSession(connection, session, state);
|
|
8307
|
-
const replay = session.attach(client, "pending_only");
|
|
9088
|
+
const replay = await session.attach(client, "pending_only");
|
|
8308
9089
|
state.attached.set(session.sessionId, {
|
|
8309
9090
|
sessionId: session.sessionId,
|
|
8310
9091
|
clientId: client.clientId
|
|
@@ -8370,7 +9151,7 @@ function buildResponseMeta(session) {
|
|
|
8370
9151
|
function buildInitializeResult() {
|
|
8371
9152
|
return {
|
|
8372
9153
|
protocolVersion: HYDRA_PROTOCOL_VERSION,
|
|
8373
|
-
agentInfo: { name: "hydra", version:
|
|
9154
|
+
agentInfo: { name: "hydra", version: HYDRA_VERSION2 },
|
|
8374
9155
|
agentCapabilities: {
|
|
8375
9156
|
// hydra is a transparent proxy: prompt blocks and MCP server configs are
|
|
8376
9157
|
// forwarded to the underlying agent unchanged. We claim the union of
|
|
@@ -8409,7 +9190,7 @@ function bindClientToSession(connection, session, state, clientInfo) {
|
|
|
8409
9190
|
}
|
|
8410
9191
|
|
|
8411
9192
|
// src/daemon/server.ts
|
|
8412
|
-
var
|
|
9193
|
+
var HYDRA_VERSION3 = "0.1.0";
|
|
8413
9194
|
async function startDaemon(config) {
|
|
8414
9195
|
ensureLoopbackOrTls(config);
|
|
8415
9196
|
const httpsOptions = config.daemon.tls ? {
|
|
@@ -8443,7 +9224,7 @@ async function startDaemon(config) {
|
|
|
8443
9224
|
idleTimeoutMs: config.daemon.sessionIdleTimeoutSeconds * 1e3
|
|
8444
9225
|
});
|
|
8445
9226
|
const extensions = new ExtensionManager(extensionList(config));
|
|
8446
|
-
registerHealthRoutes(app,
|
|
9227
|
+
registerHealthRoutes(app, HYDRA_VERSION3);
|
|
8447
9228
|
registerSessionRoutes(app, manager, {
|
|
8448
9229
|
agentId: config.defaultAgent,
|
|
8449
9230
|
cwd: config.defaultCwd
|
|
@@ -8487,9 +9268,10 @@ async function startDaemon(config) {
|
|
|
8487
9268
|
const shutdown = async () => {
|
|
8488
9269
|
await extensions.stop();
|
|
8489
9270
|
await manager.closeAll();
|
|
9271
|
+
await manager.flushMetaWrites();
|
|
8490
9272
|
await app.close();
|
|
8491
9273
|
try {
|
|
8492
|
-
|
|
9274
|
+
fs9.unlinkSync(paths.pidFile());
|
|
8493
9275
|
} catch {
|
|
8494
9276
|
}
|
|
8495
9277
|
try {
|
|
@@ -8528,7 +9310,7 @@ function ensureLoopbackOrTls(config) {
|
|
|
8528
9310
|
init_daemon_bootstrap();
|
|
8529
9311
|
|
|
8530
9312
|
// src/cli/commands/log-tail.ts
|
|
8531
|
-
import * as
|
|
9313
|
+
import * as fs10 from "fs";
|
|
8532
9314
|
import * as fsp3 from "fs/promises";
|
|
8533
9315
|
async function runLogTail(logPath, argv, notFoundMessage) {
|
|
8534
9316
|
const opts = parseLogTailFlags(argv);
|
|
@@ -8552,7 +9334,7 @@ async function runLogTail(logPath, argv, notFoundMessage) {
|
|
|
8552
9334
|
process.stdout.write(`-- following ${logPath} --
|
|
8553
9335
|
`);
|
|
8554
9336
|
let pending = false;
|
|
8555
|
-
const watcher =
|
|
9337
|
+
const watcher = fs10.watch(logPath, () => {
|
|
8556
9338
|
if (pending) {
|
|
8557
9339
|
return;
|
|
8558
9340
|
}
|
|
@@ -8580,10 +9362,10 @@ async function runLogTail(logPath, argv, notFoundMessage) {
|
|
|
8580
9362
|
}
|
|
8581
9363
|
});
|
|
8582
9364
|
});
|
|
8583
|
-
await new Promise((
|
|
9365
|
+
await new Promise((resolve4) => {
|
|
8584
9366
|
const finish = () => {
|
|
8585
9367
|
watcher.close();
|
|
8586
|
-
|
|
9368
|
+
resolve4();
|
|
8587
9369
|
};
|
|
8588
9370
|
process.once("SIGINT", finish);
|
|
8589
9371
|
process.once("SIGTERM", finish);
|
|
@@ -9655,14 +10437,16 @@ async function main() {
|
|
|
9655
10437
|
process.exit(2);
|
|
9656
10438
|
return;
|
|
9657
10439
|
}
|
|
9658
|
-
const
|
|
10440
|
+
const launchResume = flags2.resume;
|
|
10441
|
+
const sessionId2 = typeof launchResume === "string" ? launchResume : resolveOption(flags2, "session-id");
|
|
9659
10442
|
const name2 = resolveOption(flags2, "name");
|
|
9660
10443
|
await runShim({ sessionId: sessionId2, agentId, agentArgs, name: name2 });
|
|
9661
10444
|
return;
|
|
9662
10445
|
}
|
|
9663
10446
|
const { positional, flags } = parseArgs(argv);
|
|
9664
10447
|
if (flags.version === true || positional[0] === "--version") {
|
|
9665
|
-
process.stdout.write(
|
|
10448
|
+
process.stdout.write(`hydra-acp ${readVersion()}
|
|
10449
|
+
`);
|
|
9666
10450
|
return;
|
|
9667
10451
|
}
|
|
9668
10452
|
if (flags.help === true) {
|
|
@@ -9670,7 +10454,8 @@ async function main() {
|
|
|
9670
10454
|
return;
|
|
9671
10455
|
}
|
|
9672
10456
|
const subcommand = positional[0];
|
|
9673
|
-
const
|
|
10457
|
+
const resumeFlag = flags.resume;
|
|
10458
|
+
const sessionId = typeof resumeFlag === "string" ? resumeFlag : resolveOption(flags, "session-id");
|
|
9674
10459
|
const name = resolveOption(flags, "name");
|
|
9675
10460
|
const agentIdFromFlag = resolveOption(flags, "agent-id");
|
|
9676
10461
|
if (!subcommand) {
|
|
@@ -9735,6 +10520,17 @@ async function main() {
|
|
|
9735
10520
|
await runSessionsRm(positional[2]);
|
|
9736
10521
|
return;
|
|
9737
10522
|
}
|
|
10523
|
+
if (sub === "export") {
|
|
10524
|
+
const out = resolveOption(flags, "out");
|
|
10525
|
+
await runSessionsExport(positional[2], out);
|
|
10526
|
+
return;
|
|
10527
|
+
}
|
|
10528
|
+
if (sub === "import") {
|
|
10529
|
+
await runSessionsImport(positional[2], {
|
|
10530
|
+
replace: flags.replace === true
|
|
10531
|
+
});
|
|
10532
|
+
return;
|
|
10533
|
+
}
|
|
9738
10534
|
process.stderr.write(`Unknown sessions subcommand: ${sub}
|
|
9739
10535
|
`);
|
|
9740
10536
|
process.exit(2);
|
|
@@ -9828,6 +10624,17 @@ async function dispatchTui(flags, base) {
|
|
|
9828
10624
|
}
|
|
9829
10625
|
await runTui(tuiOpts);
|
|
9830
10626
|
}
|
|
10627
|
+
function readVersion() {
|
|
10628
|
+
try {
|
|
10629
|
+
const here = dirname3(fileURLToPath(import.meta.url));
|
|
10630
|
+
const pkg = JSON.parse(
|
|
10631
|
+
readFileSync(resolve3(here, "../package.json"), "utf8")
|
|
10632
|
+
);
|
|
10633
|
+
return pkg.version ?? "unknown";
|
|
10634
|
+
} catch {
|
|
10635
|
+
return "unknown";
|
|
10636
|
+
}
|
|
10637
|
+
}
|
|
9831
10638
|
function printHelp() {
|
|
9832
10639
|
process.stdout.write(
|
|
9833
10640
|
[
|
|
@@ -9841,13 +10648,17 @@ function printHelp() {
|
|
|
9841
10648
|
" Shim mode, force daemon to spawn <agent-id>",
|
|
9842
10649
|
" from the registry. Args after <agent-id>",
|
|
9843
10650
|
" are forwarded to the agent's command.",
|
|
9844
|
-
" hydra-acp --
|
|
10651
|
+
" hydra-acp --resume <id> Attach to an existing session (TUI when in a terminal, shim otherwise)",
|
|
9845
10652
|
" hydra-acp init [--rotate-token] Initialize ~/.hydra-acp/config.json",
|
|
9846
10653
|
" hydra-acp daemon start|stop|restart|status",
|
|
9847
10654
|
" hydra-acp daemon logs [-f] [-n N] Tail or follow the daemon log",
|
|
9848
10655
|
" hydra-acp sessions [list] [--all] List sessions (live + 20 most-recent cold; --all for everything)",
|
|
9849
10656
|
" hydra-acp sessions kill <id> Demote a live session to cold (keeps the on-disk record)",
|
|
9850
10657
|
" hydra-acp sessions rm <id> Remove a session entirely (live or cold)",
|
|
10658
|
+
" hydra-acp sessions export <id> [--out <file>|.]",
|
|
10659
|
+
" Write a session bundle to <file>, to a default-named file when --out=., or to stdout",
|
|
10660
|
+
" hydra-acp sessions import <file>|- [--replace]",
|
|
10661
|
+
" Import a bundle from <file> or stdin (-); --replace overwrites a lineage match (kills it if live)",
|
|
9851
10662
|
" hydra-acp extensions list List configured extensions and live state",
|
|
9852
10663
|
" hydra-acp extensions add <name> [opts] Add an extension to config",
|
|
9853
10664
|
" hydra-acp extensions remove <name> Remove an extension from config",
|
|
@@ -9855,15 +10666,16 @@ function printHelp() {
|
|
|
9855
10666
|
" hydra-acp extensions logs <name> [-f] [-n N]Tail or follow an extension's log",
|
|
9856
10667
|
" hydra-acp agents [list] List agents in the cached registry",
|
|
9857
10668
|
" hydra-acp agents refresh Force a registry re-fetch",
|
|
9858
|
-
" hydra-acp tui flags: [--
|
|
9859
|
-
"
|
|
10669
|
+
" hydra-acp tui flags: [--resume [<id>]] [--new] [--agent-id <id>] [--cwd <path>] [--name <label>]",
|
|
10670
|
+
" --resume <id> attaches to a specific session; bare --resume picks the most-recent",
|
|
10671
|
+
" in cwd. Smart default (no flags): picks if any live sessions exist, else new.",
|
|
9860
10672
|
" hydra-acp --version Print version",
|
|
9861
10673
|
" hydra-acp --help Show this help",
|
|
9862
10674
|
"",
|
|
9863
10675
|
"Config knob flags accept env-var equivalents (flag wins):",
|
|
9864
|
-
" --agent-id
|
|
9865
|
-
" --session-id
|
|
9866
|
-
" --name
|
|
10676
|
+
" --agent-id HYDRA_ACP_AGENT_ID",
|
|
10677
|
+
" --resume / --session-id HYDRA_ACP_SESSION_ID",
|
|
10678
|
+
" --name HYDRA_ACP_NAME",
|
|
9867
10679
|
""
|
|
9868
10680
|
].join("\n")
|
|
9869
10681
|
);
|