@hydra-acp/cli 0.1.24 → 0.1.26
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 +9 -9
- package/dist/cli.js +1882 -171
- package/dist/index.d.ts +135 -0
- package/dist/index.js +1065 -79
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -186,7 +186,15 @@ var TuiConfig = z.object({
|
|
|
186
186
|
// on Windows Terminal, dock badge on KDE/Konsole, etc.) while a turn is
|
|
187
187
|
// running. Set false if your terminal renders this obnoxiously or you
|
|
188
188
|
// just don't want it.
|
|
189
|
-
progressIndicator: z.boolean().default(true)
|
|
189
|
+
progressIndicator: z.boolean().default(true),
|
|
190
|
+
// What the unmodified Enter key does in the prompt composer.
|
|
191
|
+
// "enqueue" (default) — Enter enqueues the prompt (sends immediately
|
|
192
|
+
// when idle, queues behind an in-flight turn); Shift+Enter amends
|
|
193
|
+
// the in-flight turn.
|
|
194
|
+
// "amend" — flips the two: Enter amends the in-flight turn,
|
|
195
|
+
// Shift+Enter enqueues. With no turn in flight either key just
|
|
196
|
+
// enqueues, since there's nothing to amend.
|
|
197
|
+
defaultEnterAction: z.enum(["enqueue", "amend"]).default("enqueue")
|
|
190
198
|
});
|
|
191
199
|
var ExtensionName = z.string().min(1).regex(/^[A-Za-z0-9._-]+$/, "extension name must be filename-safe");
|
|
192
200
|
var ExtensionBody = z.object({
|
|
@@ -230,7 +238,8 @@ var HydraConfig = z.object({
|
|
|
230
238
|
mouse: true,
|
|
231
239
|
logMaxBytes: 5 * 1024 * 1024,
|
|
232
240
|
cwdColumnMaxWidth: 24,
|
|
233
|
-
progressIndicator: true
|
|
241
|
+
progressIndicator: true,
|
|
242
|
+
defaultEnterAction: "enqueue"
|
|
234
243
|
})
|
|
235
244
|
});
|
|
236
245
|
function extensionList(config) {
|
|
@@ -371,8 +380,10 @@ async function ensureBinary(args) {
|
|
|
371
380
|
}
|
|
372
381
|
await downloadAndExtract({
|
|
373
382
|
agentId: args.agentId,
|
|
383
|
+
version: args.version,
|
|
374
384
|
archiveUrl: args.target.archive,
|
|
375
|
-
installDir
|
|
385
|
+
installDir,
|
|
386
|
+
onProgress: args.onProgress
|
|
376
387
|
});
|
|
377
388
|
if (!await fileExists(cmdPath)) {
|
|
378
389
|
throw new Error(
|
|
@@ -392,9 +403,16 @@ async function downloadAndExtract(args) {
|
|
|
392
403
|
const archivePath = await downloadTo({
|
|
393
404
|
url: args.archiveUrl,
|
|
394
405
|
dir: tempDir,
|
|
395
|
-
agentId: args.agentId
|
|
406
|
+
agentId: args.agentId,
|
|
407
|
+
version: args.version,
|
|
408
|
+
onProgress: args.onProgress
|
|
396
409
|
});
|
|
397
410
|
logSink(`hydra-acp: extracting ${args.agentId}`);
|
|
411
|
+
safeEmit(args.onProgress, {
|
|
412
|
+
phase: "extract",
|
|
413
|
+
agentId: args.agentId,
|
|
414
|
+
version: args.version
|
|
415
|
+
});
|
|
398
416
|
await extract(archivePath, tempDir);
|
|
399
417
|
await fsp.unlink(archivePath).catch(() => void 0);
|
|
400
418
|
try {
|
|
@@ -405,16 +423,35 @@ async function downloadAndExtract(args) {
|
|
|
405
423
|
await fsp.rm(tempDir, { recursive: true, force: true }).catch(
|
|
406
424
|
() => void 0
|
|
407
425
|
);
|
|
426
|
+
safeEmit(args.onProgress, {
|
|
427
|
+
phase: "installed",
|
|
428
|
+
agentId: args.agentId,
|
|
429
|
+
version: args.version
|
|
430
|
+
});
|
|
408
431
|
return;
|
|
409
432
|
}
|
|
410
433
|
throw err;
|
|
411
434
|
}
|
|
412
435
|
logSink(`hydra-acp: installed ${args.agentId} to ${args.installDir}`);
|
|
436
|
+
safeEmit(args.onProgress, {
|
|
437
|
+
phase: "installed",
|
|
438
|
+
agentId: args.agentId,
|
|
439
|
+
version: args.version
|
|
440
|
+
});
|
|
413
441
|
} catch (err) {
|
|
414
442
|
await fsp.rm(tempDir, { recursive: true, force: true }).catch(() => void 0);
|
|
415
443
|
throw err;
|
|
416
444
|
}
|
|
417
445
|
}
|
|
446
|
+
function safeEmit(cb, event) {
|
|
447
|
+
if (!cb) {
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
try {
|
|
451
|
+
cb(event);
|
|
452
|
+
} catch {
|
|
453
|
+
}
|
|
454
|
+
}
|
|
418
455
|
async function downloadTo(args) {
|
|
419
456
|
const filename = inferArchiveName(args.url);
|
|
420
457
|
const dest = path2.join(args.dir, filename);
|
|
@@ -427,17 +464,34 @@ async function downloadTo(args) {
|
|
|
427
464
|
const total = Number(response.headers.get("content-length") ?? "0");
|
|
428
465
|
const out = fs3.createWriteStream(dest);
|
|
429
466
|
const nodeStream = Readable.fromWeb(response.body);
|
|
467
|
+
safeEmit(args.onProgress, {
|
|
468
|
+
phase: "download_start",
|
|
469
|
+
agentId: args.agentId,
|
|
470
|
+
version: args.version,
|
|
471
|
+
totalBytes: total
|
|
472
|
+
});
|
|
430
473
|
let received = 0;
|
|
431
|
-
let
|
|
432
|
-
|
|
474
|
+
let lastLogEmit = Date.now();
|
|
475
|
+
let lastCbEmit = 0;
|
|
476
|
+
const LOG_INTERVAL_MS = 2e3;
|
|
477
|
+
const CB_INTERVAL_MS = 150;
|
|
433
478
|
nodeStream.on("data", (chunk) => {
|
|
434
479
|
received += chunk.length;
|
|
435
480
|
const now = Date.now();
|
|
436
|
-
if (now -
|
|
437
|
-
|
|
481
|
+
if (now - lastCbEmit >= CB_INTERVAL_MS) {
|
|
482
|
+
lastCbEmit = now;
|
|
483
|
+
safeEmit(args.onProgress, {
|
|
484
|
+
phase: "download_progress",
|
|
485
|
+
agentId: args.agentId,
|
|
486
|
+
version: args.version,
|
|
487
|
+
receivedBytes: received,
|
|
488
|
+
totalBytes: total
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
if (now - lastLogEmit >= LOG_INTERVAL_MS) {
|
|
492
|
+
lastLogEmit = now;
|
|
493
|
+
logSink(formatProgress(args.agentId, received, total));
|
|
438
494
|
}
|
|
439
|
-
lastEmit = now;
|
|
440
|
-
logSink(formatProgress(args.agentId, received, total));
|
|
441
495
|
});
|
|
442
496
|
await new Promise((resolve3, reject) => {
|
|
443
497
|
nodeStream.on("error", reject);
|
|
@@ -452,6 +506,13 @@ async function downloadTo(args) {
|
|
|
452
506
|
/* done */
|
|
453
507
|
true
|
|
454
508
|
));
|
|
509
|
+
safeEmit(args.onProgress, {
|
|
510
|
+
phase: "download_done",
|
|
511
|
+
agentId: args.agentId,
|
|
512
|
+
version: args.version,
|
|
513
|
+
receivedBytes: received,
|
|
514
|
+
totalBytes: total
|
|
515
|
+
});
|
|
455
516
|
return dest;
|
|
456
517
|
}
|
|
457
518
|
function formatProgress(agentId, received, total, done = false) {
|
|
@@ -550,9 +611,11 @@ async function ensureNpmPackage(args) {
|
|
|
550
611
|
}
|
|
551
612
|
await installInto({
|
|
552
613
|
agentId: args.agentId,
|
|
614
|
+
version: args.version,
|
|
553
615
|
packageSpec: args.packageSpec,
|
|
554
616
|
installDir,
|
|
555
|
-
registry: args.registry
|
|
617
|
+
registry: args.registry,
|
|
618
|
+
onProgress: args.onProgress
|
|
556
619
|
});
|
|
557
620
|
if (!await fileExists2(binPath)) {
|
|
558
621
|
throw new Error(
|
|
@@ -568,6 +631,12 @@ async function installInto(args) {
|
|
|
568
631
|
logSink2(
|
|
569
632
|
`hydra-acp: installing ${args.packageSpec} for ${args.agentId} into ${tempDir}`
|
|
570
633
|
);
|
|
634
|
+
safeEmit2(args.onProgress, {
|
|
635
|
+
phase: "install_start",
|
|
636
|
+
agentId: args.agentId,
|
|
637
|
+
version: args.version,
|
|
638
|
+
packageSpec: args.packageSpec
|
|
639
|
+
});
|
|
571
640
|
await runNpmInstall({
|
|
572
641
|
packageSpec: args.packageSpec,
|
|
573
642
|
cwd: tempDir,
|
|
@@ -581,11 +650,21 @@ async function installInto(args) {
|
|
|
581
650
|
await fsp2.rm(tempDir, { recursive: true, force: true }).catch(
|
|
582
651
|
() => void 0
|
|
583
652
|
);
|
|
653
|
+
safeEmit2(args.onProgress, {
|
|
654
|
+
phase: "installed",
|
|
655
|
+
agentId: args.agentId,
|
|
656
|
+
version: args.version
|
|
657
|
+
});
|
|
584
658
|
return;
|
|
585
659
|
}
|
|
586
660
|
throw err;
|
|
587
661
|
}
|
|
588
662
|
logSink2(`hydra-acp: installed ${args.agentId} to ${args.installDir}`);
|
|
663
|
+
safeEmit2(args.onProgress, {
|
|
664
|
+
phase: "installed",
|
|
665
|
+
agentId: args.agentId,
|
|
666
|
+
version: args.version
|
|
667
|
+
});
|
|
589
668
|
} catch (err) {
|
|
590
669
|
await fsp2.rm(tempDir, { recursive: true, force: true }).catch(
|
|
591
670
|
() => void 0
|
|
@@ -593,44 +672,87 @@ async function installInto(args) {
|
|
|
593
672
|
throw err;
|
|
594
673
|
}
|
|
595
674
|
}
|
|
675
|
+
function safeEmit2(cb, event) {
|
|
676
|
+
if (!cb) {
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
try {
|
|
680
|
+
cb(event);
|
|
681
|
+
} catch {
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
var ETXTBSY_RETRIES = 5;
|
|
685
|
+
var ETXTBSY_BACKOFF_MS = 25;
|
|
596
686
|
function runNpmInstall(args) {
|
|
597
|
-
return
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
child.on("exit", (code, signal) => {
|
|
620
|
-
if (code === 0) {
|
|
621
|
-
resolve3();
|
|
687
|
+
return runNpmInstallOnce(args, 0);
|
|
688
|
+
}
|
|
689
|
+
async function runNpmInstallOnce(args, attempt) {
|
|
690
|
+
try {
|
|
691
|
+
await new Promise((resolve3, reject) => {
|
|
692
|
+
const registryArgs = args.registry ? ["--registry", args.registry] : [];
|
|
693
|
+
let child;
|
|
694
|
+
try {
|
|
695
|
+
child = spawn2(
|
|
696
|
+
"npm",
|
|
697
|
+
[
|
|
698
|
+
"install",
|
|
699
|
+
"--no-audit",
|
|
700
|
+
"--no-fund",
|
|
701
|
+
"--silent",
|
|
702
|
+
...registryArgs,
|
|
703
|
+
args.packageSpec
|
|
704
|
+
],
|
|
705
|
+
{ cwd: args.cwd, stdio: ["ignore", "pipe", "pipe"] }
|
|
706
|
+
);
|
|
707
|
+
} catch (err) {
|
|
708
|
+
reject(err);
|
|
622
709
|
return;
|
|
623
710
|
}
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
711
|
+
let stderrTail = "";
|
|
712
|
+
child.stdout?.on("data", (chunk) => {
|
|
713
|
+
void chunk;
|
|
714
|
+
});
|
|
715
|
+
child.stderr?.setEncoding("utf8");
|
|
716
|
+
child.stderr?.on("data", (chunk) => {
|
|
717
|
+
stderrTail = (stderrTail + chunk).slice(-4096);
|
|
718
|
+
});
|
|
719
|
+
child.on("error", (err) => {
|
|
720
|
+
const e = err;
|
|
721
|
+
if (e.code === "ENOENT") {
|
|
722
|
+
reject(
|
|
723
|
+
new Error(
|
|
724
|
+
`npm not found on PATH (install Node.js / npm, or use a binary-distributed agent)`
|
|
725
|
+
)
|
|
726
|
+
);
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
reject(err);
|
|
730
|
+
});
|
|
731
|
+
child.on("exit", (code, signal) => {
|
|
732
|
+
if (code === 0) {
|
|
733
|
+
resolve3();
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
const reason = code !== null ? `exit code ${code}` : `signal ${signal ?? "unknown"}`;
|
|
737
|
+
const tail = stderrTail.trim();
|
|
738
|
+
reject(
|
|
739
|
+
new Error(
|
|
740
|
+
tail ? `npm install ${args.packageSpec} failed (${reason})
|
|
629
741
|
stderr: ${tail}` : `npm install ${args.packageSpec} failed (${reason})`
|
|
630
|
-
|
|
631
|
-
|
|
742
|
+
)
|
|
743
|
+
);
|
|
744
|
+
});
|
|
632
745
|
});
|
|
633
|
-
})
|
|
746
|
+
} catch (err) {
|
|
747
|
+
const code = err.code;
|
|
748
|
+
if (code === "ETXTBSY" && attempt < ETXTBSY_RETRIES) {
|
|
749
|
+
await new Promise(
|
|
750
|
+
(r) => setTimeout(r, ETXTBSY_BACKOFF_MS * (attempt + 1))
|
|
751
|
+
);
|
|
752
|
+
return runNpmInstallOnce(args, attempt + 1);
|
|
753
|
+
}
|
|
754
|
+
throw err;
|
|
755
|
+
}
|
|
634
756
|
}
|
|
635
757
|
async function fileExists2(p) {
|
|
636
758
|
try {
|
|
@@ -828,12 +950,14 @@ async function planSpawn(agent, callerArgs = [], options = {}) {
|
|
|
828
950
|
};
|
|
829
951
|
}
|
|
830
952
|
const bin = npx.bin ?? npxPackageBasename(agent) ?? npx.package;
|
|
953
|
+
const npmCb = options.onInstallProgress;
|
|
831
954
|
const binPath = await ensureNpmPackage({
|
|
832
955
|
agentId: agent.id,
|
|
833
956
|
version,
|
|
834
957
|
packageSpec: npx.package,
|
|
835
958
|
bin,
|
|
836
|
-
registry: options.npmRegistry
|
|
959
|
+
registry: options.npmRegistry,
|
|
960
|
+
onProgress: npmCb ? (e) => npmCb({ source: "npm", ...e }) : void 0
|
|
837
961
|
});
|
|
838
962
|
return {
|
|
839
963
|
command: binPath,
|
|
@@ -849,10 +973,12 @@ async function planSpawn(agent, callerArgs = [], options = {}) {
|
|
|
849
973
|
`Agent ${agent.id} has no binary distribution for ${currentPlatformKey() ?? "this platform"}.`
|
|
850
974
|
);
|
|
851
975
|
}
|
|
976
|
+
const binCb = options.onInstallProgress;
|
|
852
977
|
const cmdPath = await ensureBinary({
|
|
853
978
|
agentId: agent.id,
|
|
854
979
|
version,
|
|
855
|
-
target
|
|
980
|
+
target,
|
|
981
|
+
onProgress: binCb ? (e) => binCb({ source: "binary", ...e }) : void 0
|
|
856
982
|
});
|
|
857
983
|
const tail = callerArgs.length > 0 ? callerArgs : target.args ?? [];
|
|
858
984
|
return {
|
|
@@ -1024,6 +1150,18 @@ function extractHydraMeta(meta) {
|
|
|
1024
1150
|
if (typeof obj.promptQueueing === "boolean") {
|
|
1025
1151
|
out.promptQueueing = obj.promptQueueing;
|
|
1026
1152
|
}
|
|
1153
|
+
if (typeof obj.promptCancelling === "boolean") {
|
|
1154
|
+
out.promptCancelling = obj.promptCancelling;
|
|
1155
|
+
}
|
|
1156
|
+
if (typeof obj.promptUpdating === "boolean") {
|
|
1157
|
+
out.promptUpdating = obj.promptUpdating;
|
|
1158
|
+
}
|
|
1159
|
+
if (typeof obj.promptAmending === "boolean") {
|
|
1160
|
+
out.promptAmending = obj.promptAmending;
|
|
1161
|
+
}
|
|
1162
|
+
if (typeof obj.promptPipelining === "boolean") {
|
|
1163
|
+
out.promptPipelining = obj.promptPipelining;
|
|
1164
|
+
}
|
|
1027
1165
|
if (Array.isArray(obj.queue)) {
|
|
1028
1166
|
const entries = [];
|
|
1029
1167
|
for (const raw of obj.queue) {
|
|
@@ -1073,6 +1211,29 @@ function extractHydraMeta(meta) {
|
|
|
1073
1211
|
out.availableModes = modes;
|
|
1074
1212
|
}
|
|
1075
1213
|
}
|
|
1214
|
+
if (Array.isArray(obj.availableModels)) {
|
|
1215
|
+
const models = [];
|
|
1216
|
+
for (const raw of obj.availableModels) {
|
|
1217
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
1218
|
+
continue;
|
|
1219
|
+
}
|
|
1220
|
+
const m = raw;
|
|
1221
|
+
if (typeof m.modelId !== "string") {
|
|
1222
|
+
continue;
|
|
1223
|
+
}
|
|
1224
|
+
const model = { modelId: m.modelId };
|
|
1225
|
+
if (typeof m.name === "string") {
|
|
1226
|
+
model.name = m.name;
|
|
1227
|
+
}
|
|
1228
|
+
if (typeof m.description === "string") {
|
|
1229
|
+
model.description = m.description;
|
|
1230
|
+
}
|
|
1231
|
+
models.push(model);
|
|
1232
|
+
}
|
|
1233
|
+
if (models.length > 0) {
|
|
1234
|
+
out.availableModels = models;
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1076
1237
|
return out;
|
|
1077
1238
|
}
|
|
1078
1239
|
function mergeMeta(passthrough, ours) {
|
|
@@ -1168,6 +1329,51 @@ var UpdatePromptResult = z3.object({
|
|
|
1168
1329
|
updated: z3.boolean(),
|
|
1169
1330
|
reason: z3.enum(["ok", "not_found", "already_running"])
|
|
1170
1331
|
});
|
|
1332
|
+
var AmendPromptParams = z3.object({
|
|
1333
|
+
sessionId: z3.string(),
|
|
1334
|
+
targetMessageId: z3.string(),
|
|
1335
|
+
prompt: z3.array(z3.unknown()),
|
|
1336
|
+
replaceQueue: z3.boolean().optional(),
|
|
1337
|
+
onTargetCompleted: z3.enum(["reject", "send_anyway"]).optional()
|
|
1338
|
+
});
|
|
1339
|
+
var AmendPromptResult = z3.object({
|
|
1340
|
+
amended: z3.boolean(),
|
|
1341
|
+
reason: z3.enum([
|
|
1342
|
+
"ok",
|
|
1343
|
+
"target_completed",
|
|
1344
|
+
"target_cancelled",
|
|
1345
|
+
"target_not_found"
|
|
1346
|
+
]),
|
|
1347
|
+
// Present when a prompt was sent or replaced: the amendment's id on
|
|
1348
|
+
// success, or the regular follow-up's id when onTargetCompleted is
|
|
1349
|
+
// "send_anyway" and the daemon forwarded the prompt anyway.
|
|
1350
|
+
messageId: z3.string().optional()
|
|
1351
|
+
});
|
|
1352
|
+
var PromptAmendedParams = z3.object({
|
|
1353
|
+
sessionId: z3.string(),
|
|
1354
|
+
cancelledMessageId: z3.string(),
|
|
1355
|
+
newMessageId: z3.string(),
|
|
1356
|
+
prompt: z3.array(z3.unknown()),
|
|
1357
|
+
originator: PromptOriginatorSchema,
|
|
1358
|
+
amendedAt: z3.number()
|
|
1359
|
+
});
|
|
1360
|
+
var AgentInstallProgressParams = z3.object({
|
|
1361
|
+
agentId: z3.string(),
|
|
1362
|
+
version: z3.string(),
|
|
1363
|
+
source: z3.enum(["binary", "npm"]),
|
|
1364
|
+
phase: z3.enum([
|
|
1365
|
+
"download_start",
|
|
1366
|
+
"download_progress",
|
|
1367
|
+
"download_done",
|
|
1368
|
+
"extract",
|
|
1369
|
+
"install_start",
|
|
1370
|
+
"installed"
|
|
1371
|
+
]),
|
|
1372
|
+
receivedBytes: z3.number().optional(),
|
|
1373
|
+
totalBytes: z3.number().optional(),
|
|
1374
|
+
packageSpec: z3.string().optional()
|
|
1375
|
+
});
|
|
1376
|
+
var AGENT_INSTALL_PROGRESS_METHOD = "hydra-acp/agent_install_progress";
|
|
1171
1377
|
var ProxyInitializeParams = z3.object({
|
|
1172
1378
|
protocolVersion: z3.number().optional(),
|
|
1173
1379
|
proxyInfo: z3.object({
|
|
@@ -1646,6 +1852,7 @@ function stripHydraSessionPrefix(id) {
|
|
|
1646
1852
|
return id.startsWith(HYDRA_SESSION_PREFIX) ? id.slice(HYDRA_SESSION_PREFIX.length) : id;
|
|
1647
1853
|
}
|
|
1648
1854
|
var DEFAULT_HISTORY_MAX_ENTRIES = 1e3;
|
|
1855
|
+
var RECENTLY_TERMINAL_LIMIT = 64;
|
|
1649
1856
|
var Session = class {
|
|
1650
1857
|
sessionId;
|
|
1651
1858
|
cwd;
|
|
@@ -1737,14 +1944,36 @@ var Session = class {
|
|
|
1737
1944
|
// Last available_modes_update we observed from the agent. Same
|
|
1738
1945
|
// pattern as commands: cache, persist, broadcast on change.
|
|
1739
1946
|
agentAdvertisedModes = [];
|
|
1947
|
+
// Last availableModels payload we observed (from current_model_update,
|
|
1948
|
+
// a session/new / session/load response, or — for opencode — a
|
|
1949
|
+
// config_option_update where configOptions[i].id === "model").
|
|
1950
|
+
// Cached so a mid-session attach can synthesize a model picker
|
|
1951
|
+
// snapshot, and so session/set_model can validate the requested id
|
|
1952
|
+
// against what the agent claims to support.
|
|
1953
|
+
agentAdvertisedModels = [];
|
|
1740
1954
|
// Persist hooks for snapshot-shaped state. SessionManager hooks these
|
|
1741
1955
|
// to mirror changes into meta.json so cold-resurrect attaches can
|
|
1742
1956
|
// surface the latest snapshot via the attach response _meta.
|
|
1743
1957
|
agentCommandsHandlers = [];
|
|
1744
1958
|
agentModesHandlers = [];
|
|
1959
|
+
agentModelsHandlers = [];
|
|
1745
1960
|
modelHandlers = [];
|
|
1746
1961
|
modeHandlers = [];
|
|
1747
1962
|
usageHandlers = [];
|
|
1963
|
+
// Set by amendPrompt at the start of a cancel-and-resubmit dance.
|
|
1964
|
+
// broadcastTurnComplete reads it to attach the _meta.amended marker
|
|
1965
|
+
// to the cancelled turn's turn_complete notification, and to fire the
|
|
1966
|
+
// dedicated prompt_amended notification. Cleared when the cancelled
|
|
1967
|
+
// turn's task completes (runQueueEntry) OR if the amendment is
|
|
1968
|
+
// cancelled mid-window via cancel_prompt(M2) before drainQueue picks
|
|
1969
|
+
// it up.
|
|
1970
|
+
amendInProgress;
|
|
1971
|
+
// LRU of recently-terminal messageIds → stopReason. Used by
|
|
1972
|
+
// amendPrompt to resolve targets that completed/cancelled before
|
|
1973
|
+
// the amend arrived. Capped at RECENTLY_TERMINAL_LIMIT entries;
|
|
1974
|
+
// older entries fall out and resolve to target_not_found, which is
|
|
1975
|
+
// the correct behavior.
|
|
1976
|
+
recentlyTerminal = /* @__PURE__ */ new Map();
|
|
1748
1977
|
constructor(init) {
|
|
1749
1978
|
this.sessionId = init.sessionId ?? `${HYDRA_SESSION_PREFIX}${generateHydraId()}`;
|
|
1750
1979
|
this.cwd = init.cwd;
|
|
@@ -1763,6 +1992,9 @@ var Session = class {
|
|
|
1763
1992
|
if (init.agentModes && init.agentModes.length > 0) {
|
|
1764
1993
|
this.agentAdvertisedModes = [...init.agentModes];
|
|
1765
1994
|
}
|
|
1995
|
+
if (init.agentModels && init.agentModels.length > 0) {
|
|
1996
|
+
this.agentAdvertisedModels = [...init.agentModels];
|
|
1997
|
+
}
|
|
1766
1998
|
this.idleTimeoutMs = init.idleTimeoutMs ?? 0;
|
|
1767
1999
|
this.spawnReplacementAgent = init.spawnReplacementAgent;
|
|
1768
2000
|
this.logger = init.logger;
|
|
@@ -1800,6 +2032,23 @@ var Session = class {
|
|
|
1800
2032
|
}
|
|
1801
2033
|
});
|
|
1802
2034
|
}
|
|
2035
|
+
// Re-broadcast our cached availableModels via current_model_update.
|
|
2036
|
+
// Spec shape: { currentModel, availableModels } — we only include the
|
|
2037
|
+
// currentModel field when we know it, so this broadcast can also fire
|
|
2038
|
+
// model-list updates standalone before any current model is set.
|
|
2039
|
+
broadcastAvailableModels() {
|
|
2040
|
+
const update = {
|
|
2041
|
+
sessionUpdate: "current_model_update",
|
|
2042
|
+
availableModels: [...this.agentAdvertisedModels]
|
|
2043
|
+
};
|
|
2044
|
+
if (this.currentModel !== void 0 && this.currentModel.length > 0) {
|
|
2045
|
+
update.currentModel = this.currentModel;
|
|
2046
|
+
}
|
|
2047
|
+
this.recordAndBroadcast("session/update", {
|
|
2048
|
+
sessionId: this.upstreamSessionId,
|
|
2049
|
+
update
|
|
2050
|
+
});
|
|
2051
|
+
}
|
|
1803
2052
|
// Register session/update, session/request_permission, and onExit
|
|
1804
2053
|
// handlers on an agent connection. Re-run on every /hydra agent so
|
|
1805
2054
|
// the new agent is plumbed identically. The exit handler's identity
|
|
@@ -1830,6 +2079,10 @@ var Session = class {
|
|
|
1830
2079
|
this.recordAndBroadcast("session/update", params);
|
|
1831
2080
|
return;
|
|
1832
2081
|
}
|
|
2082
|
+
if (this.maybeApplyAgentConfigOption(params)) {
|
|
2083
|
+
this.recordAndBroadcast("session/update", params);
|
|
2084
|
+
return;
|
|
2085
|
+
}
|
|
1833
2086
|
if (this.maybeApplyAgentUsage(params)) {
|
|
1834
2087
|
this.recordAndBroadcast("session/update", params);
|
|
1835
2088
|
return;
|
|
@@ -1978,16 +2231,19 @@ var Session = class {
|
|
|
1978
2231
|
recordedAt
|
|
1979
2232
|
});
|
|
1980
2233
|
}
|
|
1981
|
-
if (this.currentModel !== void 0 && this.currentModel.length > 0) {
|
|
2234
|
+
if (this.currentModel !== void 0 && this.currentModel.length > 0 || this.agentAdvertisedModels.length > 0) {
|
|
2235
|
+
const update = {
|
|
2236
|
+
sessionUpdate: "current_model_update"
|
|
2237
|
+
};
|
|
2238
|
+
if (this.currentModel !== void 0 && this.currentModel.length > 0) {
|
|
2239
|
+
update.currentModel = this.currentModel;
|
|
2240
|
+
}
|
|
2241
|
+
if (this.agentAdvertisedModels.length > 0) {
|
|
2242
|
+
update.availableModels = [...this.agentAdvertisedModels];
|
|
2243
|
+
}
|
|
1982
2244
|
out.push({
|
|
1983
2245
|
method: "session/update",
|
|
1984
|
-
params: {
|
|
1985
|
-
sessionId,
|
|
1986
|
-
update: {
|
|
1987
|
-
sessionUpdate: "current_model_update",
|
|
1988
|
-
currentModel: this.currentModel
|
|
1989
|
-
}
|
|
1990
|
-
},
|
|
2246
|
+
params: { sessionId, update },
|
|
1991
2247
|
recordedAt
|
|
1992
2248
|
});
|
|
1993
2249
|
}
|
|
@@ -2174,7 +2430,7 @@ var Session = class {
|
|
|
2174
2430
|
);
|
|
2175
2431
|
}
|
|
2176
2432
|
}
|
|
2177
|
-
broadcastTurnComplete(originatorClientId, response) {
|
|
2433
|
+
broadcastTurnComplete(originatorClientId, response, promptMessageId, wasAmend) {
|
|
2178
2434
|
const stopReason = response && typeof response === "object" && "stopReason" in response && typeof response.stopReason === "string" ? response.stopReason : void 0;
|
|
2179
2435
|
const update = {
|
|
2180
2436
|
sessionUpdate: "turn_complete",
|
|
@@ -2183,15 +2439,83 @@ var Session = class {
|
|
|
2183
2439
|
if (stopReason !== void 0) {
|
|
2184
2440
|
update.stopReason = stopReason;
|
|
2185
2441
|
}
|
|
2442
|
+
const amend = this.amendInProgress;
|
|
2443
|
+
if (amend && promptMessageId !== void 0 && amend.cancelledMessageId === promptMessageId) {
|
|
2444
|
+
update._meta = {
|
|
2445
|
+
"hydra-acp": {
|
|
2446
|
+
amended: {
|
|
2447
|
+
cancelledMessageId: amend.cancelledMessageId,
|
|
2448
|
+
newMessageId: amend.newMessageId
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
};
|
|
2452
|
+
}
|
|
2186
2453
|
this.promptStartedAt = void 0;
|
|
2454
|
+
if (promptMessageId !== void 0 && stopReason !== void 0) {
|
|
2455
|
+
this.recordTerminal(promptMessageId, stopReason);
|
|
2456
|
+
}
|
|
2187
2457
|
this.recordAndBroadcast(
|
|
2188
2458
|
"session/update",
|
|
2189
2459
|
{
|
|
2190
2460
|
sessionId: this.sessionId,
|
|
2191
2461
|
update
|
|
2192
2462
|
},
|
|
2193
|
-
originatorClientId
|
|
2463
|
+
wasAmend ? void 0 : originatorClientId
|
|
2194
2464
|
);
|
|
2465
|
+
if (amend && promptMessageId !== void 0 && amend.cancelledMessageId === promptMessageId) {
|
|
2466
|
+
this.broadcastPromptAmended(amend);
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
// Record that a prompt's turn has ended, with its terminal stopReason.
|
|
2470
|
+
// Used by amendPrompt to resolve targetMessageIds that completed/cancelled
|
|
2471
|
+
// before the amend arrived. LRU-trimmed at RECENTLY_TERMINAL_LIMIT.
|
|
2472
|
+
recordTerminal(messageId, stopReason) {
|
|
2473
|
+
this.recentlyTerminal.set(messageId, {
|
|
2474
|
+
stopReason,
|
|
2475
|
+
terminatedAt: Date.now()
|
|
2476
|
+
});
|
|
2477
|
+
while (this.recentlyTerminal.size > RECENTLY_TERMINAL_LIMIT) {
|
|
2478
|
+
const oldest = this.recentlyTerminal.keys().next().value;
|
|
2479
|
+
if (oldest === void 0) {
|
|
2480
|
+
break;
|
|
2481
|
+
}
|
|
2482
|
+
this.recentlyTerminal.delete(oldest);
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
// Fire hydra-acp/prompt_amended for the M1→M2 linkage. The amendment's
|
|
2486
|
+
// current content is read live from the queue entry so any update_prompt
|
|
2487
|
+
// calls during the amend window are reflected. Best-effort: if M2 has
|
|
2488
|
+
// already been cancelled out of the queue by the time we get here, we
|
|
2489
|
+
// skip — the amendInProgress clearing in cancelQueuedPrompt should have
|
|
2490
|
+
// prevented this code path from running in that case.
|
|
2491
|
+
broadcastPromptAmended(amend) {
|
|
2492
|
+
const entry = this.findUserEntry(amend.newMessageId);
|
|
2493
|
+
if (!entry) {
|
|
2494
|
+
return;
|
|
2495
|
+
}
|
|
2496
|
+
const params = {
|
|
2497
|
+
sessionId: this.sessionId,
|
|
2498
|
+
cancelledMessageId: amend.cancelledMessageId,
|
|
2499
|
+
newMessageId: amend.newMessageId,
|
|
2500
|
+
prompt: entry.prompt,
|
|
2501
|
+
originator: entry.originator,
|
|
2502
|
+
amendedAt: Date.now()
|
|
2503
|
+
};
|
|
2504
|
+
this.broadcastQueueNotification(
|
|
2505
|
+
"hydra-acp/prompt_amended",
|
|
2506
|
+
params
|
|
2507
|
+
);
|
|
2508
|
+
}
|
|
2509
|
+
// Look up a user-prompt queue entry by messageId, searching both the
|
|
2510
|
+
// currentEntry slot and the waiting queue.
|
|
2511
|
+
findUserEntry(messageId) {
|
|
2512
|
+
if (this.currentEntry?.messageId === messageId && this.currentEntry.kind === "user") {
|
|
2513
|
+
return this.currentEntry;
|
|
2514
|
+
}
|
|
2515
|
+
const queued = this.promptQueue.find(
|
|
2516
|
+
(e) => e.messageId === messageId && e.kind === "user"
|
|
2517
|
+
);
|
|
2518
|
+
return queued?.kind === "user" ? queued : void 0;
|
|
2195
2519
|
}
|
|
2196
2520
|
// Total visible-or-running entries: the in-flight head (if any) plus
|
|
2197
2521
|
// the queue's user-visible waiting entries. Internal entries don't
|
|
@@ -2204,9 +2528,9 @@ var Session = class {
|
|
|
2204
2528
|
}
|
|
2205
2529
|
return count;
|
|
2206
2530
|
}
|
|
2207
|
-
broadcastQueueAdded(entry) {
|
|
2531
|
+
broadcastQueueAdded(entry, options) {
|
|
2208
2532
|
const depth = this.visibleQueueDepth();
|
|
2209
|
-
const position = Math.max(0, depth - 1);
|
|
2533
|
+
const position = options?.position ?? Math.max(0, depth - 1);
|
|
2210
2534
|
const params = {
|
|
2211
2535
|
sessionId: this.sessionId,
|
|
2212
2536
|
messageId: entry.messageId,
|
|
@@ -2216,6 +2540,11 @@ var Session = class {
|
|
|
2216
2540
|
queueDepth: depth,
|
|
2217
2541
|
enqueuedAt: entry.enqueuedAt
|
|
2218
2542
|
};
|
|
2543
|
+
if (options?.amending !== void 0) {
|
|
2544
|
+
params._meta = {
|
|
2545
|
+
"hydra-acp": { amending: options.amending }
|
|
2546
|
+
};
|
|
2547
|
+
}
|
|
2219
2548
|
this.broadcastQueueNotification("hydra-acp/prompt_queue_added", params);
|
|
2220
2549
|
}
|
|
2221
2550
|
broadcastQueueUpdated(messageId, prompt) {
|
|
@@ -2338,6 +2667,9 @@ var Session = class {
|
|
|
2338
2667
|
this.broadcastQueueRemoved(messageId, "cancelled");
|
|
2339
2668
|
this.persistRewrite();
|
|
2340
2669
|
}
|
|
2670
|
+
if (this.amendInProgress?.newMessageId === messageId) {
|
|
2671
|
+
this.amendInProgress = void 0;
|
|
2672
|
+
}
|
|
2341
2673
|
entry.resolve({ stopReason: "cancelled" });
|
|
2342
2674
|
return { cancelled: true, reason: "ok" };
|
|
2343
2675
|
}
|
|
@@ -2359,6 +2691,143 @@ var Session = class {
|
|
|
2359
2691
|
this.persistRewrite();
|
|
2360
2692
|
return { updated: true, reason: "ok" };
|
|
2361
2693
|
}
|
|
2694
|
+
// Amend the head prompt: cancel the in-flight turn and submit a
|
|
2695
|
+
// replacement that sits at the head of the queue. Resolves the
|
|
2696
|
+
// request immediately (the caller doesn't wait on cancel-settle).
|
|
2697
|
+
// Honours race outcomes — if the target finished or was cancelled
|
|
2698
|
+
// before this arrived, the request resolves with an outcome explaining
|
|
2699
|
+
// why and (depending on onTargetCompleted) optionally forwards as a
|
|
2700
|
+
// plain prompt. Queued targets are edited in place (same machinery
|
|
2701
|
+
// as updateQueuedPrompt).
|
|
2702
|
+
amendPrompt(clientId, params) {
|
|
2703
|
+
const client = this.clients.get(clientId);
|
|
2704
|
+
if (!client) {
|
|
2705
|
+
throw withCode(
|
|
2706
|
+
new Error("client not attached"),
|
|
2707
|
+
JsonRpcErrorCodes.SessionNotFound
|
|
2708
|
+
);
|
|
2709
|
+
}
|
|
2710
|
+
const { targetMessageId, prompt, replaceQueue, onTargetCompleted } = params;
|
|
2711
|
+
if (this.currentEntry?.messageId === targetMessageId && this.currentEntry.kind === "user" && !this.currentEntry.cancelled && this.amendInProgress === void 0) {
|
|
2712
|
+
return this.amendOnHead(client, prompt, targetMessageId, replaceQueue);
|
|
2713
|
+
}
|
|
2714
|
+
const queuedEntry = this.promptQueue.find(
|
|
2715
|
+
(e) => e.messageId === targetMessageId && e.kind === "user"
|
|
2716
|
+
);
|
|
2717
|
+
if (queuedEntry && queuedEntry.kind === "user" && !queuedEntry.cancelled) {
|
|
2718
|
+
queuedEntry.prompt = prompt;
|
|
2719
|
+
this.broadcastQueueUpdated(targetMessageId, prompt);
|
|
2720
|
+
this.persistRewrite();
|
|
2721
|
+
return { amended: true, reason: "ok", messageId: targetMessageId };
|
|
2722
|
+
}
|
|
2723
|
+
const terminal = this.recentlyTerminal.get(targetMessageId);
|
|
2724
|
+
if (terminal) {
|
|
2725
|
+
if (terminal.stopReason === "cancelled") {
|
|
2726
|
+
return { amended: false, reason: "target_cancelled" };
|
|
2727
|
+
}
|
|
2728
|
+
if (onTargetCompleted === "send_anyway") {
|
|
2729
|
+
const newMessageId = this.enqueueAmendmentAsFollowUp(client, prompt);
|
|
2730
|
+
return {
|
|
2731
|
+
amended: false,
|
|
2732
|
+
reason: "target_completed",
|
|
2733
|
+
messageId: newMessageId
|
|
2734
|
+
};
|
|
2735
|
+
}
|
|
2736
|
+
return { amended: false, reason: "target_completed" };
|
|
2737
|
+
}
|
|
2738
|
+
return { amended: false, reason: "target_not_found" };
|
|
2739
|
+
}
|
|
2740
|
+
// Head-of-queue amendment: splice M2 in front of any waiting entries,
|
|
2741
|
+
// broadcast the amend window's queue_added with the amending hint,
|
|
2742
|
+
// mark amendInProgress so the cancelled turn's broadcastTurnComplete
|
|
2743
|
+
// attaches the _meta marker and fires prompt_amended, then fire the
|
|
2744
|
+
// upstream session/cancel without awaiting it. drainQueue is already
|
|
2745
|
+
// running on the head; when its session/prompt returns, it advances
|
|
2746
|
+
// to M2 in the normal way.
|
|
2747
|
+
amendOnHead(client, prompt, targetMessageId, replaceQueue) {
|
|
2748
|
+
const newMessageId = generateMessageId();
|
|
2749
|
+
const originator = { clientId: client.clientId };
|
|
2750
|
+
if (client.clientInfo?.name) {
|
|
2751
|
+
originator.name = client.clientInfo.name;
|
|
2752
|
+
}
|
|
2753
|
+
if (client.clientInfo?.version) {
|
|
2754
|
+
originator.version = client.clientInfo.version;
|
|
2755
|
+
}
|
|
2756
|
+
if (replaceQueue) {
|
|
2757
|
+
const survivors = [];
|
|
2758
|
+
for (const entry2 of this.promptQueue) {
|
|
2759
|
+
if (entry2.kind === "user" && !entry2.cancelled) {
|
|
2760
|
+
entry2.cancelled = true;
|
|
2761
|
+
this.broadcastQueueRemoved(entry2.messageId, "cancelled");
|
|
2762
|
+
entry2.resolve({ stopReason: "cancelled" });
|
|
2763
|
+
continue;
|
|
2764
|
+
}
|
|
2765
|
+
survivors.push(entry2);
|
|
2766
|
+
}
|
|
2767
|
+
this.promptQueue = survivors;
|
|
2768
|
+
}
|
|
2769
|
+
const entry = {
|
|
2770
|
+
kind: "user",
|
|
2771
|
+
messageId: newMessageId,
|
|
2772
|
+
originator,
|
|
2773
|
+
clientId: client.clientId,
|
|
2774
|
+
prompt,
|
|
2775
|
+
enqueuedAt: Date.now(),
|
|
2776
|
+
cancelled: false,
|
|
2777
|
+
wasAmend: true,
|
|
2778
|
+
// No-op resolve/reject: there's no client request awaiting M2's
|
|
2779
|
+
// session/prompt response. The amend_prompt request has already
|
|
2780
|
+
// returned by this point. drainQueue calls these unconditionally
|
|
2781
|
+
// when runQueueEntry settles; making them no-ops is safe.
|
|
2782
|
+
resolve: () => void 0,
|
|
2783
|
+
reject: () => void 0
|
|
2784
|
+
};
|
|
2785
|
+
this.promptQueue.unshift(entry);
|
|
2786
|
+
this.persistRewrite();
|
|
2787
|
+
this.broadcastQueueAdded(entry, {
|
|
2788
|
+
amending: targetMessageId,
|
|
2789
|
+
position: 1
|
|
2790
|
+
});
|
|
2791
|
+
this.amendInProgress = {
|
|
2792
|
+
cancelledMessageId: targetMessageId,
|
|
2793
|
+
newMessageId
|
|
2794
|
+
};
|
|
2795
|
+
void this.agent.connection.notify("session/cancel", { sessionId: this.upstreamSessionId }).catch(() => void 0);
|
|
2796
|
+
return {
|
|
2797
|
+
amended: true,
|
|
2798
|
+
reason: "ok",
|
|
2799
|
+
messageId: newMessageId
|
|
2800
|
+
};
|
|
2801
|
+
}
|
|
2802
|
+
// Send the amendment as a plain follow-up prompt — used when the
|
|
2803
|
+
// target already completed and the caller opted in to send_anyway.
|
|
2804
|
+
// Returns the new prompt's messageId so the result can surface it.
|
|
2805
|
+
enqueueAmendmentAsFollowUp(client, prompt) {
|
|
2806
|
+
const messageId = generateMessageId();
|
|
2807
|
+
const originator = { clientId: client.clientId };
|
|
2808
|
+
if (client.clientInfo?.name) {
|
|
2809
|
+
originator.name = client.clientInfo.name;
|
|
2810
|
+
}
|
|
2811
|
+
if (client.clientInfo?.version) {
|
|
2812
|
+
originator.version = client.clientInfo.version;
|
|
2813
|
+
}
|
|
2814
|
+
const entry = {
|
|
2815
|
+
kind: "user",
|
|
2816
|
+
messageId,
|
|
2817
|
+
originator,
|
|
2818
|
+
clientId: client.clientId,
|
|
2819
|
+
prompt,
|
|
2820
|
+
enqueuedAt: Date.now(),
|
|
2821
|
+
cancelled: false,
|
|
2822
|
+
resolve: () => void 0,
|
|
2823
|
+
reject: () => void 0
|
|
2824
|
+
};
|
|
2825
|
+
this.promptQueue.push(entry);
|
|
2826
|
+
this.persistRewrite();
|
|
2827
|
+
this.broadcastQueueAdded(entry);
|
|
2828
|
+
void this.drainQueue();
|
|
2829
|
+
return messageId;
|
|
2830
|
+
}
|
|
2362
2831
|
async cancel(clientId) {
|
|
2363
2832
|
const client = this.clients.get(clientId);
|
|
2364
2833
|
if (!client) {
|
|
@@ -2409,6 +2878,18 @@ var Session = class {
|
|
|
2409
2878
|
onTitleChange(handler) {
|
|
2410
2879
|
this.titleHandlers.push(handler);
|
|
2411
2880
|
}
|
|
2881
|
+
// External entry point for retitling a live session from outside the
|
|
2882
|
+
// ACP slash-command path (e.g. PATCH /v1/sessions/:id from the picker).
|
|
2883
|
+
// Goes through the same enqueuePrompt path as /hydra title so it
|
|
2884
|
+
// serializes after any in-flight turn and shares broadcast/persistence.
|
|
2885
|
+
retitle(title) {
|
|
2886
|
+
return this.runTitleCommand(title);
|
|
2887
|
+
}
|
|
2888
|
+
// External entry point for the LLM-regen title path (T in the picker,
|
|
2889
|
+
// equivalent to bare /hydra title with no arg).
|
|
2890
|
+
retitleFromAgent() {
|
|
2891
|
+
return this.runTitleCommand("");
|
|
2892
|
+
}
|
|
2412
2893
|
// Update the canonical title and broadcast a session_info_update to
|
|
2413
2894
|
// every attached client. Clients that already speak the spec's
|
|
2414
2895
|
// session_info_update need no hydra-specific wiring to pick this up.
|
|
@@ -2456,12 +2937,19 @@ var Session = class {
|
|
|
2456
2937
|
// Apply an agent-emitted current_model_update. Returns true if the
|
|
2457
2938
|
// notification was a model update (caller still needs to broadcast
|
|
2458
2939
|
// it). Returns false otherwise so the caller can try the next kind.
|
|
2940
|
+
// current_model_update can carry availableModels in the same payload
|
|
2941
|
+
// (per ACP spec); we cache that list too so session/set_model can
|
|
2942
|
+
// validate against it.
|
|
2459
2943
|
maybeApplyAgentModel(params) {
|
|
2460
2944
|
const obj = params ?? {};
|
|
2461
2945
|
const update = obj.update ?? {};
|
|
2462
2946
|
if (update.sessionUpdate !== "current_model_update") {
|
|
2463
2947
|
return false;
|
|
2464
2948
|
}
|
|
2949
|
+
const models = parseModelsList(update.availableModels);
|
|
2950
|
+
if (models.length > 0) {
|
|
2951
|
+
this.setAgentAdvertisedModels(models);
|
|
2952
|
+
}
|
|
2465
2953
|
const raw = typeof update.currentModel === "string" ? update.currentModel : typeof update.model === "string" ? update.model : void 0;
|
|
2466
2954
|
if (raw === void 0) {
|
|
2467
2955
|
return true;
|
|
@@ -2479,6 +2967,55 @@ var Session = class {
|
|
|
2479
2967
|
}
|
|
2480
2968
|
return true;
|
|
2481
2969
|
}
|
|
2970
|
+
// Apply an opencode-style config_option_update. opencode emits this
|
|
2971
|
+
// (not the spec-shaped current_model_update / available_models_update)
|
|
2972
|
+
// to carry both the current model and the list of available models.
|
|
2973
|
+
// The payload is `configOptions: [{ id: "model", currentValue, options:
|
|
2974
|
+
// [{ value, name }] }, ...]`. We harvest only the entry whose id is
|
|
2975
|
+
// "model" — other ids ("mode", "effort", etc.) are opencode-internal
|
|
2976
|
+
// and not consumed by hydra. Returns true when we recognized and
|
|
2977
|
+
// handled the notification so the wireAgent loop can stop trying
|
|
2978
|
+
// further extractors (the broadcast still fires; clients that grok
|
|
2979
|
+
// config_option_update render it directly).
|
|
2980
|
+
maybeApplyAgentConfigOption(params) {
|
|
2981
|
+
const obj = params ?? {};
|
|
2982
|
+
const update = obj.update ?? {};
|
|
2983
|
+
if (update.sessionUpdate !== "config_option_update") {
|
|
2984
|
+
return false;
|
|
2985
|
+
}
|
|
2986
|
+
const list = update.configOptions;
|
|
2987
|
+
if (!Array.isArray(list)) {
|
|
2988
|
+
return true;
|
|
2989
|
+
}
|
|
2990
|
+
for (const raw of list) {
|
|
2991
|
+
if (!raw || typeof raw !== "object") {
|
|
2992
|
+
continue;
|
|
2993
|
+
}
|
|
2994
|
+
const opt = raw;
|
|
2995
|
+
if (opt.id !== "model") {
|
|
2996
|
+
continue;
|
|
2997
|
+
}
|
|
2998
|
+
const models = parseModelsList(opt.options);
|
|
2999
|
+
if (models.length > 0) {
|
|
3000
|
+
this.setAgentAdvertisedModels(models);
|
|
3001
|
+
}
|
|
3002
|
+
const cv = opt.currentValue;
|
|
3003
|
+
if (typeof cv === "string") {
|
|
3004
|
+
const trimmed = cv.trim();
|
|
3005
|
+
if (trimmed && trimmed !== this.currentModel) {
|
|
3006
|
+
this.currentModel = trimmed;
|
|
3007
|
+
for (const handler of this.modelHandlers) {
|
|
3008
|
+
try {
|
|
3009
|
+
handler(trimmed);
|
|
3010
|
+
} catch {
|
|
3011
|
+
}
|
|
3012
|
+
}
|
|
3013
|
+
}
|
|
3014
|
+
}
|
|
3015
|
+
break;
|
|
3016
|
+
}
|
|
3017
|
+
return true;
|
|
3018
|
+
}
|
|
2482
3019
|
maybeApplyAgentMode(params) {
|
|
2483
3020
|
const obj = params ?? {};
|
|
2484
3021
|
const update = obj.update ?? {};
|
|
@@ -2577,6 +3114,20 @@ var Session = class {
|
|
|
2577
3114
|
}
|
|
2578
3115
|
this.broadcastAvailableModes();
|
|
2579
3116
|
}
|
|
3117
|
+
setAgentAdvertisedModels(models) {
|
|
3118
|
+
if (sameAdvertisedModels(this.agentAdvertisedModels, models)) {
|
|
3119
|
+
this.broadcastAvailableModels();
|
|
3120
|
+
return;
|
|
3121
|
+
}
|
|
3122
|
+
this.agentAdvertisedModels = models;
|
|
3123
|
+
for (const handler of this.agentModelsHandlers) {
|
|
3124
|
+
try {
|
|
3125
|
+
handler(models);
|
|
3126
|
+
} catch {
|
|
3127
|
+
}
|
|
3128
|
+
}
|
|
3129
|
+
this.broadcastAvailableModels();
|
|
3130
|
+
}
|
|
2580
3131
|
// Subscribe to snapshot-state updates. SessionManager wires these to
|
|
2581
3132
|
// persist the new value into meta.json so cold resurrect can restore
|
|
2582
3133
|
// them via the attach response _meta.
|
|
@@ -2586,6 +3137,9 @@ var Session = class {
|
|
|
2586
3137
|
onAgentModesChange(handler) {
|
|
2587
3138
|
this.agentModesHandlers.push(handler);
|
|
2588
3139
|
}
|
|
3140
|
+
onAgentModelsChange(handler) {
|
|
3141
|
+
this.agentModelsHandlers.push(handler);
|
|
3142
|
+
}
|
|
2589
3143
|
onModelChange(handler) {
|
|
2590
3144
|
this.modelHandlers.push(handler);
|
|
2591
3145
|
}
|
|
@@ -2611,6 +3165,15 @@ var Session = class {
|
|
|
2611
3165
|
availableModes() {
|
|
2612
3166
|
return [...this.agentAdvertisedModes];
|
|
2613
3167
|
}
|
|
3168
|
+
// The agent's advertised models list. Used by acp-ws.ts's dedicated
|
|
3169
|
+
// session/set_model handler to validate the requested modelId before
|
|
3170
|
+
// forwarding to the agent (catches cross-agent set_model storms from
|
|
3171
|
+
// clients that assume a different agent is on the other end). When
|
|
3172
|
+
// the agent never advertised any models, returns [] and the
|
|
3173
|
+
// set_model handler falls back to pass-through.
|
|
3174
|
+
availableModels() {
|
|
3175
|
+
return [...this.agentAdvertisedModels];
|
|
3176
|
+
}
|
|
2614
3177
|
// Pick up an agent-emitted session_info_update and store its title
|
|
2615
3178
|
// as our canonical record. The notification is also forwarded to
|
|
2616
3179
|
// clients via the surrounding recordAndBroadcast call. Authoritative
|
|
@@ -2758,6 +3321,12 @@ var Session = class {
|
|
|
2758
3321
|
this.agentMeta = fresh.agentMeta;
|
|
2759
3322
|
this.agentAdvertisedCommands = [];
|
|
2760
3323
|
this.broadcastMergedCommands();
|
|
3324
|
+
if (this.agentAdvertisedModels.length > 0) {
|
|
3325
|
+
this.setAgentAdvertisedModels([]);
|
|
3326
|
+
}
|
|
3327
|
+
if (this.agentAdvertisedModes.length > 0) {
|
|
3328
|
+
this.setAgentAdvertisedModes([]);
|
|
3329
|
+
}
|
|
2761
3330
|
await oldAgent.kill().catch(() => void 0);
|
|
2762
3331
|
if (transcript) {
|
|
2763
3332
|
await this.runInternalPrompt(transcript).catch(() => void 0);
|
|
@@ -3217,6 +3786,7 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
3217
3786
|
try {
|
|
3218
3787
|
const result = await this.runQueueEntry(next);
|
|
3219
3788
|
next.resolve(result);
|
|
3789
|
+
await Promise.resolve();
|
|
3220
3790
|
} catch (err) {
|
|
3221
3791
|
next.reject(err);
|
|
3222
3792
|
} finally {
|
|
@@ -3253,12 +3823,33 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
3253
3823
|
}
|
|
3254
3824
|
);
|
|
3255
3825
|
} catch (err) {
|
|
3256
|
-
this.broadcastTurnComplete(
|
|
3826
|
+
this.broadcastTurnComplete(
|
|
3827
|
+
entry.clientId,
|
|
3828
|
+
{ stopReason: "error" },
|
|
3829
|
+
entry.messageId,
|
|
3830
|
+
entry.wasAmend
|
|
3831
|
+
);
|
|
3832
|
+
this.clearAmendIfMatches(entry.messageId);
|
|
3257
3833
|
throw err;
|
|
3258
3834
|
}
|
|
3259
|
-
this.broadcastTurnComplete(
|
|
3835
|
+
this.broadcastTurnComplete(
|
|
3836
|
+
entry.clientId,
|
|
3837
|
+
response,
|
|
3838
|
+
entry.messageId,
|
|
3839
|
+
entry.wasAmend
|
|
3840
|
+
);
|
|
3841
|
+
this.clearAmendIfMatches(entry.messageId);
|
|
3260
3842
|
return response;
|
|
3261
3843
|
}
|
|
3844
|
+
// Clear amendInProgress once the cancelled turn's task has fully
|
|
3845
|
+
// settled. broadcastTurnComplete needs the marker still set when it
|
|
3846
|
+
// fires, so the clear must happen *after*. Called from runQueueEntry's
|
|
3847
|
+
// settle path for both success and error.
|
|
3848
|
+
clearAmendIfMatches(messageId) {
|
|
3849
|
+
if (this.amendInProgress?.cancelledMessageId === messageId) {
|
|
3850
|
+
this.amendInProgress = void 0;
|
|
3851
|
+
}
|
|
3852
|
+
}
|
|
3262
3853
|
};
|
|
3263
3854
|
function withCode(err, code) {
|
|
3264
3855
|
err.code = code;
|
|
@@ -3302,6 +3893,42 @@ function sameAdvertisedModes(a, b) {
|
|
|
3302
3893
|
}
|
|
3303
3894
|
return true;
|
|
3304
3895
|
}
|
|
3896
|
+
function sameAdvertisedModels(a, b) {
|
|
3897
|
+
if (a.length !== b.length) {
|
|
3898
|
+
return false;
|
|
3899
|
+
}
|
|
3900
|
+
for (let i = 0; i < a.length; i++) {
|
|
3901
|
+
if (a[i]?.modelId !== b[i]?.modelId || a[i]?.name !== b[i]?.name || a[i]?.description !== b[i]?.description) {
|
|
3902
|
+
return false;
|
|
3903
|
+
}
|
|
3904
|
+
}
|
|
3905
|
+
return true;
|
|
3906
|
+
}
|
|
3907
|
+
function parseModelsList(list) {
|
|
3908
|
+
if (!Array.isArray(list)) {
|
|
3909
|
+
return [];
|
|
3910
|
+
}
|
|
3911
|
+
const out = [];
|
|
3912
|
+
for (const raw of list) {
|
|
3913
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
3914
|
+
continue;
|
|
3915
|
+
}
|
|
3916
|
+
const r = raw;
|
|
3917
|
+
const modelId = typeof r.modelId === "string" && r.modelId.trim() || typeof r.value === "string" && r.value.trim() || typeof r.id === "string" && r.id.trim() || void 0;
|
|
3918
|
+
if (!modelId) {
|
|
3919
|
+
continue;
|
|
3920
|
+
}
|
|
3921
|
+
const model = { modelId };
|
|
3922
|
+
if (typeof r.name === "string" && r.name.length > 0) {
|
|
3923
|
+
model.name = r.name;
|
|
3924
|
+
}
|
|
3925
|
+
if (typeof r.description === "string" && r.description.length > 0) {
|
|
3926
|
+
model.description = r.description;
|
|
3927
|
+
}
|
|
3928
|
+
out.push(model);
|
|
3929
|
+
}
|
|
3930
|
+
return out;
|
|
3931
|
+
}
|
|
3305
3932
|
function extractAdvertisedModes(params) {
|
|
3306
3933
|
const obj = params ?? {};
|
|
3307
3934
|
const update = obj.update ?? {};
|
|
@@ -3506,6 +4133,11 @@ var PersistedAgentMode = z4.object({
|
|
|
3506
4133
|
name: z4.string().optional(),
|
|
3507
4134
|
description: z4.string().optional()
|
|
3508
4135
|
});
|
|
4136
|
+
var PersistedAgentModel = z4.object({
|
|
4137
|
+
modelId: z4.string(),
|
|
4138
|
+
name: z4.string().optional(),
|
|
4139
|
+
description: z4.string().optional()
|
|
4140
|
+
});
|
|
3509
4141
|
var PersistedUsage = z4.object({
|
|
3510
4142
|
used: z4.number().optional(),
|
|
3511
4143
|
size: z4.number().optional(),
|
|
@@ -3552,6 +4184,7 @@ var SessionRecord = z4.object({
|
|
|
3552
4184
|
currentUsage: PersistedUsage.optional(),
|
|
3553
4185
|
agentCommands: z4.array(PersistedAgentCommand).optional(),
|
|
3554
4186
|
agentModes: z4.array(PersistedAgentMode).optional(),
|
|
4187
|
+
agentModels: z4.array(PersistedAgentModel).optional(),
|
|
3555
4188
|
createdAt: z4.string(),
|
|
3556
4189
|
updatedAt: z4.string()
|
|
3557
4190
|
});
|
|
@@ -3671,6 +4304,7 @@ function recordFromMemorySession(args) {
|
|
|
3671
4304
|
currentUsage: args.currentUsage,
|
|
3672
4305
|
agentCommands: args.agentCommands,
|
|
3673
4306
|
agentModes: args.agentModes,
|
|
4307
|
+
agentModels: args.agentModels,
|
|
3674
4308
|
createdAt: args.createdAt ?? now,
|
|
3675
4309
|
updatedAt: args.updatedAt ?? now
|
|
3676
4310
|
};
|
|
@@ -3905,7 +4539,8 @@ var SessionManager = class {
|
|
|
3905
4539
|
cwd: params.cwd,
|
|
3906
4540
|
agentArgs: params.agentArgs,
|
|
3907
4541
|
mcpServers: params.mcpServers,
|
|
3908
|
-
model: params.model
|
|
4542
|
+
model: params.model,
|
|
4543
|
+
onInstallProgress: params.onInstallProgress
|
|
3909
4544
|
});
|
|
3910
4545
|
const session = new Session({
|
|
3911
4546
|
cwd: params.cwd,
|
|
@@ -3922,7 +4557,8 @@ var SessionManager = class {
|
|
|
3922
4557
|
historyMaxEntries: this.sessionHistoryMaxEntries,
|
|
3923
4558
|
currentModel: fresh.initialModel,
|
|
3924
4559
|
currentMode: fresh.initialMode,
|
|
3925
|
-
agentModes: fresh.initialModes
|
|
4560
|
+
agentModes: fresh.initialModes,
|
|
4561
|
+
agentModels: fresh.initialModels
|
|
3926
4562
|
});
|
|
3927
4563
|
await this.attachManagerHooks(session);
|
|
3928
4564
|
return session;
|
|
@@ -3967,7 +4603,10 @@ var SessionManager = class {
|
|
|
3967
4603
|
if (params.upstreamSessionId === "") {
|
|
3968
4604
|
return this.doResurrectFromImport(params);
|
|
3969
4605
|
}
|
|
3970
|
-
const plan = await planSpawn(agentDef, params.agentArgs ?? [], {
|
|
4606
|
+
const plan = await planSpawn(agentDef, params.agentArgs ?? [], {
|
|
4607
|
+
npmRegistry: this.npmRegistry,
|
|
4608
|
+
onInstallProgress: params.onInstallProgress
|
|
4609
|
+
});
|
|
3971
4610
|
const agent = this.spawner({
|
|
3972
4611
|
agentId: params.agentId,
|
|
3973
4612
|
cwd: params.cwd,
|
|
@@ -4025,6 +4664,7 @@ var SessionManager = class {
|
|
|
4025
4664
|
currentUsage: params.currentUsage,
|
|
4026
4665
|
agentCommands: params.agentCommands,
|
|
4027
4666
|
agentModes: params.agentModes ?? nonEmptyOrUndefined(extractInitialModes(loadResult ?? {})),
|
|
4667
|
+
agentModels: params.agentModels ?? nonEmptyOrUndefined(extractInitialModels(loadResult ?? {})),
|
|
4028
4668
|
// Only gate the first-prompt title heuristic when we actually have
|
|
4029
4669
|
// a title to preserve. A title-less session (lost to a write race
|
|
4030
4670
|
// or never seeded) should re-derive from the next prompt rather
|
|
@@ -4048,7 +4688,8 @@ var SessionManager = class {
|
|
|
4048
4688
|
agentId: params.agentId,
|
|
4049
4689
|
cwd,
|
|
4050
4690
|
agentArgs: params.agentArgs,
|
|
4051
|
-
mcpServers: []
|
|
4691
|
+
mcpServers: [],
|
|
4692
|
+
onInstallProgress: params.onInstallProgress
|
|
4052
4693
|
});
|
|
4053
4694
|
const session = new Session({
|
|
4054
4695
|
sessionId: params.hydraSessionId,
|
|
@@ -4071,6 +4712,7 @@ var SessionManager = class {
|
|
|
4071
4712
|
currentUsage: params.currentUsage,
|
|
4072
4713
|
agentCommands: params.agentCommands,
|
|
4073
4714
|
agentModes: params.agentModes ?? fresh.initialModes,
|
|
4715
|
+
agentModels: params.agentModels ?? fresh.initialModels,
|
|
4074
4716
|
firstPromptSeeded: !!params.title,
|
|
4075
4717
|
createdAt: params.createdAt ? new Date(params.createdAt).getTime() : void 0
|
|
4076
4718
|
});
|
|
@@ -4100,7 +4742,10 @@ var SessionManager = class {
|
|
|
4100
4742
|
err.code = JsonRpcErrorCodes.AgentNotInstalled;
|
|
4101
4743
|
throw err;
|
|
4102
4744
|
}
|
|
4103
|
-
const plan = await planSpawn(agentDef, params.agentArgs ?? [], {
|
|
4745
|
+
const plan = await planSpawn(agentDef, params.agentArgs ?? [], {
|
|
4746
|
+
npmRegistry: this.npmRegistry,
|
|
4747
|
+
onInstallProgress: params.onInstallProgress
|
|
4748
|
+
});
|
|
4104
4749
|
const agent = this.spawner({
|
|
4105
4750
|
agentId: params.agentId,
|
|
4106
4751
|
cwd: params.cwd,
|
|
@@ -4126,15 +4771,25 @@ var SessionManager = class {
|
|
|
4126
4771
|
);
|
|
4127
4772
|
}
|
|
4128
4773
|
let initialModel = extractInitialModel(newResult);
|
|
4774
|
+
const initialModels = extractInitialModels(newResult);
|
|
4129
4775
|
const desired = params.model ?? this.defaultModels[params.agentId];
|
|
4130
4776
|
if (desired && desired !== initialModel) {
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4777
|
+
const validates = initialModels.length === 0 || initialModels.some((m) => m.modelId === desired);
|
|
4778
|
+
if (validates) {
|
|
4779
|
+
try {
|
|
4780
|
+
await agent.connection.request("session/set_model", {
|
|
4781
|
+
sessionId: sessionIdRaw,
|
|
4782
|
+
modelId: desired
|
|
4783
|
+
});
|
|
4784
|
+
initialModel = desired;
|
|
4785
|
+
} catch {
|
|
4786
|
+
}
|
|
4787
|
+
} else {
|
|
4788
|
+
const known = initialModels.map((m) => m.modelId).join(", ");
|
|
4789
|
+
process.stderr.write(
|
|
4790
|
+
`hydra-acp: defaultModels[${params.agentId}]=${JSON.stringify(desired)} is not in the agent's availableModels (${known}); skipping session/set_model
|
|
4791
|
+
`
|
|
4792
|
+
);
|
|
4138
4793
|
}
|
|
4139
4794
|
}
|
|
4140
4795
|
const initialModes = extractInitialModes(newResult);
|
|
@@ -4144,6 +4799,7 @@ var SessionManager = class {
|
|
|
4144
4799
|
upstreamSessionId: sessionIdRaw,
|
|
4145
4800
|
agentMeta: newResult._meta,
|
|
4146
4801
|
initialModel,
|
|
4802
|
+
initialModels: initialModels.length > 0 ? initialModels : void 0,
|
|
4147
4803
|
initialModes: initialModes.length > 0 ? initialModes : void 0,
|
|
4148
4804
|
initialMode
|
|
4149
4805
|
};
|
|
@@ -4206,6 +4862,15 @@ var SessionManager = class {
|
|
|
4206
4862
|
}))
|
|
4207
4863
|
}).catch(() => void 0);
|
|
4208
4864
|
});
|
|
4865
|
+
session.onAgentModelsChange((models) => {
|
|
4866
|
+
void this.persistSnapshot(session.sessionId, {
|
|
4867
|
+
agentModels: models.map((m) => ({
|
|
4868
|
+
modelId: m.modelId,
|
|
4869
|
+
...m.name !== void 0 ? { name: m.name } : {},
|
|
4870
|
+
...m.description !== void 0 ? { description: m.description } : {}
|
|
4871
|
+
}))
|
|
4872
|
+
}).catch(() => void 0);
|
|
4873
|
+
});
|
|
4209
4874
|
this.sessions.set(session.sessionId, session);
|
|
4210
4875
|
await this.enqueueMetaWrite(session.sessionId, async () => {
|
|
4211
4876
|
const existing = await this.store.read(session.sessionId);
|
|
@@ -4249,6 +4914,7 @@ var SessionManager = class {
|
|
|
4249
4914
|
currentUsage: persistedUsageToSnapshot(record.currentUsage),
|
|
4250
4915
|
agentCommands: record.agentCommands,
|
|
4251
4916
|
agentModes: record.agentModes,
|
|
4917
|
+
agentModels: record.agentModels,
|
|
4252
4918
|
createdAt: record.createdAt
|
|
4253
4919
|
};
|
|
4254
4920
|
}
|
|
@@ -4492,6 +5158,26 @@ var SessionManager = class {
|
|
|
4492
5158
|
const record = await this.store.read(sessionId).catch(() => void 0);
|
|
4493
5159
|
return record !== void 0;
|
|
4494
5160
|
}
|
|
5161
|
+
// Public retitle entry point that works on live AND cold sessions.
|
|
5162
|
+
// - Live: routes through Session.retitle so attached clients receive
|
|
5163
|
+
// a session_info_update broadcast (and persistTitle fires from the
|
|
5164
|
+
// onTitleChange handler, just like /hydra title).
|
|
5165
|
+
// - Cold: writes the new title straight into meta.json — there's
|
|
5166
|
+
// nothing in memory to broadcast to, but a later resurrect / list
|
|
5167
|
+
// will pick up the new title.
|
|
5168
|
+
// Returns false when no record exists at all (live or on disk).
|
|
5169
|
+
async setTitle(sessionId, title) {
|
|
5170
|
+
const live = this.get(sessionId);
|
|
5171
|
+
if (live) {
|
|
5172
|
+
await live.retitle(title);
|
|
5173
|
+
return true;
|
|
5174
|
+
}
|
|
5175
|
+
if (!await this.hasRecord(sessionId)) {
|
|
5176
|
+
return false;
|
|
5177
|
+
}
|
|
5178
|
+
await this.persistTitle(sessionId, title);
|
|
5179
|
+
return true;
|
|
5180
|
+
}
|
|
4495
5181
|
// Persist a title update from Session.setTitle. The on-disk record
|
|
4496
5182
|
// was written at create time; updating it here keeps the session
|
|
4497
5183
|
// record's title in sync with what was broadcast to clients so a
|
|
@@ -4544,6 +5230,7 @@ var SessionManager = class {
|
|
|
4544
5230
|
...update.currentUsage !== void 0 ? { currentUsage: update.currentUsage } : {},
|
|
4545
5231
|
...update.agentCommands !== void 0 ? { agentCommands: update.agentCommands } : {},
|
|
4546
5232
|
...update.agentModes !== void 0 ? { agentModes: update.agentModes } : {},
|
|
5233
|
+
...update.agentModels !== void 0 ? { agentModels: update.agentModels } : {},
|
|
4547
5234
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4548
5235
|
});
|
|
4549
5236
|
});
|
|
@@ -4645,6 +5332,18 @@ function mergeForPersistence(session, existing) {
|
|
|
4645
5332
|
return out;
|
|
4646
5333
|
}) : void 0;
|
|
4647
5334
|
const agentModes = persistedModes ?? existing?.agentModes;
|
|
5335
|
+
const sessionModels = session.availableModels();
|
|
5336
|
+
const persistedModels = sessionModels.length > 0 ? sessionModels.map((m) => {
|
|
5337
|
+
const out = { modelId: m.modelId };
|
|
5338
|
+
if (m.name !== void 0) {
|
|
5339
|
+
out.name = m.name;
|
|
5340
|
+
}
|
|
5341
|
+
if (m.description !== void 0) {
|
|
5342
|
+
out.description = m.description;
|
|
5343
|
+
}
|
|
5344
|
+
return out;
|
|
5345
|
+
}) : void 0;
|
|
5346
|
+
const agentModels = persistedModels ?? existing?.agentModels;
|
|
4648
5347
|
return recordFromMemorySession({
|
|
4649
5348
|
sessionId: session.sessionId,
|
|
4650
5349
|
lineageId: existing?.lineageId ?? generateLineageId(),
|
|
@@ -4661,6 +5360,7 @@ function mergeForPersistence(session, existing) {
|
|
|
4661
5360
|
currentUsage: usageSnapshotToPersisted(session.currentUsage) ?? existing?.currentUsage,
|
|
4662
5361
|
agentCommands,
|
|
4663
5362
|
agentModes,
|
|
5363
|
+
agentModels,
|
|
4664
5364
|
createdAt: existing?.createdAt ?? new Date(session.createdAt).toISOString()
|
|
4665
5365
|
});
|
|
4666
5366
|
}
|
|
@@ -4726,6 +5426,40 @@ function asString(value) {
|
|
|
4726
5426
|
function nonEmptyOrUndefined(arr) {
|
|
4727
5427
|
return arr.length > 0 ? arr : void 0;
|
|
4728
5428
|
}
|
|
5429
|
+
function extractInitialModels(result) {
|
|
5430
|
+
const direct = parseModelsList(result.availableModels);
|
|
5431
|
+
if (direct.length > 0) {
|
|
5432
|
+
return direct;
|
|
5433
|
+
}
|
|
5434
|
+
const models = result.models;
|
|
5435
|
+
if (models && typeof models === "object" && !Array.isArray(models)) {
|
|
5436
|
+
const fromModelsObj = parseModelsList(
|
|
5437
|
+
models.availableModels
|
|
5438
|
+
);
|
|
5439
|
+
if (fromModelsObj.length > 0) {
|
|
5440
|
+
return fromModelsObj;
|
|
5441
|
+
}
|
|
5442
|
+
}
|
|
5443
|
+
const meta = result._meta;
|
|
5444
|
+
if (meta && typeof meta === "object" && !Array.isArray(meta)) {
|
|
5445
|
+
for (const [key, value] of Object.entries(
|
|
5446
|
+
meta
|
|
5447
|
+
)) {
|
|
5448
|
+
if (key === "hydra-acp") {
|
|
5449
|
+
continue;
|
|
5450
|
+
}
|
|
5451
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
5452
|
+
const fromMeta = parseModelsList(
|
|
5453
|
+
value.availableModels
|
|
5454
|
+
);
|
|
5455
|
+
if (fromMeta.length > 0) {
|
|
5456
|
+
return fromMeta;
|
|
5457
|
+
}
|
|
5458
|
+
}
|
|
5459
|
+
}
|
|
5460
|
+
}
|
|
5461
|
+
return [];
|
|
5462
|
+
}
|
|
4729
5463
|
function extractInitialModes(result) {
|
|
4730
5464
|
const direct = parseModesList(result.availableModes);
|
|
4731
5465
|
if (direct.length > 0) {
|
|
@@ -5948,8 +6682,55 @@ function mapToolCallUpdate(u) {
|
|
|
5948
6682
|
if (status !== void 0) {
|
|
5949
6683
|
event.status = status;
|
|
5950
6684
|
}
|
|
6685
|
+
if (status === "failed") {
|
|
6686
|
+
const errorText = extractToolFailureText(u);
|
|
6687
|
+
if (errorText !== null) {
|
|
6688
|
+
event.errorText = errorText;
|
|
6689
|
+
}
|
|
6690
|
+
if (isUpstreamInterrupted(u, errorText)) {
|
|
6691
|
+
event.upstreamInterrupted = true;
|
|
6692
|
+
}
|
|
6693
|
+
}
|
|
5951
6694
|
return event;
|
|
5952
6695
|
}
|
|
6696
|
+
function extractToolFailureText(u) {
|
|
6697
|
+
const content = u.content;
|
|
6698
|
+
if (Array.isArray(content)) {
|
|
6699
|
+
for (const block of content) {
|
|
6700
|
+
if (!block || typeof block !== "object") {
|
|
6701
|
+
continue;
|
|
6702
|
+
}
|
|
6703
|
+
const b = block;
|
|
6704
|
+
const text = extractContentText(b.content);
|
|
6705
|
+
if (text !== null && text.length > 0) {
|
|
6706
|
+
return text;
|
|
6707
|
+
}
|
|
6708
|
+
}
|
|
6709
|
+
}
|
|
6710
|
+
const rawOutput = u.rawOutput;
|
|
6711
|
+
if (rawOutput && typeof rawOutput === "object") {
|
|
6712
|
+
const err = rawOutput.error;
|
|
6713
|
+
if (typeof err === "string" && err.length > 0) {
|
|
6714
|
+
return sanitizeWireText(err);
|
|
6715
|
+
}
|
|
6716
|
+
}
|
|
6717
|
+
return null;
|
|
6718
|
+
}
|
|
6719
|
+
function isUpstreamInterrupted(u, errorText) {
|
|
6720
|
+
const rawOutput = u.rawOutput;
|
|
6721
|
+
if (rawOutput && typeof rawOutput === "object") {
|
|
6722
|
+
const meta = rawOutput.metadata;
|
|
6723
|
+
if (meta && typeof meta === "object") {
|
|
6724
|
+
if (meta.interrupted === true) {
|
|
6725
|
+
return true;
|
|
6726
|
+
}
|
|
6727
|
+
}
|
|
6728
|
+
}
|
|
6729
|
+
if (errorText !== null && errorText.toLowerCase().includes("tool execution aborted")) {
|
|
6730
|
+
return true;
|
|
6731
|
+
}
|
|
6732
|
+
return false;
|
|
6733
|
+
}
|
|
5953
6734
|
function mapPlan(u) {
|
|
5954
6735
|
const entries = u.entries;
|
|
5955
6736
|
if (!Array.isArray(entries)) {
|
|
@@ -5992,7 +6773,16 @@ function mapModel(u) {
|
|
|
5992
6773
|
}
|
|
5993
6774
|
function mapTurnComplete(u) {
|
|
5994
6775
|
const stopReason = readString(u, "stopReason");
|
|
5995
|
-
|
|
6776
|
+
const meta = u._meta;
|
|
6777
|
+
const amended = meta?.["hydra-acp"]?.amended !== void 0 && meta["hydra-acp"].amended !== null;
|
|
6778
|
+
const out = { kind: "turn-complete" };
|
|
6779
|
+
if (stopReason !== void 0) {
|
|
6780
|
+
out.stopReason = stopReason;
|
|
6781
|
+
}
|
|
6782
|
+
if (amended) {
|
|
6783
|
+
out.amended = true;
|
|
6784
|
+
}
|
|
6785
|
+
return out;
|
|
5996
6786
|
}
|
|
5997
6787
|
function extractContentText(content) {
|
|
5998
6788
|
if (typeof content === "string") {
|
|
@@ -6321,6 +7111,35 @@ function registerSessionRoutes(app, manager, defaults) {
|
|
|
6321
7111
|
}
|
|
6322
7112
|
reply.code(204).send();
|
|
6323
7113
|
});
|
|
7114
|
+
app.patch("/v1/sessions/:id", async (request, reply) => {
|
|
7115
|
+
const raw = request.params.id;
|
|
7116
|
+
const id = await manager.resolveCanonicalId(raw) ?? raw;
|
|
7117
|
+
const body = request.body ?? {};
|
|
7118
|
+
if (body.regen === true) {
|
|
7119
|
+
const session = manager.get(id);
|
|
7120
|
+
if (!session) {
|
|
7121
|
+
reply.code(409).send({ error: "regen requires a live session" });
|
|
7122
|
+
return;
|
|
7123
|
+
}
|
|
7124
|
+
void session.retitleFromAgent().catch((err) => {
|
|
7125
|
+
app.log.warn(
|
|
7126
|
+
`title regen failed for ${id}: ${err.message}`
|
|
7127
|
+
);
|
|
7128
|
+
});
|
|
7129
|
+
reply.code(202).send();
|
|
7130
|
+
return;
|
|
7131
|
+
}
|
|
7132
|
+
if (typeof body.title !== "string" || body.title.trim().length === 0) {
|
|
7133
|
+
reply.code(400).send({ error: "title must be a non-empty string" });
|
|
7134
|
+
return;
|
|
7135
|
+
}
|
|
7136
|
+
const ok = await manager.setTitle(id, body.title);
|
|
7137
|
+
if (!ok) {
|
|
7138
|
+
reply.code(404).send({ error: "session not found" });
|
|
7139
|
+
return;
|
|
7140
|
+
}
|
|
7141
|
+
reply.code(204).send();
|
|
7142
|
+
});
|
|
6324
7143
|
app.delete("/v1/sessions/:id", async (request, reply) => {
|
|
6325
7144
|
const raw = request.params.id;
|
|
6326
7145
|
const id = await manager.resolveCanonicalId(raw) ?? raw;
|
|
@@ -6908,7 +7727,8 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
6908
7727
|
mcpServers: params.mcpServers,
|
|
6909
7728
|
title: hydraMeta.name,
|
|
6910
7729
|
agentArgs: hydraMeta.agentArgs,
|
|
6911
|
-
model: hydraMeta.model
|
|
7730
|
+
model: hydraMeta.model,
|
|
7731
|
+
onInstallProgress: makeInstallProgressForwarder(connection)
|
|
6912
7732
|
});
|
|
6913
7733
|
const client = bindClientToSession(connection, session, state);
|
|
6914
7734
|
const { entries: replay } = await session.attach(client, "full");
|
|
@@ -6924,6 +7744,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
6924
7744
|
})();
|
|
6925
7745
|
});
|
|
6926
7746
|
const modesPayload = buildModesPayload(session);
|
|
7747
|
+
const modelsPayload = buildModelsPayload(session);
|
|
6927
7748
|
return {
|
|
6928
7749
|
sessionId: session.sessionId,
|
|
6929
7750
|
// session/new is implicitly an attach; mirror session/attach's
|
|
@@ -6932,6 +7753,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
6932
7753
|
// events without an extra round-trip.
|
|
6933
7754
|
clientId: client.clientId,
|
|
6934
7755
|
...modesPayload ? { modes: modesPayload } : {},
|
|
7756
|
+
...modelsPayload ? { models: modelsPayload } : {},
|
|
6935
7757
|
_meta: buildResponseMeta(session)
|
|
6936
7758
|
};
|
|
6937
7759
|
});
|
|
@@ -6967,7 +7789,10 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
6967
7789
|
err.code = JsonRpcErrorCodes.SessionNotFound;
|
|
6968
7790
|
throw err;
|
|
6969
7791
|
}
|
|
6970
|
-
session = await deps.manager.resurrect(
|
|
7792
|
+
session = await deps.manager.resurrect({
|
|
7793
|
+
...resurrectParams,
|
|
7794
|
+
onInstallProgress: makeInstallProgressForwarder(connection)
|
|
7795
|
+
});
|
|
6971
7796
|
}
|
|
6972
7797
|
const client = bindClientToSession(
|
|
6973
7798
|
connection,
|
|
@@ -6993,6 +7818,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
6993
7818
|
}
|
|
6994
7819
|
session.replayPendingPermissions(client);
|
|
6995
7820
|
const modesPayload = buildModesPayload(session);
|
|
7821
|
+
const modelsPayload = buildModelsPayload(session);
|
|
6996
7822
|
return {
|
|
6997
7823
|
sessionId: session.sessionId,
|
|
6998
7824
|
clientId: client.clientId,
|
|
@@ -7004,6 +7830,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
7004
7830
|
historyPolicy: appliedPolicy,
|
|
7005
7831
|
replayed: replay.length,
|
|
7006
7832
|
...modesPayload ? { modes: modesPayload } : {},
|
|
7833
|
+
...modelsPayload ? { models: modelsPayload } : {},
|
|
7007
7834
|
_meta: buildResponseMeta(session)
|
|
7008
7835
|
};
|
|
7009
7836
|
});
|
|
@@ -7111,6 +7938,22 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
7111
7938
|
}
|
|
7112
7939
|
return session.updateQueuedPrompt(params.messageId, params.prompt);
|
|
7113
7940
|
});
|
|
7941
|
+
connection.onRequest("hydra-acp/amend_prompt", async (raw) => {
|
|
7942
|
+
const params = AmendPromptParams.parse(raw);
|
|
7943
|
+
const att = state.attached.get(params.sessionId);
|
|
7944
|
+
if (!att) {
|
|
7945
|
+
const err = new Error("not attached to session");
|
|
7946
|
+
err.code = JsonRpcErrorCodes.SessionNotFound;
|
|
7947
|
+
throw err;
|
|
7948
|
+
}
|
|
7949
|
+
const session = deps.manager.get(params.sessionId);
|
|
7950
|
+
if (!session) {
|
|
7951
|
+
const err = new Error(`session ${params.sessionId} not found`);
|
|
7952
|
+
err.code = JsonRpcErrorCodes.SessionNotFound;
|
|
7953
|
+
throw err;
|
|
7954
|
+
}
|
|
7955
|
+
return session.amendPrompt(att.clientId, params);
|
|
7956
|
+
});
|
|
7114
7957
|
connection.onRequest("session/load", async (raw) => {
|
|
7115
7958
|
const rawObj = raw ?? {};
|
|
7116
7959
|
const rawSessionId = typeof rawObj.sessionId === "string" ? rawObj.sessionId : void 0;
|
|
@@ -7143,15 +7986,39 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
7143
7986
|
}
|
|
7144
7987
|
session.replayPendingPermissions(client);
|
|
7145
7988
|
const modesPayload = buildModesPayload(session);
|
|
7989
|
+
const modelsPayload = buildModelsPayload(session);
|
|
7146
7990
|
return {
|
|
7147
7991
|
sessionId: session.sessionId,
|
|
7148
7992
|
// Same as session/new: include clientId so the deferred-echo
|
|
7149
7993
|
// path in queue-aware clients can recognize own broadcasts.
|
|
7150
7994
|
clientId: client.clientId,
|
|
7151
7995
|
...modesPayload ? { modes: modesPayload } : {},
|
|
7996
|
+
...modelsPayload ? { models: modelsPayload } : {},
|
|
7152
7997
|
_meta: buildResponseMeta(session)
|
|
7153
7998
|
};
|
|
7154
7999
|
});
|
|
8000
|
+
connection.onRequest("session/set_model", async (rawParams) => {
|
|
8001
|
+
const decision = decideSetModel(rawParams, deps.manager);
|
|
8002
|
+
if (decision.kind === "error") {
|
|
8003
|
+
app.log.warn(decision.logMessage);
|
|
8004
|
+
const err = new Error(decision.message);
|
|
8005
|
+
err.code = decision.code;
|
|
8006
|
+
throw err;
|
|
8007
|
+
}
|
|
8008
|
+
if (decision.kind === "no_op") {
|
|
8009
|
+
app.log.warn(decision.logMessage);
|
|
8010
|
+
await connection.notify("session/update", {
|
|
8011
|
+
sessionId: decision.sessionId,
|
|
8012
|
+
update: {
|
|
8013
|
+
sessionUpdate: "current_model_update",
|
|
8014
|
+
currentModel: decision.currentModel
|
|
8015
|
+
}
|
|
8016
|
+
}).catch(() => void 0);
|
|
8017
|
+
return null;
|
|
8018
|
+
}
|
|
8019
|
+
app.log.info(decision.logMessage);
|
|
8020
|
+
return decision.session.forwardRequest("session/set_model", rawParams);
|
|
8021
|
+
});
|
|
7155
8022
|
connection.setDefaultHandler(async (rawParams, method) => {
|
|
7156
8023
|
if (!method.startsWith("session/") || rawParams === null || typeof rawParams !== "object") {
|
|
7157
8024
|
const err = new Error(`Method not found: ${method}`);
|
|
@@ -7174,6 +8041,26 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
7174
8041
|
});
|
|
7175
8042
|
});
|
|
7176
8043
|
}
|
|
8044
|
+
function makeInstallProgressForwarder(connection) {
|
|
8045
|
+
return (event) => {
|
|
8046
|
+
const payload = {
|
|
8047
|
+
agentId: event.agentId,
|
|
8048
|
+
version: event.version,
|
|
8049
|
+
source: event.source,
|
|
8050
|
+
phase: event.phase
|
|
8051
|
+
};
|
|
8052
|
+
if ("receivedBytes" in event) {
|
|
8053
|
+
payload.receivedBytes = event.receivedBytes;
|
|
8054
|
+
}
|
|
8055
|
+
if ("totalBytes" in event) {
|
|
8056
|
+
payload.totalBytes = event.totalBytes;
|
|
8057
|
+
}
|
|
8058
|
+
if ("packageSpec" in event) {
|
|
8059
|
+
payload.packageSpec = event.packageSpec;
|
|
8060
|
+
}
|
|
8061
|
+
void connection.notify(AGENT_INSTALL_PROGRESS_METHOD, payload).catch(() => void 0);
|
|
8062
|
+
};
|
|
8063
|
+
}
|
|
7177
8064
|
function buildModesPayload(session) {
|
|
7178
8065
|
const modes = session.availableModes();
|
|
7179
8066
|
if (modes.length === 0) {
|
|
@@ -7194,6 +8081,94 @@ function buildModesPayload(session) {
|
|
|
7194
8081
|
const currentModeId = session.currentMode ?? modes[0].id;
|
|
7195
8082
|
return { currentModeId, availableModes };
|
|
7196
8083
|
}
|
|
8084
|
+
function buildModelsPayload(session) {
|
|
8085
|
+
const models = session.availableModels();
|
|
8086
|
+
if (models.length === 0) {
|
|
8087
|
+
return void 0;
|
|
8088
|
+
}
|
|
8089
|
+
const availableModels = models.map((m) => {
|
|
8090
|
+
const out = {
|
|
8091
|
+
modelId: m.modelId
|
|
8092
|
+
};
|
|
8093
|
+
if (m.name !== void 0) {
|
|
8094
|
+
out.name = m.name;
|
|
8095
|
+
}
|
|
8096
|
+
if (m.description !== void 0) {
|
|
8097
|
+
out.description = m.description;
|
|
8098
|
+
}
|
|
8099
|
+
return out;
|
|
8100
|
+
});
|
|
8101
|
+
const currentModelId = session.currentModel ?? models[0].modelId;
|
|
8102
|
+
return { currentModelId, availableModels };
|
|
8103
|
+
}
|
|
8104
|
+
function decideSetModel(rawParams, manager) {
|
|
8105
|
+
if (!rawParams || typeof rawParams !== "object") {
|
|
8106
|
+
return {
|
|
8107
|
+
kind: "error",
|
|
8108
|
+
code: JsonRpcErrorCodes.InvalidParams,
|
|
8109
|
+
message: "session/set_model requires params",
|
|
8110
|
+
logMessage: "session/set_model rejected: params not an object"
|
|
8111
|
+
};
|
|
8112
|
+
}
|
|
8113
|
+
const params = rawParams;
|
|
8114
|
+
if (typeof params.sessionId !== "string") {
|
|
8115
|
+
return {
|
|
8116
|
+
kind: "error",
|
|
8117
|
+
code: JsonRpcErrorCodes.InvalidParams,
|
|
8118
|
+
message: "session/set_model requires string sessionId",
|
|
8119
|
+
logMessage: "session/set_model rejected: missing/non-string sessionId"
|
|
8120
|
+
};
|
|
8121
|
+
}
|
|
8122
|
+
if (typeof params.modelId !== "string") {
|
|
8123
|
+
return {
|
|
8124
|
+
kind: "error",
|
|
8125
|
+
code: JsonRpcErrorCodes.InvalidParams,
|
|
8126
|
+
message: "session/set_model requires string modelId",
|
|
8127
|
+
logMessage: `session/set_model rejected: missing/non-string modelId sessionId=${params.sessionId}`
|
|
8128
|
+
};
|
|
8129
|
+
}
|
|
8130
|
+
const session = manager.get(params.sessionId);
|
|
8131
|
+
if (!session) {
|
|
8132
|
+
return {
|
|
8133
|
+
kind: "error",
|
|
8134
|
+
code: JsonRpcErrorCodes.SessionNotFound,
|
|
8135
|
+
message: `session ${params.sessionId} not found`,
|
|
8136
|
+
logMessage: `session/set_model rejected: session not found sessionId=${params.sessionId}`
|
|
8137
|
+
};
|
|
8138
|
+
}
|
|
8139
|
+
const advertised = session.availableModels();
|
|
8140
|
+
if (advertised.length === 0) {
|
|
8141
|
+
return {
|
|
8142
|
+
kind: "ok",
|
|
8143
|
+
session,
|
|
8144
|
+
logMessage: `session/set_model passthrough (no availableModels) sessionId=${params.sessionId} modelId=${JSON.stringify(params.modelId)}`
|
|
8145
|
+
};
|
|
8146
|
+
}
|
|
8147
|
+
const match = advertised.find((m) => m.modelId === params.modelId);
|
|
8148
|
+
if (!match) {
|
|
8149
|
+
const known = advertised.map((m) => m.modelId).join(", ");
|
|
8150
|
+
if (session.currentModel !== void 0 && session.currentModel.length > 0) {
|
|
8151
|
+
return {
|
|
8152
|
+
kind: "no_op",
|
|
8153
|
+
session,
|
|
8154
|
+
sessionId: params.sessionId,
|
|
8155
|
+
currentModel: session.currentModel,
|
|
8156
|
+
logMessage: `session/set_model no_op (resyncing client) sessionId=${params.sessionId} requested=${JSON.stringify(params.modelId)} actual=${JSON.stringify(session.currentModel)} agentId=${session.agentId} known=[${known}]`
|
|
8157
|
+
};
|
|
8158
|
+
}
|
|
8159
|
+
return {
|
|
8160
|
+
kind: "error",
|
|
8161
|
+
code: JsonRpcErrorCodes.InvalidParams,
|
|
8162
|
+
message: `model "${params.modelId}" is not in this session's availableModels (agent ${session.agentId}); known models: ${known}`,
|
|
8163
|
+
logMessage: `session/set_model rejected sessionId=${params.sessionId} modelId=${JSON.stringify(params.modelId)} agentId=${session.agentId} known=[${known}] (no current model to fall back to)`
|
|
8164
|
+
};
|
|
8165
|
+
}
|
|
8166
|
+
return {
|
|
8167
|
+
kind: "ok",
|
|
8168
|
+
session,
|
|
8169
|
+
logMessage: `session/set_model accepted sessionId=${params.sessionId} modelId=${JSON.stringify(params.modelId)}`
|
|
8170
|
+
};
|
|
8171
|
+
}
|
|
7197
8172
|
function buildResponseMeta(session) {
|
|
7198
8173
|
const ours = {
|
|
7199
8174
|
upstreamSessionId: session.upstreamSessionId,
|
|
@@ -7223,6 +8198,10 @@ function buildResponseMeta(session) {
|
|
|
7223
8198
|
if (modes.length > 0) {
|
|
7224
8199
|
ours.availableModes = modes;
|
|
7225
8200
|
}
|
|
8201
|
+
const models = session.availableModels();
|
|
8202
|
+
if (models.length > 0) {
|
|
8203
|
+
ours.availableModels = models;
|
|
8204
|
+
}
|
|
7226
8205
|
if (session.turnStartedAt !== void 0) {
|
|
7227
8206
|
ours.turnStartedAt = session.turnStartedAt;
|
|
7228
8207
|
}
|
|
@@ -7263,10 +8242,17 @@ function buildInitializeResult() {
|
|
|
7263
8242
|
],
|
|
7264
8243
|
// Advertise hydra-only capabilities via _meta["hydra-acp"]. Generic
|
|
7265
8244
|
// ACP clients ignore the field; capability-aware clients learn here
|
|
7266
|
-
//
|
|
7267
|
-
//
|
|
7268
|
-
//
|
|
7269
|
-
|
|
8245
|
+
// which hydra-acp extensions the daemon supports so they can gate
|
|
8246
|
+
// UI surface accordingly. promptPipelining is false until the
|
|
8247
|
+
// streaming-input probe lands (Option A in the steering brief);
|
|
8248
|
+
// the others are unconditional method-availability flags.
|
|
8249
|
+
_meta: mergeMeta(void 0, {
|
|
8250
|
+
promptQueueing: true,
|
|
8251
|
+
promptCancelling: true,
|
|
8252
|
+
promptUpdating: true,
|
|
8253
|
+
promptAmending: true,
|
|
8254
|
+
promptPipelining: false
|
|
8255
|
+
})
|
|
7270
8256
|
};
|
|
7271
8257
|
}
|
|
7272
8258
|
function bindClientToSession(connection, session, state, clientInfo, callerClientId) {
|