@hydra-acp/cli 0.1.52 → 0.1.53
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 +4 -2
- package/dist/cli.js +1460 -385
- package/dist/index.d.ts +37 -2
- package/dist/index.js +558 -239
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -18,7 +18,7 @@ import pino from "pino";
|
|
|
18
18
|
import createPinoRoll from "pino-roll";
|
|
19
19
|
|
|
20
20
|
// src/core/config.ts
|
|
21
|
-
import * as
|
|
21
|
+
import * as fs3 from "fs/promises";
|
|
22
22
|
import { homedir as homedir2 } from "os";
|
|
23
23
|
import { z } from "zod";
|
|
24
24
|
|
|
@@ -156,6 +156,70 @@ async function ensureServiceToken() {
|
|
|
156
156
|
return token;
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
// src/core/json-store.ts
|
|
160
|
+
import * as fs2 from "fs/promises";
|
|
161
|
+
import * as fsSync from "fs";
|
|
162
|
+
import { randomBytes } from "crypto";
|
|
163
|
+
async function writeJsonAtomic(filePath, data, opts = {}) {
|
|
164
|
+
const pretty = opts.pretty ?? true;
|
|
165
|
+
const body = (pretty ? JSON.stringify(data, null, 2) : JSON.stringify(data)) + "\n";
|
|
166
|
+
await writeFileAtomic(filePath, body, opts);
|
|
167
|
+
}
|
|
168
|
+
async function writeFileAtomic(filePath, body, opts = {}) {
|
|
169
|
+
const dir = dirname(filePath);
|
|
170
|
+
await fs2.mkdir(dir, { recursive: true });
|
|
171
|
+
const tmp = `${filePath}.tmp-${process.pid}-${randSuffix()}`;
|
|
172
|
+
try {
|
|
173
|
+
const writeOpts = {
|
|
174
|
+
encoding: "utf8"
|
|
175
|
+
};
|
|
176
|
+
if (opts.mode !== void 0) {
|
|
177
|
+
writeOpts.mode = opts.mode;
|
|
178
|
+
}
|
|
179
|
+
await fs2.writeFile(tmp, body, writeOpts);
|
|
180
|
+
await fs2.rename(tmp, filePath);
|
|
181
|
+
} catch (err) {
|
|
182
|
+
await fs2.unlink(tmp).catch(() => void 0);
|
|
183
|
+
throw err;
|
|
184
|
+
}
|
|
185
|
+
if (opts.mode !== void 0) {
|
|
186
|
+
try {
|
|
187
|
+
fsSync.chmodSync(filePath, opts.mode);
|
|
188
|
+
} catch {
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
async function readJsonSafe(filePath) {
|
|
193
|
+
let raw;
|
|
194
|
+
try {
|
|
195
|
+
raw = await fs2.readFile(filePath, "utf8");
|
|
196
|
+
} catch (err) {
|
|
197
|
+
const e = err;
|
|
198
|
+
if (e.code === "ENOENT") {
|
|
199
|
+
return void 0;
|
|
200
|
+
}
|
|
201
|
+
throw err;
|
|
202
|
+
}
|
|
203
|
+
if (raw.trim().length === 0) {
|
|
204
|
+
return void 0;
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
return JSON.parse(raw);
|
|
208
|
+
} catch {
|
|
209
|
+
return void 0;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function dirname(p) {
|
|
213
|
+
const slash = p.lastIndexOf("/");
|
|
214
|
+
if (slash <= 0) {
|
|
215
|
+
return ".";
|
|
216
|
+
}
|
|
217
|
+
return p.slice(0, slash);
|
|
218
|
+
}
|
|
219
|
+
function randSuffix() {
|
|
220
|
+
return randomBytes(4).toString("hex");
|
|
221
|
+
}
|
|
222
|
+
|
|
159
223
|
// src/core/config.ts
|
|
160
224
|
var REGISTRY_URL_DEFAULT = "https://cdn.agentclientprotocol.com/registry/v1/latest/registry.json";
|
|
161
225
|
var TlsConfig = z.object({
|
|
@@ -245,7 +309,22 @@ var TuiConfig = z.object({
|
|
|
245
309
|
// shared across all sessions; it's append-only on disk, so long-lived
|
|
246
310
|
// installs can grow past this — it's enforced at load time and per
|
|
247
311
|
// append in memory.
|
|
248
|
-
promptHistoryMaxEntries: z.number().int().positive().default(2e3)
|
|
312
|
+
promptHistoryMaxEntries: z.number().int().positive().default(2e3),
|
|
313
|
+
// How edit-style tool calls (Edit, Write, str_replace) render in
|
|
314
|
+
// scrollback, *in addition to* the normal tool row inside the tools
|
|
315
|
+
// block.
|
|
316
|
+
// "none" — nothing extra; the collapsed tool row is the only signal.
|
|
317
|
+
// "edit" (default) — a one-line scrollback mark naming the file
|
|
318
|
+
// that was touched, so the user can scroll back and see which
|
|
319
|
+
// files moved without expanding the tools block. Suppressed on
|
|
320
|
+
// tool-only turns (no agent prose) since the marks would only
|
|
321
|
+
// duplicate the still-visible tool rows.
|
|
322
|
+
// "diff" — same mark plus a syntax-highlighted unified diff body,
|
|
323
|
+
// Claude Code's Update(file) look.
|
|
324
|
+
// The diff payload is extracted from the ACP wire (content[]
|
|
325
|
+
// type:"diff" entries, falling back to rawInput shapes), so any agent
|
|
326
|
+
// that emits one of those shapes gets the treatment.
|
|
327
|
+
showFileUpdates: z.enum(["none", "edit", "diff"]).default("edit")
|
|
249
328
|
});
|
|
250
329
|
var ExtensionName = z.string().min(1).regex(/^[A-Za-z0-9._-]+$/, "extension name must be filename-safe");
|
|
251
330
|
var ExtensionBody = z.object({
|
|
@@ -300,7 +379,8 @@ var HydraConfig = z.object({
|
|
|
300
379
|
progressIndicator: true,
|
|
301
380
|
defaultEnterAction: "amend",
|
|
302
381
|
showThoughts: true,
|
|
303
|
-
promptHistoryMaxEntries: 2e3
|
|
382
|
+
promptHistoryMaxEntries: 2e3,
|
|
383
|
+
showFileUpdates: "edit"
|
|
304
384
|
})
|
|
305
385
|
});
|
|
306
386
|
function extensionList(config) {
|
|
@@ -316,17 +396,8 @@ function transformerList(config) {
|
|
|
316
396
|
}));
|
|
317
397
|
}
|
|
318
398
|
async function readConfigFile() {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
raw = await fs2.readFile(paths.config(), "utf8");
|
|
322
|
-
} catch (err) {
|
|
323
|
-
const e = err;
|
|
324
|
-
if (e.code === "ENOENT") {
|
|
325
|
-
return {};
|
|
326
|
-
}
|
|
327
|
-
throw err;
|
|
328
|
-
}
|
|
329
|
-
return JSON.parse(raw);
|
|
399
|
+
const parsed = await readJsonSafe(paths.config());
|
|
400
|
+
return parsed ?? {};
|
|
330
401
|
}
|
|
331
402
|
async function migrateLegacyAuthToken() {
|
|
332
403
|
const raw = await readConfigFile();
|
|
@@ -337,7 +408,7 @@ async function migrateLegacyAuthToken() {
|
|
|
337
408
|
}
|
|
338
409
|
let tokenFileExists = false;
|
|
339
410
|
try {
|
|
340
|
-
await
|
|
411
|
+
await fs3.access(paths.authToken());
|
|
341
412
|
tokenFileExists = true;
|
|
342
413
|
} catch (err) {
|
|
343
414
|
const e = err;
|
|
@@ -355,10 +426,7 @@ async function migrateLegacyAuthToken() {
|
|
|
355
426
|
if (Object.keys(daemon).length === 0) {
|
|
356
427
|
delete raw.daemon;
|
|
357
428
|
}
|
|
358
|
-
await
|
|
359
|
-
encoding: "utf8",
|
|
360
|
-
mode: 384
|
|
361
|
-
});
|
|
429
|
+
await writeJsonAtomic(paths.config(), raw, { mode: 384 });
|
|
362
430
|
process.stderr.write(
|
|
363
431
|
`hydra-acp: migrated auth token from ${paths.config()} to ${paths.authToken()}.
|
|
364
432
|
`
|
|
@@ -369,11 +437,7 @@ async function loadConfig() {
|
|
|
369
437
|
return HydraConfig.parse(await readConfigFile());
|
|
370
438
|
}
|
|
371
439
|
async function writeConfig(config) {
|
|
372
|
-
await
|
|
373
|
-
await fs2.writeFile(paths.config(), JSON.stringify(config, null, 2) + "\n", {
|
|
374
|
-
encoding: "utf8",
|
|
375
|
-
mode: 384
|
|
376
|
-
});
|
|
440
|
+
await writeJsonAtomic(paths.config(), config, { mode: 384 });
|
|
377
441
|
}
|
|
378
442
|
function defaultConfig() {
|
|
379
443
|
return HydraConfig.parse({});
|
|
@@ -392,12 +456,12 @@ function expandHome(p) {
|
|
|
392
456
|
}
|
|
393
457
|
|
|
394
458
|
// src/core/registry.ts
|
|
395
|
-
import * as
|
|
459
|
+
import * as fs5 from "fs/promises";
|
|
396
460
|
import * as path4 from "path";
|
|
397
461
|
import { z as z2 } from "zod";
|
|
398
462
|
|
|
399
463
|
// src/core/binary-install.ts
|
|
400
|
-
import * as
|
|
464
|
+
import * as fs4 from "fs";
|
|
401
465
|
import * as fsp from "fs/promises";
|
|
402
466
|
import * as path2 from "path";
|
|
403
467
|
import { spawn } from "child_process";
|
|
@@ -530,7 +594,7 @@ async function downloadTo(args) {
|
|
|
530
594
|
);
|
|
531
595
|
}
|
|
532
596
|
const total = Number(response.headers.get("content-length") ?? "0");
|
|
533
|
-
const out =
|
|
597
|
+
const out = fs4.createWriteStream(dest);
|
|
534
598
|
const nodeStream = Readable.fromWeb(response.body);
|
|
535
599
|
safeEmit(args.onProgress, {
|
|
536
600
|
phase: "download_start",
|
|
@@ -1030,54 +1094,26 @@ var Registry = class {
|
|
|
1030
1094
|
return cached;
|
|
1031
1095
|
}
|
|
1032
1096
|
async readDiskCache() {
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
if (e.code === "ENOENT") {
|
|
1039
|
-
return void 0;
|
|
1040
|
-
}
|
|
1041
|
-
throw err;
|
|
1097
|
+
const parsed = await readJsonSafe(
|
|
1098
|
+
paths.registryCache()
|
|
1099
|
+
);
|
|
1100
|
+
if (!parsed || typeof parsed.fetchedAt !== "number" || parsed.data === void 0) {
|
|
1101
|
+
return void 0;
|
|
1042
1102
|
}
|
|
1043
1103
|
try {
|
|
1044
|
-
const parsed = JSON.parse(text);
|
|
1045
|
-
if (typeof parsed.fetchedAt !== "number" || parsed.data === void 0) {
|
|
1046
|
-
return void 0;
|
|
1047
|
-
}
|
|
1048
1104
|
const data = RegistryDocument.parse(parsed.data);
|
|
1049
1105
|
return { fetchedAt: parsed.fetchedAt, raw: parsed.data, data };
|
|
1050
1106
|
} catch {
|
|
1051
1107
|
return void 0;
|
|
1052
1108
|
}
|
|
1053
1109
|
}
|
|
1054
|
-
// Atomic write: dump to a sibling temp path, then rename onto the
|
|
1055
|
-
// target. POSIX rename is atomic within a filesystem, so readers
|
|
1056
|
-
// either see the old file or the fully-written new file — never a
|
|
1057
|
-
// truncated middle. This also makes simultaneous writers safe
|
|
1058
|
-
// without a lock file: the loser of the rename race just gets its
|
|
1059
|
-
// version replaced by the winner's.
|
|
1060
1110
|
async writeDiskCache(cache) {
|
|
1061
|
-
await
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
{ fetchedAt: cache.fetchedAt, data: cache.raw },
|
|
1066
|
-
null,
|
|
1067
|
-
2
|
|
1068
|
-
) + "\n";
|
|
1069
|
-
try {
|
|
1070
|
-
await fs4.writeFile(tmp, body, "utf8");
|
|
1071
|
-
await fs4.rename(tmp, final);
|
|
1072
|
-
} catch (err) {
|
|
1073
|
-
await fs4.unlink(tmp).catch(() => void 0);
|
|
1074
|
-
throw err;
|
|
1075
|
-
}
|
|
1111
|
+
await writeJsonAtomic(paths.registryCache(), {
|
|
1112
|
+
fetchedAt: cache.fetchedAt,
|
|
1113
|
+
data: cache.raw
|
|
1114
|
+
});
|
|
1076
1115
|
}
|
|
1077
1116
|
};
|
|
1078
|
-
function randSuffix() {
|
|
1079
|
-
return Math.random().toString(36).slice(2, 10);
|
|
1080
|
-
}
|
|
1081
1117
|
function npxPackageBasename(agent) {
|
|
1082
1118
|
const pkg = agent.distribution.npx?.package;
|
|
1083
1119
|
if (!pkg) {
|
|
@@ -1122,7 +1158,7 @@ async function agentInstallState(agent) {
|
|
|
1122
1158
|
}
|
|
1123
1159
|
async function fileExists3(p) {
|
|
1124
1160
|
try {
|
|
1125
|
-
await
|
|
1161
|
+
await fs5.access(p);
|
|
1126
1162
|
return true;
|
|
1127
1163
|
} catch {
|
|
1128
1164
|
return false;
|
|
@@ -1476,6 +1512,11 @@ var SessionListEntry = z3.object({
|
|
|
1476
1512
|
importedFromUpstreamSessionId: z3.string().optional(),
|
|
1477
1513
|
// Set when this session was spawned as a child by a transformer.
|
|
1478
1514
|
parentSessionId: z3.string().optional(),
|
|
1515
|
+
// Local-fork breadcrumbs set by hydra-acp/fork_session. Distinct from
|
|
1516
|
+
// the imported* family above: a fork is a local branch off another
|
|
1517
|
+
// local session, an import is a cross-machine takeover.
|
|
1518
|
+
forkedFromSessionId: z3.string().optional(),
|
|
1519
|
+
forkedFromMessageId: z3.string().optional(),
|
|
1479
1520
|
// clientInfo from the process that issued session/new. Lets list views
|
|
1480
1521
|
// hide cat-style ancillary sessions by default while letting an
|
|
1481
1522
|
// override flag surface them.
|
|
@@ -2073,7 +2114,7 @@ stderr: ${tail}` : reason;
|
|
|
2073
2114
|
};
|
|
2074
2115
|
|
|
2075
2116
|
// src/core/session-manager.ts
|
|
2076
|
-
import * as
|
|
2117
|
+
import * as fs11 from "fs/promises";
|
|
2077
2118
|
import * as os2 from "os";
|
|
2078
2119
|
import { customAlphabet as customAlphabet3 } from "nanoid";
|
|
2079
2120
|
|
|
@@ -2553,22 +2594,22 @@ function hydraCommandsAsAdvertised() {
|
|
|
2553
2594
|
}
|
|
2554
2595
|
|
|
2555
2596
|
// src/core/queue-store.ts
|
|
2556
|
-
import * as
|
|
2597
|
+
import * as fs6 from "fs/promises";
|
|
2557
2598
|
async function rewriteQueue(sessionId, entries) {
|
|
2558
2599
|
const file = paths.queueFile(sessionId);
|
|
2559
2600
|
if (entries.length === 0) {
|
|
2560
|
-
await
|
|
2601
|
+
await fs6.unlink(file).catch(() => void 0);
|
|
2561
2602
|
return;
|
|
2562
2603
|
}
|
|
2563
|
-
await
|
|
2604
|
+
await fs6.mkdir(paths.sessionDir(sessionId), { recursive: true });
|
|
2564
2605
|
const body = entries.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
2565
|
-
await
|
|
2606
|
+
await fs6.writeFile(file, body, "utf8");
|
|
2566
2607
|
}
|
|
2567
2608
|
async function loadQueue(sessionId) {
|
|
2568
2609
|
const file = paths.queueFile(sessionId);
|
|
2569
2610
|
let text;
|
|
2570
2611
|
try {
|
|
2571
|
-
text = await
|
|
2612
|
+
text = await fs6.readFile(file, "utf8");
|
|
2572
2613
|
} catch (err) {
|
|
2573
2614
|
if (err.code === "ENOENT") {
|
|
2574
2615
|
return [];
|
|
@@ -2590,7 +2631,7 @@ async function loadQueue(sessionId) {
|
|
|
2590
2631
|
}
|
|
2591
2632
|
async function deleteQueue(sessionId) {
|
|
2592
2633
|
const file = paths.queueFile(sessionId);
|
|
2593
|
-
await
|
|
2634
|
+
await fs6.unlink(file).catch(() => void 0);
|
|
2594
2635
|
}
|
|
2595
2636
|
|
|
2596
2637
|
// src/core/session.ts
|
|
@@ -2622,6 +2663,8 @@ var Session = class {
|
|
|
2622
2663
|
agentCapabilities;
|
|
2623
2664
|
agentArgs;
|
|
2624
2665
|
parentSessionId;
|
|
2666
|
+
forkedFromSessionId;
|
|
2667
|
+
forkedFromMessageId;
|
|
2625
2668
|
originatingClient;
|
|
2626
2669
|
title;
|
|
2627
2670
|
// Snapshot state delivered to attaching clients via the attach
|
|
@@ -2650,6 +2693,13 @@ var Session = class {
|
|
|
2650
2693
|
// enqueue) and leave the file out of sync with in-memory state.
|
|
2651
2694
|
queueWriteChain = Promise.resolve();
|
|
2652
2695
|
closed = false;
|
|
2696
|
+
// Set true at the start of close() / markClosed before any await yields.
|
|
2697
|
+
// drainQueue checks this between iterations and bails out, so a queued
|
|
2698
|
+
// entry can't be promoted to currentEntry (with its prompt_received and
|
|
2699
|
+
// synthesized turn_complete(interrupted)) while the session is tearing
|
|
2700
|
+
// down. markClosed sweeps the remaining queue with the normal abandoned
|
|
2701
|
+
// / cancelled handling.
|
|
2702
|
+
closing = false;
|
|
2653
2703
|
closeHandlers = [];
|
|
2654
2704
|
titleHandlers = [];
|
|
2655
2705
|
// Subscribers notified after every entry that's actually persisted to
|
|
@@ -2777,6 +2827,8 @@ var Session = class {
|
|
|
2777
2827
|
this.agentCapabilities = init.agentCapabilities;
|
|
2778
2828
|
this.agentArgs = init.agentArgs;
|
|
2779
2829
|
this.parentSessionId = init.parentSessionId;
|
|
2830
|
+
this.forkedFromSessionId = init.forkedFromSessionId;
|
|
2831
|
+
this.forkedFromMessageId = init.forkedFromMessageId;
|
|
2780
2832
|
this.originatingClient = init.originatingClient;
|
|
2781
2833
|
this.title = init.title;
|
|
2782
2834
|
this.currentModel = init.currentModel;
|
|
@@ -3885,6 +3937,7 @@ var Session = class {
|
|
|
3885
3937
|
if (this.closed) {
|
|
3886
3938
|
return;
|
|
3887
3939
|
}
|
|
3940
|
+
this.closing = true;
|
|
3888
3941
|
this.logger?.info(
|
|
3889
3942
|
`session ${this.sessionId} closing deleteRecord=${opts.deleteRecord ?? false} regenTitle=${opts.regenTitle ?? false}`
|
|
3890
3943
|
);
|
|
@@ -5016,21 +5069,22 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
5016
5069
|
if (this.closed) {
|
|
5017
5070
|
return;
|
|
5018
5071
|
}
|
|
5072
|
+
this.closing = true;
|
|
5019
5073
|
this.closed = true;
|
|
5020
5074
|
this.cancelIdleTimer();
|
|
5021
5075
|
if (this.extensionCommandsUnsub) {
|
|
5022
5076
|
this.extensionCommandsUnsub();
|
|
5023
5077
|
this.extensionCommandsUnsub = void 0;
|
|
5024
5078
|
}
|
|
5025
|
-
if (this.currentEntry?.kind === "user") {
|
|
5079
|
+
if (this.currentEntry?.kind === "user" && !this.recentlyTerminal.has(this.currentEntry.messageId)) {
|
|
5026
5080
|
this.broadcastTurnComplete(
|
|
5027
5081
|
this.currentEntry.clientId,
|
|
5028
5082
|
{ stopReason: "interrupted" },
|
|
5029
5083
|
this.currentEntry.messageId,
|
|
5030
5084
|
this.currentEntry.wasAmend
|
|
5031
5085
|
);
|
|
5032
|
-
this.currentEntry = void 0;
|
|
5033
5086
|
}
|
|
5087
|
+
this.currentEntry = void 0;
|
|
5034
5088
|
const stranded = this.promptQueue;
|
|
5035
5089
|
this.promptQueue = [];
|
|
5036
5090
|
for (const entry of stranded) {
|
|
@@ -5359,6 +5413,9 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
5359
5413
|
await new Promise((r) => setImmediate(r));
|
|
5360
5414
|
try {
|
|
5361
5415
|
while (this.promptQueue.length > 0) {
|
|
5416
|
+
if (this.closing) {
|
|
5417
|
+
break;
|
|
5418
|
+
}
|
|
5362
5419
|
const next = this.promptQueue.shift();
|
|
5363
5420
|
if (!next) {
|
|
5364
5421
|
break;
|
|
@@ -5730,7 +5787,7 @@ function firstLine(text, max) {
|
|
|
5730
5787
|
}
|
|
5731
5788
|
|
|
5732
5789
|
// src/core/session-store.ts
|
|
5733
|
-
import * as
|
|
5790
|
+
import * as fs7 from "fs/promises";
|
|
5734
5791
|
import * as path5 from "path";
|
|
5735
5792
|
import { customAlphabet as customAlphabet2 } from "nanoid";
|
|
5736
5793
|
import { z as z4 } from "zod";
|
|
@@ -5815,6 +5872,12 @@ var SessionRecord = z4.object({
|
|
|
5815
5872
|
// Set when this session was spawned as a child by a transformer via
|
|
5816
5873
|
// hydra-acp/spawn_child_session. Points to the spawning session's id.
|
|
5817
5874
|
parentSessionId: z4.string().optional(),
|
|
5875
|
+
// Set when this session was created by hydra-acp/fork_session.
|
|
5876
|
+
// forkedFromSessionId points to the local source session; forkedFromMessageId
|
|
5877
|
+
// is the resolved forkAt — the messageId of the turn_complete the slice
|
|
5878
|
+
// ended at. Kept so future UI can show "branched from turn N of session X".
|
|
5879
|
+
forkedFromSessionId: z4.string().optional(),
|
|
5880
|
+
forkedFromMessageId: z4.string().optional(),
|
|
5818
5881
|
// clientInfo from the process that issued session/new. Picker and
|
|
5819
5882
|
// `sessions list` use this to hide cat-style ancillary sessions by
|
|
5820
5883
|
// default; carried in meta.json so cold sessions filter the same way.
|
|
@@ -5831,30 +5894,21 @@ function assertSafeId(id) {
|
|
|
5831
5894
|
var SessionStore = class {
|
|
5832
5895
|
async write(record) {
|
|
5833
5896
|
assertSafeId(record.sessionId);
|
|
5834
|
-
await fs6.mkdir(paths.sessionDir(record.sessionId), { recursive: true });
|
|
5835
5897
|
const full = { version: 1, ...record };
|
|
5836
|
-
await
|
|
5837
|
-
|
|
5838
|
-
|
|
5839
|
-
{ encoding: "utf8", mode: 384 }
|
|
5840
|
-
);
|
|
5898
|
+
await writeJsonAtomic(paths.sessionFile(record.sessionId), full, {
|
|
5899
|
+
mode: 384
|
|
5900
|
+
});
|
|
5841
5901
|
}
|
|
5842
5902
|
async read(sessionId) {
|
|
5843
5903
|
if (!SESSION_ID_PATTERN.test(sessionId)) {
|
|
5844
5904
|
return void 0;
|
|
5845
5905
|
}
|
|
5846
|
-
|
|
5847
|
-
|
|
5848
|
-
|
|
5849
|
-
} catch (err) {
|
|
5850
|
-
const e = err;
|
|
5851
|
-
if (e.code === "ENOENT") {
|
|
5852
|
-
return void 0;
|
|
5853
|
-
}
|
|
5854
|
-
throw err;
|
|
5906
|
+
const parsed = await readJsonSafe(paths.sessionFile(sessionId));
|
|
5907
|
+
if (parsed === void 0) {
|
|
5908
|
+
return void 0;
|
|
5855
5909
|
}
|
|
5856
5910
|
try {
|
|
5857
|
-
return SessionRecord.parse(
|
|
5911
|
+
return SessionRecord.parse(parsed);
|
|
5858
5912
|
} catch {
|
|
5859
5913
|
return void 0;
|
|
5860
5914
|
}
|
|
@@ -5864,7 +5918,7 @@ var SessionStore = class {
|
|
|
5864
5918
|
return;
|
|
5865
5919
|
}
|
|
5866
5920
|
try {
|
|
5867
|
-
await
|
|
5921
|
+
await fs7.unlink(paths.sessionFile(sessionId));
|
|
5868
5922
|
} catch (err) {
|
|
5869
5923
|
const e = err;
|
|
5870
5924
|
if (e.code !== "ENOENT") {
|
|
@@ -5872,7 +5926,7 @@ var SessionStore = class {
|
|
|
5872
5926
|
}
|
|
5873
5927
|
}
|
|
5874
5928
|
try {
|
|
5875
|
-
await
|
|
5929
|
+
await fs7.rmdir(paths.sessionDir(sessionId));
|
|
5876
5930
|
} catch (err) {
|
|
5877
5931
|
const e = err;
|
|
5878
5932
|
if (e.code !== "ENOENT" && e.code !== "ENOTEMPTY") {
|
|
@@ -5902,7 +5956,7 @@ var SessionStore = class {
|
|
|
5902
5956
|
async list() {
|
|
5903
5957
|
let entries;
|
|
5904
5958
|
try {
|
|
5905
|
-
entries = await
|
|
5959
|
+
entries = await fs7.readdir(paths.sessionsDir());
|
|
5906
5960
|
} catch (err) {
|
|
5907
5961
|
const e = err;
|
|
5908
5962
|
if (e.code === "ENOENT") {
|
|
@@ -5941,6 +5995,8 @@ function recordFromMemorySession(args) {
|
|
|
5941
5995
|
agentModels: args.agentModels,
|
|
5942
5996
|
pendingHistorySync: args.pendingHistorySync,
|
|
5943
5997
|
parentSessionId: args.parentSessionId,
|
|
5998
|
+
forkedFromSessionId: args.forkedFromSessionId,
|
|
5999
|
+
forkedFromMessageId: args.forkedFromMessageId,
|
|
5944
6000
|
originatingClient: args.originatingClient,
|
|
5945
6001
|
createdAt: args.createdAt ?? now,
|
|
5946
6002
|
updatedAt: args.updatedAt ?? now
|
|
@@ -5948,7 +6004,7 @@ function recordFromMemorySession(args) {
|
|
|
5948
6004
|
}
|
|
5949
6005
|
|
|
5950
6006
|
// src/core/history-store.ts
|
|
5951
|
-
import * as
|
|
6007
|
+
import * as fs8 from "fs/promises";
|
|
5952
6008
|
var SESSION_ID_PATTERN2 = /^[A-Za-z0-9_-]+$/;
|
|
5953
6009
|
var DEFAULT_MAX_ENTRIES = 1e3;
|
|
5954
6010
|
var HistoryStore = class {
|
|
@@ -5965,9 +6021,9 @@ var HistoryStore = class {
|
|
|
5965
6021
|
return;
|
|
5966
6022
|
}
|
|
5967
6023
|
return this.enqueue(sessionId, async () => {
|
|
5968
|
-
await
|
|
6024
|
+
await fs8.mkdir(paths.sessionDir(sessionId), { recursive: true });
|
|
5969
6025
|
const line = JSON.stringify(entry) + "\n";
|
|
5970
|
-
await
|
|
6026
|
+
await fs8.appendFile(paths.historyFile(sessionId), line, {
|
|
5971
6027
|
encoding: "utf8",
|
|
5972
6028
|
mode: 384
|
|
5973
6029
|
});
|
|
@@ -5978,9 +6034,9 @@ var HistoryStore = class {
|
|
|
5978
6034
|
return;
|
|
5979
6035
|
}
|
|
5980
6036
|
return this.enqueue(sessionId, async () => {
|
|
5981
|
-
await
|
|
6037
|
+
await fs8.mkdir(paths.sessionDir(sessionId), { recursive: true });
|
|
5982
6038
|
const body = entries.length === 0 ? "" : entries.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
5983
|
-
await
|
|
6039
|
+
await fs8.writeFile(paths.historyFile(sessionId), body, {
|
|
5984
6040
|
encoding: "utf8",
|
|
5985
6041
|
mode: 384
|
|
5986
6042
|
});
|
|
@@ -5997,7 +6053,7 @@ var HistoryStore = class {
|
|
|
5997
6053
|
return this.enqueue(sessionId, async () => {
|
|
5998
6054
|
let raw;
|
|
5999
6055
|
try {
|
|
6000
|
-
raw = await
|
|
6056
|
+
raw = await fs8.readFile(paths.historyFile(sessionId), "utf8");
|
|
6001
6057
|
} catch (err) {
|
|
6002
6058
|
const e = err;
|
|
6003
6059
|
if (e.code === "ENOENT") {
|
|
@@ -6010,7 +6066,7 @@ var HistoryStore = class {
|
|
|
6010
6066
|
return;
|
|
6011
6067
|
}
|
|
6012
6068
|
const trimmed = lines.slice(-maxEntries);
|
|
6013
|
-
await
|
|
6069
|
+
await fs8.writeFile(paths.historyFile(sessionId), trimmed.join("\n") + "\n", {
|
|
6014
6070
|
encoding: "utf8",
|
|
6015
6071
|
mode: 384
|
|
6016
6072
|
});
|
|
@@ -6026,7 +6082,7 @@ var HistoryStore = class {
|
|
|
6026
6082
|
}
|
|
6027
6083
|
let raw;
|
|
6028
6084
|
try {
|
|
6029
|
-
raw = await
|
|
6085
|
+
raw = await fs8.readFile(paths.historyFile(sessionId), "utf8");
|
|
6030
6086
|
} catch (err) {
|
|
6031
6087
|
const e = err;
|
|
6032
6088
|
if (e.code === "ENOENT") {
|
|
@@ -6066,13 +6122,26 @@ var HistoryStore = class {
|
|
|
6066
6122
|
}
|
|
6067
6123
|
return out;
|
|
6068
6124
|
}
|
|
6125
|
+
// Wait for every pending append/rewrite/compact across all sessions to
|
|
6126
|
+
// settle. Daemon shutdown calls this after closing sessions so the final
|
|
6127
|
+
// turn_complete(interrupted) emitted by markClosed reaches disk before
|
|
6128
|
+
// the process exits — without this, history-replay attaches after a
|
|
6129
|
+
// restart see an unmatched prompt_received and leak pendingTurns on
|
|
6130
|
+
// every client.
|
|
6131
|
+
async flushAll() {
|
|
6132
|
+
const pending = [...this.writeQueues.values()];
|
|
6133
|
+
if (pending.length === 0) {
|
|
6134
|
+
return;
|
|
6135
|
+
}
|
|
6136
|
+
await Promise.allSettled(pending);
|
|
6137
|
+
}
|
|
6069
6138
|
async delete(sessionId) {
|
|
6070
6139
|
if (!SESSION_ID_PATTERN2.test(sessionId)) {
|
|
6071
6140
|
return;
|
|
6072
6141
|
}
|
|
6073
6142
|
return this.enqueue(sessionId, async () => {
|
|
6074
6143
|
try {
|
|
6075
|
-
await
|
|
6144
|
+
await fs8.unlink(paths.historyFile(sessionId));
|
|
6076
6145
|
} catch (err) {
|
|
6077
6146
|
const e = err;
|
|
6078
6147
|
if (e.code !== "ENOENT") {
|
|
@@ -6080,7 +6149,7 @@ var HistoryStore = class {
|
|
|
6080
6149
|
}
|
|
6081
6150
|
}
|
|
6082
6151
|
try {
|
|
6083
|
-
await
|
|
6152
|
+
await fs8.rmdir(paths.sessionDir(sessionId));
|
|
6084
6153
|
} catch (err) {
|
|
6085
6154
|
const e = err;
|
|
6086
6155
|
if (e.code !== "ENOENT" && e.code !== "ENOTEMPTY") {
|
|
@@ -6104,25 +6173,108 @@ var HistoryStore = class {
|
|
|
6104
6173
|
};
|
|
6105
6174
|
|
|
6106
6175
|
// src/tui/history.ts
|
|
6107
|
-
import { promises as
|
|
6176
|
+
import { promises as fs9 } from "fs";
|
|
6108
6177
|
import * as path6 from "path";
|
|
6109
6178
|
async function saveHistory(file, history) {
|
|
6110
|
-
await
|
|
6179
|
+
await fs9.mkdir(path6.dirname(file), { recursive: true });
|
|
6111
6180
|
const lines = history.map((entry) => JSON.stringify(entry));
|
|
6112
|
-
await
|
|
6181
|
+
await fs9.writeFile(file, lines.length > 0 ? lines.join("\n") + "\n" : "");
|
|
6182
|
+
}
|
|
6183
|
+
|
|
6184
|
+
// src/core/bundle.ts
|
|
6185
|
+
import { z as z5 } from "zod";
|
|
6186
|
+
var HistoryEntrySchema = z5.object({
|
|
6187
|
+
method: z5.string(),
|
|
6188
|
+
params: z5.unknown(),
|
|
6189
|
+
recordedAt: z5.number()
|
|
6190
|
+
});
|
|
6191
|
+
var BundleSession = z5.object({
|
|
6192
|
+
// The exporter's local id. Regenerated fresh on import (sessionId is
|
|
6193
|
+
// the local namespace; lineageId is what survives across hops).
|
|
6194
|
+
sessionId: z5.string(),
|
|
6195
|
+
// Required on bundles — the export path backfills if the source
|
|
6196
|
+
// record was written before lineageId existed.
|
|
6197
|
+
lineageId: z5.string(),
|
|
6198
|
+
// The exporter's agent-side session id at export time. Carried so
|
|
6199
|
+
// importers can persist it as a breadcrumb (and, eventually, as the
|
|
6200
|
+
// handle a "connect back to origin" feature would need). Omitted on
|
|
6201
|
+
// bundles whose source record never bound to an agent (e.g. a
|
|
6202
|
+
// re-export of an imported, not-yet-attached session).
|
|
6203
|
+
upstreamSessionId: z5.string().optional(),
|
|
6204
|
+
agentId: z5.string(),
|
|
6205
|
+
cwd: z5.string(),
|
|
6206
|
+
title: z5.string().optional(),
|
|
6207
|
+
currentModel: z5.string().optional(),
|
|
6208
|
+
currentMode: z5.string().optional(),
|
|
6209
|
+
currentUsage: PersistedUsage.optional(),
|
|
6210
|
+
agentCommands: z5.array(PersistedAgentCommand).optional(),
|
|
6211
|
+
agentModes: z5.array(PersistedAgentMode).optional(),
|
|
6212
|
+
createdAt: z5.string(),
|
|
6213
|
+
updatedAt: z5.string()
|
|
6214
|
+
});
|
|
6215
|
+
var Bundle = z5.object({
|
|
6216
|
+
version: z5.literal(1),
|
|
6217
|
+
exportedAt: z5.string(),
|
|
6218
|
+
exportedFrom: z5.object({
|
|
6219
|
+
hydraVersion: z5.string(),
|
|
6220
|
+
machine: z5.string(),
|
|
6221
|
+
// Externally-reachable name (and optional ":port") for the exporting
|
|
6222
|
+
// daemon, sourced from config.daemon.publicHost (or daemon.host when
|
|
6223
|
+
// non-loopback). Carried so an importer can construct a hydra:// URL
|
|
6224
|
+
// that dials back to the origin — e.g. over Tailscale. Omitted when
|
|
6225
|
+
// the exporter has no routable address; never falls back to loopback.
|
|
6226
|
+
hydraHost: z5.string().optional()
|
|
6227
|
+
}),
|
|
6228
|
+
session: BundleSession,
|
|
6229
|
+
history: z5.array(HistoryEntrySchema),
|
|
6230
|
+
promptHistory: z5.array(z5.string()).optional()
|
|
6231
|
+
});
|
|
6232
|
+
function encodeBundle(params) {
|
|
6233
|
+
const bundle = {
|
|
6234
|
+
version: 1,
|
|
6235
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6236
|
+
exportedFrom: {
|
|
6237
|
+
hydraVersion: params.hydraVersion,
|
|
6238
|
+
machine: params.machine,
|
|
6239
|
+
...params.hydraHost !== void 0 && params.hydraHost.length > 0 ? { hydraHost: params.hydraHost } : {}
|
|
6240
|
+
},
|
|
6241
|
+
session: {
|
|
6242
|
+
sessionId: params.record.sessionId,
|
|
6243
|
+
lineageId: params.record.lineageId,
|
|
6244
|
+
...params.record.upstreamSessionId ? { upstreamSessionId: params.record.upstreamSessionId } : {},
|
|
6245
|
+
agentId: params.record.agentId,
|
|
6246
|
+
cwd: params.record.cwd,
|
|
6247
|
+
...params.record.title !== void 0 ? { title: params.record.title } : {},
|
|
6248
|
+
...params.record.currentModel !== void 0 ? { currentModel: params.record.currentModel } : {},
|
|
6249
|
+
...params.record.currentMode !== void 0 ? { currentMode: params.record.currentMode } : {},
|
|
6250
|
+
...params.record.currentUsage !== void 0 ? { currentUsage: params.record.currentUsage } : {},
|
|
6251
|
+
...params.record.agentCommands !== void 0 ? { agentCommands: params.record.agentCommands } : {},
|
|
6252
|
+
...params.record.agentModes !== void 0 ? { agentModes: params.record.agentModes } : {},
|
|
6253
|
+
createdAt: params.record.createdAt,
|
|
6254
|
+
updatedAt: params.record.updatedAt
|
|
6255
|
+
},
|
|
6256
|
+
history: params.history
|
|
6257
|
+
};
|
|
6258
|
+
if (params.promptHistory !== void 0) {
|
|
6259
|
+
bundle.promptHistory = params.promptHistory;
|
|
6260
|
+
}
|
|
6261
|
+
return bundle;
|
|
6262
|
+
}
|
|
6263
|
+
function decodeBundle(raw) {
|
|
6264
|
+
return Bundle.parse(raw);
|
|
6113
6265
|
}
|
|
6114
6266
|
|
|
6115
6267
|
// src/core/hydra-version.ts
|
|
6116
6268
|
import { fileURLToPath } from "url";
|
|
6117
6269
|
import * as path7 from "path";
|
|
6118
|
-
import * as
|
|
6270
|
+
import * as fs10 from "fs";
|
|
6119
6271
|
function resolveVersion() {
|
|
6120
6272
|
try {
|
|
6121
6273
|
let dir = path7.dirname(fileURLToPath(import.meta.url));
|
|
6122
6274
|
for (let i = 0; i < 8; i += 1) {
|
|
6123
6275
|
const candidate = path7.join(dir, "package.json");
|
|
6124
|
-
if (
|
|
6125
|
-
const pkg = JSON.parse(
|
|
6276
|
+
if (fs10.existsSync(candidate)) {
|
|
6277
|
+
const pkg = JSON.parse(fs10.readFileSync(candidate, "utf8"));
|
|
6126
6278
|
if (typeof pkg.version === "string" && pkg.version.length > 0 && (typeof pkg.name !== "string" || pkg.name.includes("hydra-acp"))) {
|
|
6127
6279
|
return pkg.version;
|
|
6128
6280
|
}
|
|
@@ -6403,6 +6555,8 @@ var SessionManager = class {
|
|
|
6403
6555
|
firstPromptSeeded: !!params.title,
|
|
6404
6556
|
createdAt: params.createdAt ? new Date(params.createdAt).getTime() : void 0,
|
|
6405
6557
|
originatingClient: params.originatingClient,
|
|
6558
|
+
forkedFromSessionId: params.forkedFromSessionId,
|
|
6559
|
+
forkedFromMessageId: params.forkedFromMessageId,
|
|
6406
6560
|
extensionCommands: this.extensionCommands
|
|
6407
6561
|
});
|
|
6408
6562
|
await this.attachManagerHooks(session);
|
|
@@ -6471,6 +6625,8 @@ var SessionManager = class {
|
|
|
6471
6625
|
firstPromptSeeded: !!params.title,
|
|
6472
6626
|
createdAt: params.createdAt ? new Date(params.createdAt).getTime() : void 0,
|
|
6473
6627
|
originatingClient: params.originatingClient,
|
|
6628
|
+
forkedFromSessionId: params.forkedFromSessionId,
|
|
6629
|
+
forkedFromMessageId: params.forkedFromMessageId,
|
|
6474
6630
|
extensionCommands: this.extensionCommands
|
|
6475
6631
|
});
|
|
6476
6632
|
await this.attachManagerHooks(session);
|
|
@@ -6479,7 +6635,7 @@ var SessionManager = class {
|
|
|
6479
6635
|
}
|
|
6480
6636
|
async resolveImportCwd(cwd) {
|
|
6481
6637
|
try {
|
|
6482
|
-
const stat2 = await
|
|
6638
|
+
const stat2 = await fs11.stat(cwd);
|
|
6483
6639
|
if (stat2.isDirectory()) {
|
|
6484
6640
|
return cwd;
|
|
6485
6641
|
}
|
|
@@ -6821,7 +6977,9 @@ var SessionManager = class {
|
|
|
6821
6977
|
agentModels: record.agentModels,
|
|
6822
6978
|
createdAt: record.createdAt,
|
|
6823
6979
|
pendingHistorySync: record.pendingHistorySync,
|
|
6824
|
-
originatingClient: record.originatingClient
|
|
6980
|
+
originatingClient: record.originatingClient,
|
|
6981
|
+
forkedFromSessionId: record.forkedFromSessionId,
|
|
6982
|
+
forkedFromMessageId: record.forkedFromMessageId
|
|
6825
6983
|
};
|
|
6826
6984
|
}
|
|
6827
6985
|
async clearPendingHistorySync(sessionId) {
|
|
@@ -6922,6 +7080,8 @@ var SessionManager = class {
|
|
|
6922
7080
|
currentModel: session.currentModel,
|
|
6923
7081
|
currentUsage: session.currentUsage,
|
|
6924
7082
|
parentSessionId: session.parentSessionId,
|
|
7083
|
+
forkedFromSessionId: session.forkedFromSessionId,
|
|
7084
|
+
forkedFromMessageId: session.forkedFromMessageId,
|
|
6925
7085
|
originatingClient: session.originatingClient,
|
|
6926
7086
|
updatedAt: used,
|
|
6927
7087
|
attachedClients: session.attachedCount,
|
|
@@ -6952,6 +7112,8 @@ var SessionManager = class {
|
|
|
6952
7112
|
importedFromMachine: r.importedFromMachine,
|
|
6953
7113
|
importedFromUpstreamSessionId: r.importedFromUpstreamSessionId,
|
|
6954
7114
|
parentSessionId: r.parentSessionId,
|
|
7115
|
+
forkedFromSessionId: r.forkedFromSessionId,
|
|
7116
|
+
forkedFromMessageId: r.forkedFromMessageId,
|
|
6955
7117
|
originatingClient: r.originatingClient,
|
|
6956
7118
|
updatedAt: used,
|
|
6957
7119
|
attachedClients: 0,
|
|
@@ -7039,10 +7201,114 @@ var SessionManager = class {
|
|
|
7039
7201
|
replaced: false
|
|
7040
7202
|
};
|
|
7041
7203
|
}
|
|
7042
|
-
//
|
|
7043
|
-
//
|
|
7204
|
+
// Branch an existing local session into a new one that shares context
|
|
7205
|
+
// up to the chosen turn boundary and diverges from there. Composes the
|
|
7206
|
+
// import pipeline: synthesizes a Bundle from the source's record and
|
|
7207
|
+
// sliced history, mints a fresh lineageId, then writes the new record
|
|
7208
|
+
// via writeImportedRecord with forked* breadcrumbs instead of
|
|
7209
|
+
// imported*. The fork carries upstreamSessionId="" so the first attach
|
|
7210
|
+
// triggers seedFromImport — same wire shape as an imported session.
|
|
7211
|
+
//
|
|
7212
|
+
// forkAt defaults to the messageId of the source's most recent
|
|
7213
|
+
// turn_complete; explicit forkAt must reference a session/update
|
|
7214
|
+
// entry that's present in the source's history.jsonl. Cutting at a
|
|
7215
|
+
// completed turn excludes any in-flight prompt by construction
|
|
7216
|
+
// (history.jsonl is appended serially per session), so no locking
|
|
7217
|
+
// against the live source is needed.
|
|
7218
|
+
//
|
|
7219
|
+
// agentId defaults to the source's agent. Overriding to a different
|
|
7220
|
+
// agent scrubs agent-specific state from the fork (model, mode,
|
|
7221
|
+
// usage, agent-emitted commands/modes/models) so the new agent boots
|
|
7222
|
+
// clean — title and conversation transcript are agent-agnostic and
|
|
7223
|
+
// are kept.
|
|
7224
|
+
async forkSession(sourceSessionId, opts = {}) {
|
|
7225
|
+
const sourceRecord = await this.store.read(sourceSessionId);
|
|
7226
|
+
if (!sourceRecord) {
|
|
7227
|
+
const err = new Error(`source session not found: ${sourceSessionId}`);
|
|
7228
|
+
err.code = JsonRpcErrorCodes.SessionNotFound;
|
|
7229
|
+
throw err;
|
|
7230
|
+
}
|
|
7231
|
+
const targetAgentId = opts.agentId ?? sourceRecord.agentId;
|
|
7232
|
+
const crossAgent = targetAgentId !== sourceRecord.agentId;
|
|
7233
|
+
if (crossAgent) {
|
|
7234
|
+
const def = await this.registry.getAgent(targetAgentId);
|
|
7235
|
+
if (!def) {
|
|
7236
|
+
const err = new Error(
|
|
7237
|
+
`agent ${targetAgentId} not found in registry`
|
|
7238
|
+
);
|
|
7239
|
+
err.code = JsonRpcErrorCodes.AgentNotInstalled;
|
|
7240
|
+
throw err;
|
|
7241
|
+
}
|
|
7242
|
+
}
|
|
7243
|
+
const sourceHistory = await this.histories.load(sourceSessionId).catch(() => []);
|
|
7244
|
+
let cutoffIndex;
|
|
7245
|
+
let forkedAt;
|
|
7246
|
+
if (opts.forkAt !== void 0) {
|
|
7247
|
+
cutoffIndex = findMessageIdIndex(sourceHistory, opts.forkAt);
|
|
7248
|
+
if (cutoffIndex < 0) {
|
|
7249
|
+
const err = new Error(
|
|
7250
|
+
`forkAt messageId not found in source history: ${opts.forkAt}`
|
|
7251
|
+
);
|
|
7252
|
+
err.code = JsonRpcErrorCodes.InvalidParams;
|
|
7253
|
+
throw err;
|
|
7254
|
+
}
|
|
7255
|
+
forkedAt = opts.forkAt;
|
|
7256
|
+
} else {
|
|
7257
|
+
const found = findLastTurnComplete(sourceHistory);
|
|
7258
|
+
if (!found) {
|
|
7259
|
+
const err = new Error(
|
|
7260
|
+
`source session ${sourceSessionId} has no completed turns to fork from`
|
|
7261
|
+
);
|
|
7262
|
+
err.code = JsonRpcErrorCodes.InvalidParams;
|
|
7263
|
+
throw err;
|
|
7264
|
+
}
|
|
7265
|
+
cutoffIndex = found.index;
|
|
7266
|
+
forkedAt = found.messageId;
|
|
7267
|
+
}
|
|
7268
|
+
const slicedHistory = sourceHistory.slice(0, cutoffIndex + 1);
|
|
7269
|
+
const promptHistory = await loadPromptHistorySafely(sourceSessionId);
|
|
7270
|
+
const recordForBundle = {
|
|
7271
|
+
...sourceRecord,
|
|
7272
|
+
lineageId: generateLineageId(),
|
|
7273
|
+
agentId: targetAgentId,
|
|
7274
|
+
...crossAgent ? {
|
|
7275
|
+
currentModel: void 0,
|
|
7276
|
+
currentMode: void 0,
|
|
7277
|
+
currentUsage: void 0,
|
|
7278
|
+
agentCommands: void 0,
|
|
7279
|
+
agentModes: void 0,
|
|
7280
|
+
agentModels: void 0
|
|
7281
|
+
} : {}
|
|
7282
|
+
};
|
|
7283
|
+
const bundle = encodeBundle({
|
|
7284
|
+
record: recordForBundle,
|
|
7285
|
+
history: slicedHistory,
|
|
7286
|
+
promptHistory: promptHistory.length > 0 ? promptHistory : void 0,
|
|
7287
|
+
hydraVersion: HYDRA_VERSION,
|
|
7288
|
+
machine: os2.hostname()
|
|
7289
|
+
});
|
|
7290
|
+
const newId = `${HYDRA_SESSION_PREFIX}${generateRawSessionId()}`;
|
|
7291
|
+
await this.writeImportedRecord({
|
|
7292
|
+
sessionId: newId,
|
|
7293
|
+
bundle,
|
|
7294
|
+
cwd: opts.cwd,
|
|
7295
|
+
forkedFromSessionId: sourceSessionId,
|
|
7296
|
+
forkedFromMessageId: forkedAt
|
|
7297
|
+
});
|
|
7298
|
+
return {
|
|
7299
|
+
sessionId: newId,
|
|
7300
|
+
forkedFromSessionId: sourceSessionId,
|
|
7301
|
+
forkedAt
|
|
7302
|
+
};
|
|
7303
|
+
}
|
|
7304
|
+
// Write the imported (or forked) bundle's history.jsonl, prompt-history
|
|
7305
|
+
// (if present), and meta.json. upstreamSessionId is left empty as the
|
|
7044
7306
|
// marker that the first attach should bootstrap a fresh agent and
|
|
7045
|
-
// run seedFromImport rather than calling session/load.
|
|
7307
|
+
// run seedFromImport rather than calling session/load. When
|
|
7308
|
+
// forkedFromSessionId is set, the record is marked as a local fork
|
|
7309
|
+
// (forked* fields populated) instead of a cross-machine import
|
|
7310
|
+
// (imported* fields populated) — both share the seed-on-first-attach
|
|
7311
|
+
// wire shape but trace differently in list views.
|
|
7046
7312
|
async writeImportedRecord(args) {
|
|
7047
7313
|
await this.histories.rewrite(
|
|
7048
7314
|
args.sessionId,
|
|
@@ -7050,7 +7316,7 @@ var SessionManager = class {
|
|
|
7050
7316
|
);
|
|
7051
7317
|
const sourceMtime = new Date(args.bundle.session.updatedAt);
|
|
7052
7318
|
if (!Number.isNaN(sourceMtime.getTime())) {
|
|
7053
|
-
await
|
|
7319
|
+
await fs11.utimes(paths.historyFile(args.sessionId), sourceMtime, sourceMtime).catch(() => void 0);
|
|
7054
7320
|
}
|
|
7055
7321
|
if (args.bundle.promptHistory && args.bundle.promptHistory.length > 0) {
|
|
7056
7322
|
await saveHistory(
|
|
@@ -7059,14 +7325,20 @@ var SessionManager = class {
|
|
|
7059
7325
|
).catch(() => void 0);
|
|
7060
7326
|
}
|
|
7061
7327
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
7328
|
+
const isFork = args.forkedFromSessionId !== void 0;
|
|
7062
7329
|
await this.enqueueMetaWrite(args.sessionId, async () => {
|
|
7063
7330
|
await this.store.write({
|
|
7064
7331
|
sessionId: args.sessionId,
|
|
7065
7332
|
lineageId: args.bundle.session.lineageId,
|
|
7066
7333
|
upstreamSessionId: "",
|
|
7067
|
-
|
|
7068
|
-
|
|
7069
|
-
|
|
7334
|
+
...isFork ? {
|
|
7335
|
+
forkedFromSessionId: args.forkedFromSessionId,
|
|
7336
|
+
forkedFromMessageId: args.forkedFromMessageId
|
|
7337
|
+
} : {
|
|
7338
|
+
importedFromSessionId: args.bundle.session.sessionId,
|
|
7339
|
+
importedFromUpstreamSessionId: args.bundle.session.upstreamSessionId,
|
|
7340
|
+
importedFromMachine: args.bundle.exportedFrom.machine
|
|
7341
|
+
},
|
|
7070
7342
|
agentId: args.bundle.session.agentId,
|
|
7071
7343
|
cwd: args.cwd ?? args.bundle.session.cwd,
|
|
7072
7344
|
title: args.bundle.session.title,
|
|
@@ -7200,6 +7472,14 @@ var SessionManager = class {
|
|
|
7200
7472
|
}
|
|
7201
7473
|
await Promise.allSettled(pending);
|
|
7202
7474
|
}
|
|
7475
|
+
// Wait for every pending history.jsonl write to settle. markClosed
|
|
7476
|
+
// broadcasts turn_complete(interrupted) for the in-flight turn via a
|
|
7477
|
+
// fire-and-forget store.append; without flushing, a SIGTERM can exit
|
|
7478
|
+
// before that append hits disk, leaving an unmatched prompt_received
|
|
7479
|
+
// in history that leaks pendingTurns on every client that replays it.
|
|
7480
|
+
async flushHistoryWrites() {
|
|
7481
|
+
await this.histories.flushAll();
|
|
7482
|
+
}
|
|
7203
7483
|
// Startup hook: scan persisted sessions for non-empty queue files,
|
|
7204
7484
|
// apply the TTL, resurrect anything with surviving entries, and
|
|
7205
7485
|
// replay them through the normal queue path. Called from the daemon
|
|
@@ -7298,6 +7578,8 @@ function mergeForPersistence(session, existing) {
|
|
|
7298
7578
|
agentModes,
|
|
7299
7579
|
agentModels,
|
|
7300
7580
|
parentSessionId: session.parentSessionId ?? existing?.parentSessionId,
|
|
7581
|
+
forkedFromSessionId: session.forkedFromSessionId ?? existing?.forkedFromSessionId,
|
|
7582
|
+
forkedFromMessageId: session.forkedFromMessageId ?? existing?.forkedFromMessageId,
|
|
7301
7583
|
originatingClient: session.originatingClient ?? existing?.originatingClient,
|
|
7302
7584
|
createdAt: existing?.createdAt ?? new Date(session.createdAt).toISOString()
|
|
7303
7585
|
});
|
|
@@ -7567,9 +7849,26 @@ function parseModesList(list) {
|
|
|
7567
7849
|
}
|
|
7568
7850
|
return out;
|
|
7569
7851
|
}
|
|
7852
|
+
function findLastTurnComplete(history) {
|
|
7853
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
7854
|
+
const entry = history[i];
|
|
7855
|
+
if (!entry || entry.method !== "session/update") {
|
|
7856
|
+
continue;
|
|
7857
|
+
}
|
|
7858
|
+
const update = entry.params?.update;
|
|
7859
|
+
if (update?.sessionUpdate !== "turn_complete") {
|
|
7860
|
+
continue;
|
|
7861
|
+
}
|
|
7862
|
+
if (typeof update.messageId !== "string" || update.messageId.length === 0) {
|
|
7863
|
+
continue;
|
|
7864
|
+
}
|
|
7865
|
+
return { index: i, messageId: update.messageId };
|
|
7866
|
+
}
|
|
7867
|
+
return void 0;
|
|
7868
|
+
}
|
|
7570
7869
|
async function loadPromptHistorySafely(sessionId) {
|
|
7571
7870
|
try {
|
|
7572
|
-
const raw = await
|
|
7871
|
+
const raw = await fs11.readFile(paths.tuiHistoryFile(sessionId), "utf8");
|
|
7573
7872
|
const out = [];
|
|
7574
7873
|
for (const line of raw.split("\n")) {
|
|
7575
7874
|
if (line.length === 0) {
|
|
@@ -7590,7 +7889,7 @@ async function loadPromptHistorySafely(sessionId) {
|
|
|
7590
7889
|
}
|
|
7591
7890
|
async function historyMtimeIso(sessionId) {
|
|
7592
7891
|
try {
|
|
7593
|
-
const st = await
|
|
7892
|
+
const st = await fs11.stat(paths.historyFile(sessionId));
|
|
7594
7893
|
return new Date(st.mtimeMs).toISOString();
|
|
7595
7894
|
} catch {
|
|
7596
7895
|
return void 0;
|
|
@@ -7599,7 +7898,7 @@ async function historyMtimeIso(sessionId) {
|
|
|
7599
7898
|
|
|
7600
7899
|
// src/core/extensions.ts
|
|
7601
7900
|
import { spawn as spawn4 } from "child_process";
|
|
7602
|
-
import * as
|
|
7901
|
+
import * as fs12 from "fs";
|
|
7603
7902
|
import * as fsp5 from "fs/promises";
|
|
7604
7903
|
import * as path8 from "path";
|
|
7605
7904
|
var RESTART_BASE_MS = 1e3;
|
|
@@ -7895,7 +8194,7 @@ var ExtensionManager = class {
|
|
|
7895
8194
|
}
|
|
7896
8195
|
const ext = entry.config;
|
|
7897
8196
|
const command = ext.command.length > 0 ? ext.command : [ext.name];
|
|
7898
|
-
const logStream =
|
|
8197
|
+
const logStream = fs12.createWriteStream(paths.extensionLogFile(ext.name), {
|
|
7899
8198
|
flags: "a"
|
|
7900
8199
|
});
|
|
7901
8200
|
logStream.write(
|
|
@@ -7948,7 +8247,7 @@ var ExtensionManager = class {
|
|
|
7948
8247
|
}
|
|
7949
8248
|
if (typeof child.pid === "number") {
|
|
7950
8249
|
try {
|
|
7951
|
-
|
|
8250
|
+
fs12.writeFileSync(paths.extensionPidFile(ext.name), `${child.pid}
|
|
7952
8251
|
`, {
|
|
7953
8252
|
encoding: "utf8",
|
|
7954
8253
|
mode: 384
|
|
@@ -7973,7 +8272,7 @@ var ExtensionManager = class {
|
|
|
7973
8272
|
});
|
|
7974
8273
|
child.on("exit", (code, signal) => {
|
|
7975
8274
|
try {
|
|
7976
|
-
|
|
8275
|
+
fs12.unlinkSync(paths.extensionPidFile(ext.name));
|
|
7977
8276
|
} catch {
|
|
7978
8277
|
}
|
|
7979
8278
|
logStream.write(
|
|
@@ -8035,7 +8334,7 @@ function withCode2(err, code) {
|
|
|
8035
8334
|
|
|
8036
8335
|
// src/core/transformer-manager.ts
|
|
8037
8336
|
import { spawn as spawn5 } from "child_process";
|
|
8038
|
-
import * as
|
|
8337
|
+
import * as fs13 from "fs";
|
|
8039
8338
|
import * as fsp6 from "fs/promises";
|
|
8040
8339
|
import * as path9 from "path";
|
|
8041
8340
|
var RESTART_BASE_MS2 = 1e3;
|
|
@@ -8355,7 +8654,7 @@ var TransformerManager = class {
|
|
|
8355
8654
|
}
|
|
8356
8655
|
const t = entry.config;
|
|
8357
8656
|
const command = t.command.length > 0 ? t.command : [t.name];
|
|
8358
|
-
const logStream =
|
|
8657
|
+
const logStream = fs13.createWriteStream(paths.transformerLogFile(t.name), {
|
|
8359
8658
|
flags: "a"
|
|
8360
8659
|
});
|
|
8361
8660
|
logStream.write(
|
|
@@ -8408,7 +8707,7 @@ var TransformerManager = class {
|
|
|
8408
8707
|
}
|
|
8409
8708
|
if (typeof child.pid === "number") {
|
|
8410
8709
|
try {
|
|
8411
|
-
|
|
8710
|
+
fs13.writeFileSync(paths.transformerPidFile(t.name), `${child.pid}
|
|
8412
8711
|
`, {
|
|
8413
8712
|
encoding: "utf8",
|
|
8414
8713
|
mode: 384
|
|
@@ -8433,7 +8732,7 @@ var TransformerManager = class {
|
|
|
8433
8732
|
});
|
|
8434
8733
|
child.on("exit", (code, signal) => {
|
|
8435
8734
|
try {
|
|
8436
|
-
|
|
8735
|
+
fs13.unlinkSync(paths.transformerPidFile(t.name));
|
|
8437
8736
|
} catch {
|
|
8438
8737
|
}
|
|
8439
8738
|
logStream.write(
|
|
@@ -8688,9 +8987,8 @@ function startAgentSyncScheduler(opts) {
|
|
|
8688
8987
|
}
|
|
8689
8988
|
|
|
8690
8989
|
// src/core/session-tokens.ts
|
|
8691
|
-
import * as fs13 from "fs/promises";
|
|
8692
8990
|
import * as path11 from "path";
|
|
8693
|
-
import { createHash, randomBytes, timingSafeEqual } from "crypto";
|
|
8991
|
+
import { createHash, randomBytes as randomBytes2, timingSafeEqual } from "crypto";
|
|
8694
8992
|
var TOKEN_PREFIX = "hydra_session_";
|
|
8695
8993
|
var DEFAULT_TTL_SEC = 60 * 60 * 24 * 30;
|
|
8696
8994
|
var ID_LENGTH = 12;
|
|
@@ -8703,7 +9001,7 @@ function sha256Hex(input) {
|
|
|
8703
9001
|
return createHash("sha256").update(input).digest("hex");
|
|
8704
9002
|
}
|
|
8705
9003
|
function randomHex(bytes) {
|
|
8706
|
-
return
|
|
9004
|
+
return randomBytes2(bytes).toString("hex");
|
|
8707
9005
|
}
|
|
8708
9006
|
function generateId() {
|
|
8709
9007
|
return randomHex(ID_LENGTH).slice(0, ID_LENGTH * 2);
|
|
@@ -8723,17 +9021,11 @@ var SessionTokenStore = class _SessionTokenStore {
|
|
|
8723
9021
|
}
|
|
8724
9022
|
static async load() {
|
|
8725
9023
|
let records = [];
|
|
8726
|
-
|
|
8727
|
-
|
|
8728
|
-
|
|
8729
|
-
|
|
8730
|
-
|
|
8731
|
-
}
|
|
8732
|
-
} catch (err) {
|
|
8733
|
-
const e = err;
|
|
8734
|
-
if (e.code !== "ENOENT") {
|
|
8735
|
-
throw err;
|
|
8736
|
-
}
|
|
9024
|
+
const parsed = await readJsonSafe(
|
|
9025
|
+
tokensFilePath()
|
|
9026
|
+
);
|
|
9027
|
+
if (parsed && Array.isArray(parsed.records)) {
|
|
9028
|
+
records = parsed.records.filter(isRecord);
|
|
8737
9029
|
}
|
|
8738
9030
|
const store = new _SessionTokenStore(records);
|
|
8739
9031
|
const removed = store.sweepExpired(/* @__PURE__ */ new Date());
|
|
@@ -8850,14 +9142,11 @@ var SessionTokenStore = class _SessionTokenStore {
|
|
|
8850
9142
|
await this.writeInflight;
|
|
8851
9143
|
}
|
|
8852
9144
|
const records = Array.from(this.records.values());
|
|
8853
|
-
|
|
8854
|
-
|
|
8855
|
-
|
|
8856
|
-
|
|
8857
|
-
|
|
8858
|
-
mode: 384
|
|
8859
|
-
});
|
|
8860
|
-
})();
|
|
9145
|
+
this.writeInflight = writeJsonAtomic(
|
|
9146
|
+
tokensFilePath(),
|
|
9147
|
+
{ records },
|
|
9148
|
+
{ mode: 384 }
|
|
9149
|
+
);
|
|
8861
9150
|
try {
|
|
8862
9151
|
await this.writeInflight;
|
|
8863
9152
|
} finally {
|
|
@@ -9025,89 +9314,6 @@ var AuthRateLimiter = class {
|
|
|
9025
9314
|
// src/daemon/routes/sessions.ts
|
|
9026
9315
|
import * as os3 from "os";
|
|
9027
9316
|
|
|
9028
|
-
// src/core/bundle.ts
|
|
9029
|
-
import { z as z5 } from "zod";
|
|
9030
|
-
var HistoryEntrySchema = z5.object({
|
|
9031
|
-
method: z5.string(),
|
|
9032
|
-
params: z5.unknown(),
|
|
9033
|
-
recordedAt: z5.number()
|
|
9034
|
-
});
|
|
9035
|
-
var BundleSession = z5.object({
|
|
9036
|
-
// The exporter's local id. Regenerated fresh on import (sessionId is
|
|
9037
|
-
// the local namespace; lineageId is what survives across hops).
|
|
9038
|
-
sessionId: z5.string(),
|
|
9039
|
-
// Required on bundles — the export path backfills if the source
|
|
9040
|
-
// record was written before lineageId existed.
|
|
9041
|
-
lineageId: z5.string(),
|
|
9042
|
-
// The exporter's agent-side session id at export time. Carried so
|
|
9043
|
-
// importers can persist it as a breadcrumb (and, eventually, as the
|
|
9044
|
-
// handle a "connect back to origin" feature would need). Omitted on
|
|
9045
|
-
// bundles whose source record never bound to an agent (e.g. a
|
|
9046
|
-
// re-export of an imported, not-yet-attached session).
|
|
9047
|
-
upstreamSessionId: z5.string().optional(),
|
|
9048
|
-
agentId: z5.string(),
|
|
9049
|
-
cwd: z5.string(),
|
|
9050
|
-
title: z5.string().optional(),
|
|
9051
|
-
currentModel: z5.string().optional(),
|
|
9052
|
-
currentMode: z5.string().optional(),
|
|
9053
|
-
currentUsage: PersistedUsage.optional(),
|
|
9054
|
-
agentCommands: z5.array(PersistedAgentCommand).optional(),
|
|
9055
|
-
agentModes: z5.array(PersistedAgentMode).optional(),
|
|
9056
|
-
createdAt: z5.string(),
|
|
9057
|
-
updatedAt: z5.string()
|
|
9058
|
-
});
|
|
9059
|
-
var Bundle = z5.object({
|
|
9060
|
-
version: z5.literal(1),
|
|
9061
|
-
exportedAt: z5.string(),
|
|
9062
|
-
exportedFrom: z5.object({
|
|
9063
|
-
hydraVersion: z5.string(),
|
|
9064
|
-
machine: z5.string(),
|
|
9065
|
-
// Externally-reachable name (and optional ":port") for the exporting
|
|
9066
|
-
// daemon, sourced from config.daemon.publicHost (or daemon.host when
|
|
9067
|
-
// non-loopback). Carried so an importer can construct a hydra:// URL
|
|
9068
|
-
// that dials back to the origin — e.g. over Tailscale. Omitted when
|
|
9069
|
-
// the exporter has no routable address; never falls back to loopback.
|
|
9070
|
-
hydraHost: z5.string().optional()
|
|
9071
|
-
}),
|
|
9072
|
-
session: BundleSession,
|
|
9073
|
-
history: z5.array(HistoryEntrySchema),
|
|
9074
|
-
promptHistory: z5.array(z5.string()).optional()
|
|
9075
|
-
});
|
|
9076
|
-
function encodeBundle(params) {
|
|
9077
|
-
const bundle = {
|
|
9078
|
-
version: 1,
|
|
9079
|
-
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9080
|
-
exportedFrom: {
|
|
9081
|
-
hydraVersion: params.hydraVersion,
|
|
9082
|
-
machine: params.machine,
|
|
9083
|
-
...params.hydraHost !== void 0 && params.hydraHost.length > 0 ? { hydraHost: params.hydraHost } : {}
|
|
9084
|
-
},
|
|
9085
|
-
session: {
|
|
9086
|
-
sessionId: params.record.sessionId,
|
|
9087
|
-
lineageId: params.record.lineageId,
|
|
9088
|
-
...params.record.upstreamSessionId ? { upstreamSessionId: params.record.upstreamSessionId } : {},
|
|
9089
|
-
agentId: params.record.agentId,
|
|
9090
|
-
cwd: params.record.cwd,
|
|
9091
|
-
...params.record.title !== void 0 ? { title: params.record.title } : {},
|
|
9092
|
-
...params.record.currentModel !== void 0 ? { currentModel: params.record.currentModel } : {},
|
|
9093
|
-
...params.record.currentMode !== void 0 ? { currentMode: params.record.currentMode } : {},
|
|
9094
|
-
...params.record.currentUsage !== void 0 ? { currentUsage: params.record.currentUsage } : {},
|
|
9095
|
-
...params.record.agentCommands !== void 0 ? { agentCommands: params.record.agentCommands } : {},
|
|
9096
|
-
...params.record.agentModes !== void 0 ? { agentModes: params.record.agentModes } : {},
|
|
9097
|
-
createdAt: params.record.createdAt,
|
|
9098
|
-
updatedAt: params.record.updatedAt
|
|
9099
|
-
},
|
|
9100
|
-
history: params.history
|
|
9101
|
-
};
|
|
9102
|
-
if (params.promptHistory !== void 0) {
|
|
9103
|
-
bundle.promptHistory = params.promptHistory;
|
|
9104
|
-
}
|
|
9105
|
-
return bundle;
|
|
9106
|
-
}
|
|
9107
|
-
function decodeBundle(raw) {
|
|
9108
|
-
return Bundle.parse(raw);
|
|
9109
|
-
}
|
|
9110
|
-
|
|
9111
9317
|
// src/core/render-update.ts
|
|
9112
9318
|
import stripAnsi from "strip-ansi";
|
|
9113
9319
|
var STRIP_CONTROLS = /[\x00-\x08\x0b-\x1f\x7f]/g;
|
|
@@ -9301,6 +9507,51 @@ function isExitPlanModeTool(name) {
|
|
|
9301
9507
|
const normalised = name.toLowerCase().replace(/[_\s-]/g, "");
|
|
9302
9508
|
return normalised === "exitplanmode";
|
|
9303
9509
|
}
|
|
9510
|
+
function extractEditDiff(u) {
|
|
9511
|
+
const content = u.content;
|
|
9512
|
+
if (Array.isArray(content)) {
|
|
9513
|
+
for (const block of content) {
|
|
9514
|
+
if (!block || typeof block !== "object") {
|
|
9515
|
+
continue;
|
|
9516
|
+
}
|
|
9517
|
+
const b = block;
|
|
9518
|
+
if (b.type !== "diff") {
|
|
9519
|
+
continue;
|
|
9520
|
+
}
|
|
9521
|
+
const oldText = typeof b.oldText === "string" ? b.oldText : void 0;
|
|
9522
|
+
const newText = typeof b.newText === "string" ? b.newText : void 0;
|
|
9523
|
+
if (oldText === void 0 && newText === void 0) {
|
|
9524
|
+
continue;
|
|
9525
|
+
}
|
|
9526
|
+
const path14 = typeof b.path === "string" ? b.path : void 0;
|
|
9527
|
+
return {
|
|
9528
|
+
...path14 !== void 0 ? { path: path14 } : {},
|
|
9529
|
+
oldText: oldText ?? "",
|
|
9530
|
+
newText: newText ?? ""
|
|
9531
|
+
};
|
|
9532
|
+
}
|
|
9533
|
+
}
|
|
9534
|
+
const rawInput = u.rawInput;
|
|
9535
|
+
if (rawInput && typeof rawInput === "object" && !Array.isArray(rawInput)) {
|
|
9536
|
+
const r = rawInput;
|
|
9537
|
+
const filePath = typeof r.file_path === "string" ? r.file_path : typeof r.path === "string" ? r.path : void 0;
|
|
9538
|
+
if (typeof r.old_string === "string" && typeof r.new_string === "string") {
|
|
9539
|
+
return {
|
|
9540
|
+
...filePath !== void 0 ? { path: filePath } : {},
|
|
9541
|
+
oldText: r.old_string,
|
|
9542
|
+
newText: r.new_string
|
|
9543
|
+
};
|
|
9544
|
+
}
|
|
9545
|
+
if (typeof r.content === "string") {
|
|
9546
|
+
return {
|
|
9547
|
+
...filePath !== void 0 ? { path: filePath } : {},
|
|
9548
|
+
oldText: "",
|
|
9549
|
+
newText: r.content
|
|
9550
|
+
};
|
|
9551
|
+
}
|
|
9552
|
+
}
|
|
9553
|
+
return null;
|
|
9554
|
+
}
|
|
9304
9555
|
function readExitPlanMarkdown(u) {
|
|
9305
9556
|
const rawInput = u.rawInput;
|
|
9306
9557
|
if (!rawInput || typeof rawInput !== "object" || Array.isArray(rawInput)) {
|
|
@@ -9340,6 +9591,10 @@ function mapToolCall(u) {
|
|
|
9340
9591
|
if (rawKind !== void 0) {
|
|
9341
9592
|
event.rawKind = rawKind;
|
|
9342
9593
|
}
|
|
9594
|
+
const diff = extractEditDiff(u);
|
|
9595
|
+
if (diff !== null) {
|
|
9596
|
+
event.editDiff = diff;
|
|
9597
|
+
}
|
|
9343
9598
|
return event;
|
|
9344
9599
|
}
|
|
9345
9600
|
function mapToolCallUpdate(u) {
|
|
@@ -9350,7 +9605,8 @@ function mapToolCallUpdate(u) {
|
|
|
9350
9605
|
const rawTitle = readString(u, "title");
|
|
9351
9606
|
const title = rawTitle !== void 0 ? sanitizeSingleLine(rawTitle) : void 0;
|
|
9352
9607
|
const status = readString(u, "status");
|
|
9353
|
-
const
|
|
9608
|
+
const diff = extractEditDiff(u);
|
|
9609
|
+
const meaningful = title !== void 0 || diff !== null || status === "completed" || status === "failed" || status === "rejected" || status === "cancelled";
|
|
9354
9610
|
if (!meaningful) {
|
|
9355
9611
|
return null;
|
|
9356
9612
|
}
|
|
@@ -9373,6 +9629,9 @@ function mapToolCallUpdate(u) {
|
|
|
9373
9629
|
if (status !== void 0) {
|
|
9374
9630
|
event.status = status;
|
|
9375
9631
|
}
|
|
9632
|
+
if (diff !== null) {
|
|
9633
|
+
event.editDiff = diff;
|
|
9634
|
+
}
|
|
9376
9635
|
if (status === "failed") {
|
|
9377
9636
|
const errorText = extractToolFailureText(u);
|
|
9378
9637
|
if (errorText !== null) {
|
|
@@ -10292,6 +10551,48 @@ function registerSessionRoutes(app, manager, defaults) {
|
|
|
10292
10551
|
reply.header("Content-Type", "text/markdown; charset=utf-8");
|
|
10293
10552
|
reply.code(200).send(bundleToMarkdown(bundle));
|
|
10294
10553
|
});
|
|
10554
|
+
app.post("/v1/sessions/:id/fork", async (request, reply) => {
|
|
10555
|
+
const raw = request.params.id;
|
|
10556
|
+
const id = await manager.resolveCanonicalId(raw) ?? raw;
|
|
10557
|
+
const body = request.body ?? {};
|
|
10558
|
+
const opts = {};
|
|
10559
|
+
if (body.forkAt !== void 0) {
|
|
10560
|
+
if (typeof body.forkAt !== "string" || body.forkAt.length === 0) {
|
|
10561
|
+
reply.code(400).send({ error: "forkAt must be a non-empty string" });
|
|
10562
|
+
return;
|
|
10563
|
+
}
|
|
10564
|
+
opts.forkAt = body.forkAt;
|
|
10565
|
+
}
|
|
10566
|
+
if (body.cwd !== void 0) {
|
|
10567
|
+
if (typeof body.cwd !== "string" || body.cwd.length === 0) {
|
|
10568
|
+
reply.code(400).send({ error: "cwd must be a non-empty string" });
|
|
10569
|
+
return;
|
|
10570
|
+
}
|
|
10571
|
+
opts.cwd = expandHome(body.cwd);
|
|
10572
|
+
}
|
|
10573
|
+
if (body.agentId !== void 0) {
|
|
10574
|
+
if (typeof body.agentId !== "string" || body.agentId.length === 0) {
|
|
10575
|
+
reply.code(400).send({ error: "agentId must be a non-empty string" });
|
|
10576
|
+
return;
|
|
10577
|
+
}
|
|
10578
|
+
opts.agentId = body.agentId;
|
|
10579
|
+
}
|
|
10580
|
+
try {
|
|
10581
|
+
const result = await manager.forkSession(id, opts);
|
|
10582
|
+
reply.code(201).send(result);
|
|
10583
|
+
} catch (err) {
|
|
10584
|
+
const e = err;
|
|
10585
|
+
if (e.code === JsonRpcErrorCodes.SessionNotFound) {
|
|
10586
|
+
reply.code(404).send({ error: e.message });
|
|
10587
|
+
return;
|
|
10588
|
+
}
|
|
10589
|
+
if (e.code === JsonRpcErrorCodes.InvalidParams || e.code === JsonRpcErrorCodes.AgentNotInstalled) {
|
|
10590
|
+
reply.code(400).send({ error: e.message });
|
|
10591
|
+
return;
|
|
10592
|
+
}
|
|
10593
|
+
reply.code(500).send({ error: e.message });
|
|
10594
|
+
}
|
|
10595
|
+
});
|
|
10295
10596
|
app.post("/v1/sessions/import", async (request, reply) => {
|
|
10296
10597
|
const body = request.body ?? {};
|
|
10297
10598
|
if (body.bundle === void 0) {
|
|
@@ -10737,7 +11038,7 @@ import { z as z6 } from "zod";
|
|
|
10737
11038
|
// src/core/password.ts
|
|
10738
11039
|
import * as fs14 from "fs/promises";
|
|
10739
11040
|
import * as path12 from "path";
|
|
10740
|
-
import { randomBytes as
|
|
11041
|
+
import { randomBytes as randomBytes3, scrypt, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
10741
11042
|
import { promisify } from "util";
|
|
10742
11043
|
var scryptAsync = promisify(scrypt);
|
|
10743
11044
|
function passwordHashPath() {
|
|
@@ -10959,7 +11260,7 @@ function wsToMessageStream(ws) {
|
|
|
10959
11260
|
// src/daemon/acp-ws.ts
|
|
10960
11261
|
import * as os4 from "os";
|
|
10961
11262
|
import * as path13 from "path";
|
|
10962
|
-
import { randomBytes as
|
|
11263
|
+
import { randomBytes as randomBytes4 } from "crypto";
|
|
10963
11264
|
function registerAcpWsEndpoint(app, deps) {
|
|
10964
11265
|
app.get("/acp", { websocket: true }, async (socket, request) => {
|
|
10965
11266
|
const token = tokenFromUpgradeRequest({
|
|
@@ -11156,6 +11457,23 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
11156
11457
|
});
|
|
11157
11458
|
return { childSessionId: child.sessionId };
|
|
11158
11459
|
});
|
|
11460
|
+
connection.onRequest("hydra-acp/fork_session", async (raw) => {
|
|
11461
|
+
const params = raw ?? {};
|
|
11462
|
+
if (typeof params.sessionId !== "string") {
|
|
11463
|
+
throw Object.assign(
|
|
11464
|
+
new Error("fork_session requires sessionId"),
|
|
11465
|
+
{ code: JsonRpcErrorCodes.InvalidParams }
|
|
11466
|
+
);
|
|
11467
|
+
}
|
|
11468
|
+
const forkAt = typeof params.forkAt === "string" ? params.forkAt : void 0;
|
|
11469
|
+
const cwd = typeof params.cwd === "string" ? params.cwd : void 0;
|
|
11470
|
+
const agentId = typeof params.agentId === "string" ? params.agentId : void 0;
|
|
11471
|
+
return await deps.manager.forkSession(params.sessionId, {
|
|
11472
|
+
...forkAt !== void 0 ? { forkAt } : {},
|
|
11473
|
+
...cwd !== void 0 ? { cwd } : {},
|
|
11474
|
+
...agentId !== void 0 ? { agentId } : {}
|
|
11475
|
+
});
|
|
11476
|
+
});
|
|
11159
11477
|
connection.onRequest("hydra-acp/await_child", async (raw) => {
|
|
11160
11478
|
const params = raw ?? {};
|
|
11161
11479
|
const childSessionId = typeof params.childSessionId === "string" ? params.childSessionId : void 0;
|
|
@@ -11230,7 +11548,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
11230
11548
|
let stdinReservation;
|
|
11231
11549
|
let augmentedMcpServers = params.mcpServers;
|
|
11232
11550
|
if (hydraMeta.mcpStdin === true && deps.mcpTokenRegistry !== void 0 && deps.getDaemonOrigin !== void 0) {
|
|
11233
|
-
stdinToken =
|
|
11551
|
+
stdinToken = randomBytes4(32).toString("hex");
|
|
11234
11552
|
stdinReservation = deps.mcpTokenRegistry.reserve(stdinToken);
|
|
11235
11553
|
const url = `${deps.getDaemonOrigin()}/mcp/hydra-acp-stdin`;
|
|
11236
11554
|
const descriptor = {
|
|
@@ -11248,7 +11566,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
11248
11566
|
if (deps.extensionMcp !== void 0 && deps.mcpTokenRegistry !== void 0 && deps.getDaemonOrigin !== void 0) {
|
|
11249
11567
|
const extNames = deps.extensionMcp.list();
|
|
11250
11568
|
if (extNames.length > 0) {
|
|
11251
|
-
extMcpToken =
|
|
11569
|
+
extMcpToken = randomBytes4(32).toString("hex");
|
|
11252
11570
|
extMcpReservation = deps.mcpTokenRegistry.reserve(extMcpToken);
|
|
11253
11571
|
const origin = deps.getDaemonOrigin();
|
|
11254
11572
|
const descriptors = extNames.map((name) => ({
|
|
@@ -12807,6 +13125,7 @@ async function startDaemon(config, serviceToken) {
|
|
|
12807
13125
|
await transformers.stop();
|
|
12808
13126
|
await manager.closeAll();
|
|
12809
13127
|
await manager.flushMetaWrites();
|
|
13128
|
+
await manager.flushHistoryWrites();
|
|
12810
13129
|
setBinaryInstallLogger(null);
|
|
12811
13130
|
setNpmInstallLogger(null);
|
|
12812
13131
|
setAgentPruneLogger(null);
|