@hydra-acp/cli 0.1.25 → 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 +1100 -92
- package/dist/index.d.ts +87 -0
- package/dist/index.js +721 -66
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -483,12 +483,35 @@ function extractHydraMeta(meta) {
|
|
|
483
483
|
out.availableModes = modes;
|
|
484
484
|
}
|
|
485
485
|
}
|
|
486
|
+
if (Array.isArray(obj.availableModels)) {
|
|
487
|
+
const models = [];
|
|
488
|
+
for (const raw of obj.availableModels) {
|
|
489
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
const m = raw;
|
|
493
|
+
if (typeof m.modelId !== "string") {
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
const model = { modelId: m.modelId };
|
|
497
|
+
if (typeof m.name === "string") {
|
|
498
|
+
model.name = m.name;
|
|
499
|
+
}
|
|
500
|
+
if (typeof m.description === "string") {
|
|
501
|
+
model.description = m.description;
|
|
502
|
+
}
|
|
503
|
+
models.push(model);
|
|
504
|
+
}
|
|
505
|
+
if (models.length > 0) {
|
|
506
|
+
out.availableModels = models;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
486
509
|
return out;
|
|
487
510
|
}
|
|
488
511
|
function mergeMeta(passthrough, ours) {
|
|
489
512
|
return { ...passthrough ?? {}, [HYDRA_META_KEY]: ours };
|
|
490
513
|
}
|
|
491
|
-
var ACP_PROTOCOL_VERSION, JsonRpcErrorCodes, InitializeParams, HistoryPolicy, SessionNewParams, SessionResumeHints, SessionAttachParams, HYDRA_META_KEY, SessionDetachParams, SessionListParams, SessionListUsage, SessionListEntry, SessionListResult, SessionPromptParams, SessionCancelParams, PromptOriginatorSchema, PromptQueueAddedParams, PromptQueueUpdatedParams, PromptQueueRemovedParams, CancelPromptParams, CancelPromptResult, UpdatePromptParams, UpdatePromptResult, AmendPromptParams, AmendPromptResult, PromptAmendedParams, ProxyInitializeParams;
|
|
514
|
+
var ACP_PROTOCOL_VERSION, JsonRpcErrorCodes, InitializeParams, HistoryPolicy, SessionNewParams, SessionResumeHints, SessionAttachParams, HYDRA_META_KEY, SessionDetachParams, SessionListParams, SessionListUsage, SessionListEntry, SessionListResult, SessionPromptParams, SessionCancelParams, PromptOriginatorSchema, PromptQueueAddedParams, PromptQueueUpdatedParams, PromptQueueRemovedParams, CancelPromptParams, CancelPromptResult, UpdatePromptParams, UpdatePromptResult, AmendPromptParams, AmendPromptResult, PromptAmendedParams, AgentInstallProgressParams, AGENT_INSTALL_PROGRESS_METHOD, ProxyInitializeParams;
|
|
492
515
|
var init_types = __esm({
|
|
493
516
|
"src/acp/types.ts"() {
|
|
494
517
|
"use strict";
|
|
@@ -682,6 +705,23 @@ var init_types = __esm({
|
|
|
682
705
|
originator: PromptOriginatorSchema,
|
|
683
706
|
amendedAt: z3.number()
|
|
684
707
|
});
|
|
708
|
+
AgentInstallProgressParams = z3.object({
|
|
709
|
+
agentId: z3.string(),
|
|
710
|
+
version: z3.string(),
|
|
711
|
+
source: z3.enum(["binary", "npm"]),
|
|
712
|
+
phase: z3.enum([
|
|
713
|
+
"download_start",
|
|
714
|
+
"download_progress",
|
|
715
|
+
"download_done",
|
|
716
|
+
"extract",
|
|
717
|
+
"install_start",
|
|
718
|
+
"installed"
|
|
719
|
+
]),
|
|
720
|
+
receivedBytes: z3.number().optional(),
|
|
721
|
+
totalBytes: z3.number().optional(),
|
|
722
|
+
packageSpec: z3.string().optional()
|
|
723
|
+
});
|
|
724
|
+
AGENT_INSTALL_PROGRESS_METHOD = "hydra-acp/agent_install_progress";
|
|
685
725
|
ProxyInitializeParams = z3.object({
|
|
686
726
|
protocolVersion: z3.number().optional(),
|
|
687
727
|
proxyInfo: z3.object({
|
|
@@ -1020,6 +1060,42 @@ function sameAdvertisedModes(a, b) {
|
|
|
1020
1060
|
}
|
|
1021
1061
|
return true;
|
|
1022
1062
|
}
|
|
1063
|
+
function sameAdvertisedModels(a, b) {
|
|
1064
|
+
if (a.length !== b.length) {
|
|
1065
|
+
return false;
|
|
1066
|
+
}
|
|
1067
|
+
for (let i = 0; i < a.length; i++) {
|
|
1068
|
+
if (a[i]?.modelId !== b[i]?.modelId || a[i]?.name !== b[i]?.name || a[i]?.description !== b[i]?.description) {
|
|
1069
|
+
return false;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
return true;
|
|
1073
|
+
}
|
|
1074
|
+
function parseModelsList(list) {
|
|
1075
|
+
if (!Array.isArray(list)) {
|
|
1076
|
+
return [];
|
|
1077
|
+
}
|
|
1078
|
+
const out = [];
|
|
1079
|
+
for (const raw of list) {
|
|
1080
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
1081
|
+
continue;
|
|
1082
|
+
}
|
|
1083
|
+
const r = raw;
|
|
1084
|
+
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;
|
|
1085
|
+
if (!modelId) {
|
|
1086
|
+
continue;
|
|
1087
|
+
}
|
|
1088
|
+
const model = { modelId };
|
|
1089
|
+
if (typeof r.name === "string" && r.name.length > 0) {
|
|
1090
|
+
model.name = r.name;
|
|
1091
|
+
}
|
|
1092
|
+
if (typeof r.description === "string" && r.description.length > 0) {
|
|
1093
|
+
model.description = r.description;
|
|
1094
|
+
}
|
|
1095
|
+
out.push(model);
|
|
1096
|
+
}
|
|
1097
|
+
return out;
|
|
1098
|
+
}
|
|
1023
1099
|
function extractAdvertisedModes(params) {
|
|
1024
1100
|
const obj = params ?? {};
|
|
1025
1101
|
const update = obj.update ?? {};
|
|
@@ -1306,11 +1382,19 @@ var init_session = __esm({
|
|
|
1306
1382
|
// Last available_modes_update we observed from the agent. Same
|
|
1307
1383
|
// pattern as commands: cache, persist, broadcast on change.
|
|
1308
1384
|
agentAdvertisedModes = [];
|
|
1385
|
+
// Last availableModels payload we observed (from current_model_update,
|
|
1386
|
+
// a session/new / session/load response, or — for opencode — a
|
|
1387
|
+
// config_option_update where configOptions[i].id === "model").
|
|
1388
|
+
// Cached so a mid-session attach can synthesize a model picker
|
|
1389
|
+
// snapshot, and so session/set_model can validate the requested id
|
|
1390
|
+
// against what the agent claims to support.
|
|
1391
|
+
agentAdvertisedModels = [];
|
|
1309
1392
|
// Persist hooks for snapshot-shaped state. SessionManager hooks these
|
|
1310
1393
|
// to mirror changes into meta.json so cold-resurrect attaches can
|
|
1311
1394
|
// surface the latest snapshot via the attach response _meta.
|
|
1312
1395
|
agentCommandsHandlers = [];
|
|
1313
1396
|
agentModesHandlers = [];
|
|
1397
|
+
agentModelsHandlers = [];
|
|
1314
1398
|
modelHandlers = [];
|
|
1315
1399
|
modeHandlers = [];
|
|
1316
1400
|
usageHandlers = [];
|
|
@@ -1346,6 +1430,9 @@ var init_session = __esm({
|
|
|
1346
1430
|
if (init.agentModes && init.agentModes.length > 0) {
|
|
1347
1431
|
this.agentAdvertisedModes = [...init.agentModes];
|
|
1348
1432
|
}
|
|
1433
|
+
if (init.agentModels && init.agentModels.length > 0) {
|
|
1434
|
+
this.agentAdvertisedModels = [...init.agentModels];
|
|
1435
|
+
}
|
|
1349
1436
|
this.idleTimeoutMs = init.idleTimeoutMs ?? 0;
|
|
1350
1437
|
this.spawnReplacementAgent = init.spawnReplacementAgent;
|
|
1351
1438
|
this.logger = init.logger;
|
|
@@ -1383,6 +1470,23 @@ var init_session = __esm({
|
|
|
1383
1470
|
}
|
|
1384
1471
|
});
|
|
1385
1472
|
}
|
|
1473
|
+
// Re-broadcast our cached availableModels via current_model_update.
|
|
1474
|
+
// Spec shape: { currentModel, availableModels } — we only include the
|
|
1475
|
+
// currentModel field when we know it, so this broadcast can also fire
|
|
1476
|
+
// model-list updates standalone before any current model is set.
|
|
1477
|
+
broadcastAvailableModels() {
|
|
1478
|
+
const update = {
|
|
1479
|
+
sessionUpdate: "current_model_update",
|
|
1480
|
+
availableModels: [...this.agentAdvertisedModels]
|
|
1481
|
+
};
|
|
1482
|
+
if (this.currentModel !== void 0 && this.currentModel.length > 0) {
|
|
1483
|
+
update.currentModel = this.currentModel;
|
|
1484
|
+
}
|
|
1485
|
+
this.recordAndBroadcast("session/update", {
|
|
1486
|
+
sessionId: this.upstreamSessionId,
|
|
1487
|
+
update
|
|
1488
|
+
});
|
|
1489
|
+
}
|
|
1386
1490
|
// Register session/update, session/request_permission, and onExit
|
|
1387
1491
|
// handlers on an agent connection. Re-run on every /hydra agent so
|
|
1388
1492
|
// the new agent is plumbed identically. The exit handler's identity
|
|
@@ -1413,6 +1517,10 @@ var init_session = __esm({
|
|
|
1413
1517
|
this.recordAndBroadcast("session/update", params);
|
|
1414
1518
|
return;
|
|
1415
1519
|
}
|
|
1520
|
+
if (this.maybeApplyAgentConfigOption(params)) {
|
|
1521
|
+
this.recordAndBroadcast("session/update", params);
|
|
1522
|
+
return;
|
|
1523
|
+
}
|
|
1416
1524
|
if (this.maybeApplyAgentUsage(params)) {
|
|
1417
1525
|
this.recordAndBroadcast("session/update", params);
|
|
1418
1526
|
return;
|
|
@@ -1561,16 +1669,19 @@ var init_session = __esm({
|
|
|
1561
1669
|
recordedAt
|
|
1562
1670
|
});
|
|
1563
1671
|
}
|
|
1564
|
-
if (this.currentModel !== void 0 && this.currentModel.length > 0) {
|
|
1672
|
+
if (this.currentModel !== void 0 && this.currentModel.length > 0 || this.agentAdvertisedModels.length > 0) {
|
|
1673
|
+
const update = {
|
|
1674
|
+
sessionUpdate: "current_model_update"
|
|
1675
|
+
};
|
|
1676
|
+
if (this.currentModel !== void 0 && this.currentModel.length > 0) {
|
|
1677
|
+
update.currentModel = this.currentModel;
|
|
1678
|
+
}
|
|
1679
|
+
if (this.agentAdvertisedModels.length > 0) {
|
|
1680
|
+
update.availableModels = [...this.agentAdvertisedModels];
|
|
1681
|
+
}
|
|
1565
1682
|
out.push({
|
|
1566
1683
|
method: "session/update",
|
|
1567
|
-
params: {
|
|
1568
|
-
sessionId,
|
|
1569
|
-
update: {
|
|
1570
|
-
sessionUpdate: "current_model_update",
|
|
1571
|
-
currentModel: this.currentModel
|
|
1572
|
-
}
|
|
1573
|
-
},
|
|
1684
|
+
params: { sessionId, update },
|
|
1574
1685
|
recordedAt
|
|
1575
1686
|
});
|
|
1576
1687
|
}
|
|
@@ -2205,6 +2316,18 @@ var init_session = __esm({
|
|
|
2205
2316
|
onTitleChange(handler) {
|
|
2206
2317
|
this.titleHandlers.push(handler);
|
|
2207
2318
|
}
|
|
2319
|
+
// External entry point for retitling a live session from outside the
|
|
2320
|
+
// ACP slash-command path (e.g. PATCH /v1/sessions/:id from the picker).
|
|
2321
|
+
// Goes through the same enqueuePrompt path as /hydra title so it
|
|
2322
|
+
// serializes after any in-flight turn and shares broadcast/persistence.
|
|
2323
|
+
retitle(title) {
|
|
2324
|
+
return this.runTitleCommand(title);
|
|
2325
|
+
}
|
|
2326
|
+
// External entry point for the LLM-regen title path (T in the picker,
|
|
2327
|
+
// equivalent to bare /hydra title with no arg).
|
|
2328
|
+
retitleFromAgent() {
|
|
2329
|
+
return this.runTitleCommand("");
|
|
2330
|
+
}
|
|
2208
2331
|
// Update the canonical title and broadcast a session_info_update to
|
|
2209
2332
|
// every attached client. Clients that already speak the spec's
|
|
2210
2333
|
// session_info_update need no hydra-specific wiring to pick this up.
|
|
@@ -2252,12 +2375,19 @@ var init_session = __esm({
|
|
|
2252
2375
|
// Apply an agent-emitted current_model_update. Returns true if the
|
|
2253
2376
|
// notification was a model update (caller still needs to broadcast
|
|
2254
2377
|
// it). Returns false otherwise so the caller can try the next kind.
|
|
2378
|
+
// current_model_update can carry availableModels in the same payload
|
|
2379
|
+
// (per ACP spec); we cache that list too so session/set_model can
|
|
2380
|
+
// validate against it.
|
|
2255
2381
|
maybeApplyAgentModel(params) {
|
|
2256
2382
|
const obj = params ?? {};
|
|
2257
2383
|
const update = obj.update ?? {};
|
|
2258
2384
|
if (update.sessionUpdate !== "current_model_update") {
|
|
2259
2385
|
return false;
|
|
2260
2386
|
}
|
|
2387
|
+
const models = parseModelsList(update.availableModels);
|
|
2388
|
+
if (models.length > 0) {
|
|
2389
|
+
this.setAgentAdvertisedModels(models);
|
|
2390
|
+
}
|
|
2261
2391
|
const raw = typeof update.currentModel === "string" ? update.currentModel : typeof update.model === "string" ? update.model : void 0;
|
|
2262
2392
|
if (raw === void 0) {
|
|
2263
2393
|
return true;
|
|
@@ -2275,6 +2405,55 @@ var init_session = __esm({
|
|
|
2275
2405
|
}
|
|
2276
2406
|
return true;
|
|
2277
2407
|
}
|
|
2408
|
+
// Apply an opencode-style config_option_update. opencode emits this
|
|
2409
|
+
// (not the spec-shaped current_model_update / available_models_update)
|
|
2410
|
+
// to carry both the current model and the list of available models.
|
|
2411
|
+
// The payload is `configOptions: [{ id: "model", currentValue, options:
|
|
2412
|
+
// [{ value, name }] }, ...]`. We harvest only the entry whose id is
|
|
2413
|
+
// "model" — other ids ("mode", "effort", etc.) are opencode-internal
|
|
2414
|
+
// and not consumed by hydra. Returns true when we recognized and
|
|
2415
|
+
// handled the notification so the wireAgent loop can stop trying
|
|
2416
|
+
// further extractors (the broadcast still fires; clients that grok
|
|
2417
|
+
// config_option_update render it directly).
|
|
2418
|
+
maybeApplyAgentConfigOption(params) {
|
|
2419
|
+
const obj = params ?? {};
|
|
2420
|
+
const update = obj.update ?? {};
|
|
2421
|
+
if (update.sessionUpdate !== "config_option_update") {
|
|
2422
|
+
return false;
|
|
2423
|
+
}
|
|
2424
|
+
const list = update.configOptions;
|
|
2425
|
+
if (!Array.isArray(list)) {
|
|
2426
|
+
return true;
|
|
2427
|
+
}
|
|
2428
|
+
for (const raw of list) {
|
|
2429
|
+
if (!raw || typeof raw !== "object") {
|
|
2430
|
+
continue;
|
|
2431
|
+
}
|
|
2432
|
+
const opt = raw;
|
|
2433
|
+
if (opt.id !== "model") {
|
|
2434
|
+
continue;
|
|
2435
|
+
}
|
|
2436
|
+
const models = parseModelsList(opt.options);
|
|
2437
|
+
if (models.length > 0) {
|
|
2438
|
+
this.setAgentAdvertisedModels(models);
|
|
2439
|
+
}
|
|
2440
|
+
const cv = opt.currentValue;
|
|
2441
|
+
if (typeof cv === "string") {
|
|
2442
|
+
const trimmed = cv.trim();
|
|
2443
|
+
if (trimmed && trimmed !== this.currentModel) {
|
|
2444
|
+
this.currentModel = trimmed;
|
|
2445
|
+
for (const handler of this.modelHandlers) {
|
|
2446
|
+
try {
|
|
2447
|
+
handler(trimmed);
|
|
2448
|
+
} catch {
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
break;
|
|
2454
|
+
}
|
|
2455
|
+
return true;
|
|
2456
|
+
}
|
|
2278
2457
|
maybeApplyAgentMode(params) {
|
|
2279
2458
|
const obj = params ?? {};
|
|
2280
2459
|
const update = obj.update ?? {};
|
|
@@ -2373,6 +2552,20 @@ var init_session = __esm({
|
|
|
2373
2552
|
}
|
|
2374
2553
|
this.broadcastAvailableModes();
|
|
2375
2554
|
}
|
|
2555
|
+
setAgentAdvertisedModels(models) {
|
|
2556
|
+
if (sameAdvertisedModels(this.agentAdvertisedModels, models)) {
|
|
2557
|
+
this.broadcastAvailableModels();
|
|
2558
|
+
return;
|
|
2559
|
+
}
|
|
2560
|
+
this.agentAdvertisedModels = models;
|
|
2561
|
+
for (const handler of this.agentModelsHandlers) {
|
|
2562
|
+
try {
|
|
2563
|
+
handler(models);
|
|
2564
|
+
} catch {
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
this.broadcastAvailableModels();
|
|
2568
|
+
}
|
|
2376
2569
|
// Subscribe to snapshot-state updates. SessionManager wires these to
|
|
2377
2570
|
// persist the new value into meta.json so cold resurrect can restore
|
|
2378
2571
|
// them via the attach response _meta.
|
|
@@ -2382,6 +2575,9 @@ var init_session = __esm({
|
|
|
2382
2575
|
onAgentModesChange(handler) {
|
|
2383
2576
|
this.agentModesHandlers.push(handler);
|
|
2384
2577
|
}
|
|
2578
|
+
onAgentModelsChange(handler) {
|
|
2579
|
+
this.agentModelsHandlers.push(handler);
|
|
2580
|
+
}
|
|
2385
2581
|
onModelChange(handler) {
|
|
2386
2582
|
this.modelHandlers.push(handler);
|
|
2387
2583
|
}
|
|
@@ -2407,6 +2603,15 @@ var init_session = __esm({
|
|
|
2407
2603
|
availableModes() {
|
|
2408
2604
|
return [...this.agentAdvertisedModes];
|
|
2409
2605
|
}
|
|
2606
|
+
// The agent's advertised models list. Used by acp-ws.ts's dedicated
|
|
2607
|
+
// session/set_model handler to validate the requested modelId before
|
|
2608
|
+
// forwarding to the agent (catches cross-agent set_model storms from
|
|
2609
|
+
// clients that assume a different agent is on the other end). When
|
|
2610
|
+
// the agent never advertised any models, returns [] and the
|
|
2611
|
+
// set_model handler falls back to pass-through.
|
|
2612
|
+
availableModels() {
|
|
2613
|
+
return [...this.agentAdvertisedModels];
|
|
2614
|
+
}
|
|
2410
2615
|
// Pick up an agent-emitted session_info_update and store its title
|
|
2411
2616
|
// as our canonical record. The notification is also forwarded to
|
|
2412
2617
|
// clients via the surrounding recordAndBroadcast call. Authoritative
|
|
@@ -2554,6 +2759,12 @@ var init_session = __esm({
|
|
|
2554
2759
|
this.agentMeta = fresh.agentMeta;
|
|
2555
2760
|
this.agentAdvertisedCommands = [];
|
|
2556
2761
|
this.broadcastMergedCommands();
|
|
2762
|
+
if (this.agentAdvertisedModels.length > 0) {
|
|
2763
|
+
this.setAgentAdvertisedModels([]);
|
|
2764
|
+
}
|
|
2765
|
+
if (this.agentAdvertisedModes.length > 0) {
|
|
2766
|
+
this.setAgentAdvertisedModes([]);
|
|
2767
|
+
}
|
|
2557
2768
|
await oldAgent.kill().catch(() => void 0);
|
|
2558
2769
|
if (transcript) {
|
|
2559
2770
|
await this.runInternalPrompt(transcript).catch(() => void 0);
|
|
@@ -3120,11 +3331,12 @@ function recordFromMemorySession(args) {
|
|
|
3120
3331
|
currentUsage: args.currentUsage,
|
|
3121
3332
|
agentCommands: args.agentCommands,
|
|
3122
3333
|
agentModes: args.agentModes,
|
|
3334
|
+
agentModels: args.agentModels,
|
|
3123
3335
|
createdAt: args.createdAt ?? now,
|
|
3124
3336
|
updatedAt: args.updatedAt ?? now
|
|
3125
3337
|
};
|
|
3126
3338
|
}
|
|
3127
|
-
var HYDRA_ID_ALPHABET2, generateRawId, HYDRA_LINEAGE_PREFIX, PersistedAgentCommand, PersistedAgentMode, PersistedUsage, SessionRecord, SESSION_ID_PATTERN, SessionStore;
|
|
3339
|
+
var HYDRA_ID_ALPHABET2, generateRawId, HYDRA_LINEAGE_PREFIX, PersistedAgentCommand, PersistedAgentMode, PersistedAgentModel, PersistedUsage, SessionRecord, SESSION_ID_PATTERN, SessionStore;
|
|
3128
3340
|
var init_session_store = __esm({
|
|
3129
3341
|
"src/core/session-store.ts"() {
|
|
3130
3342
|
"use strict";
|
|
@@ -3141,6 +3353,11 @@ var init_session_store = __esm({
|
|
|
3141
3353
|
name: z4.string().optional(),
|
|
3142
3354
|
description: z4.string().optional()
|
|
3143
3355
|
});
|
|
3356
|
+
PersistedAgentModel = z4.object({
|
|
3357
|
+
modelId: z4.string(),
|
|
3358
|
+
name: z4.string().optional(),
|
|
3359
|
+
description: z4.string().optional()
|
|
3360
|
+
});
|
|
3144
3361
|
PersistedUsage = z4.object({
|
|
3145
3362
|
used: z4.number().optional(),
|
|
3146
3363
|
size: z4.number().optional(),
|
|
@@ -3187,6 +3404,7 @@ var init_session_store = __esm({
|
|
|
3187
3404
|
currentUsage: PersistedUsage.optional(),
|
|
3188
3405
|
agentCommands: z4.array(PersistedAgentCommand).optional(),
|
|
3189
3406
|
agentModes: z4.array(PersistedAgentMode).optional(),
|
|
3407
|
+
agentModels: z4.array(PersistedAgentModel).optional(),
|
|
3190
3408
|
createdAt: z4.string(),
|
|
3191
3409
|
updatedAt: z4.string()
|
|
3192
3410
|
});
|
|
@@ -3682,8 +3900,55 @@ function mapToolCallUpdate(u) {
|
|
|
3682
3900
|
if (status !== void 0) {
|
|
3683
3901
|
event.status = status;
|
|
3684
3902
|
}
|
|
3903
|
+
if (status === "failed") {
|
|
3904
|
+
const errorText = extractToolFailureText(u);
|
|
3905
|
+
if (errorText !== null) {
|
|
3906
|
+
event.errorText = errorText;
|
|
3907
|
+
}
|
|
3908
|
+
if (isUpstreamInterrupted(u, errorText)) {
|
|
3909
|
+
event.upstreamInterrupted = true;
|
|
3910
|
+
}
|
|
3911
|
+
}
|
|
3685
3912
|
return event;
|
|
3686
3913
|
}
|
|
3914
|
+
function extractToolFailureText(u) {
|
|
3915
|
+
const content = u.content;
|
|
3916
|
+
if (Array.isArray(content)) {
|
|
3917
|
+
for (const block of content) {
|
|
3918
|
+
if (!block || typeof block !== "object") {
|
|
3919
|
+
continue;
|
|
3920
|
+
}
|
|
3921
|
+
const b = block;
|
|
3922
|
+
const text = extractContentText(b.content);
|
|
3923
|
+
if (text !== null && text.length > 0) {
|
|
3924
|
+
return text;
|
|
3925
|
+
}
|
|
3926
|
+
}
|
|
3927
|
+
}
|
|
3928
|
+
const rawOutput = u.rawOutput;
|
|
3929
|
+
if (rawOutput && typeof rawOutput === "object") {
|
|
3930
|
+
const err = rawOutput.error;
|
|
3931
|
+
if (typeof err === "string" && err.length > 0) {
|
|
3932
|
+
return sanitizeWireText(err);
|
|
3933
|
+
}
|
|
3934
|
+
}
|
|
3935
|
+
return null;
|
|
3936
|
+
}
|
|
3937
|
+
function isUpstreamInterrupted(u, errorText) {
|
|
3938
|
+
const rawOutput = u.rawOutput;
|
|
3939
|
+
if (rawOutput && typeof rawOutput === "object") {
|
|
3940
|
+
const meta = rawOutput.metadata;
|
|
3941
|
+
if (meta && typeof meta === "object") {
|
|
3942
|
+
if (meta.interrupted === true) {
|
|
3943
|
+
return true;
|
|
3944
|
+
}
|
|
3945
|
+
}
|
|
3946
|
+
}
|
|
3947
|
+
if (errorText !== null && errorText.toLowerCase().includes("tool execution aborted")) {
|
|
3948
|
+
return true;
|
|
3949
|
+
}
|
|
3950
|
+
return false;
|
|
3951
|
+
}
|
|
3687
3952
|
function mapPlan(u) {
|
|
3688
3953
|
const entries = u.entries;
|
|
3689
3954
|
if (!Array.isArray(entries)) {
|
|
@@ -5069,6 +5334,34 @@ async function killSession(config, serviceToken, id, fetchImpl = fetch) {
|
|
|
5069
5334
|
throw new Error(`daemon returned HTTP ${response.status}`);
|
|
5070
5335
|
}
|
|
5071
5336
|
}
|
|
5337
|
+
async function renameSession(config, serviceToken, id, title, fetchImpl = fetch) {
|
|
5338
|
+
const base = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
5339
|
+
const response = await fetchImpl(`${base}/v1/sessions/${id}`, {
|
|
5340
|
+
method: "PATCH",
|
|
5341
|
+
headers: {
|
|
5342
|
+
Authorization: `Bearer ${serviceToken}`,
|
|
5343
|
+
"Content-Type": "application/json"
|
|
5344
|
+
},
|
|
5345
|
+
body: JSON.stringify({ title })
|
|
5346
|
+
});
|
|
5347
|
+
if (!response.ok && response.status !== 204 && response.status !== 404) {
|
|
5348
|
+
throw new Error(`daemon returned HTTP ${response.status}`);
|
|
5349
|
+
}
|
|
5350
|
+
}
|
|
5351
|
+
async function regenSessionTitle(config, serviceToken, id, fetchImpl = fetch) {
|
|
5352
|
+
const base = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
5353
|
+
const response = await fetchImpl(`${base}/v1/sessions/${id}`, {
|
|
5354
|
+
method: "PATCH",
|
|
5355
|
+
headers: {
|
|
5356
|
+
Authorization: `Bearer ${serviceToken}`,
|
|
5357
|
+
"Content-Type": "application/json"
|
|
5358
|
+
},
|
|
5359
|
+
body: JSON.stringify({ regen: true })
|
|
5360
|
+
});
|
|
5361
|
+
if (!response.ok && response.status !== 202 && response.status !== 204 && response.status !== 404 && response.status !== 409) {
|
|
5362
|
+
throw new Error(`daemon returned HTTP ${response.status}`);
|
|
5363
|
+
}
|
|
5364
|
+
}
|
|
5072
5365
|
async function deleteSession(config, serviceToken, id, fetchImpl = fetch) {
|
|
5073
5366
|
const base = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
|
|
5074
5367
|
const response = await fetchImpl(`${base}/v1/sessions/${id}`, {
|
|
@@ -5139,6 +5432,7 @@ async function pickSession(term, opts) {
|
|
|
5139
5432
|
let cwdOnly = false;
|
|
5140
5433
|
let mode = "normal";
|
|
5141
5434
|
let pendingAction = null;
|
|
5435
|
+
let renameBuffer = "";
|
|
5142
5436
|
let transientStatus = null;
|
|
5143
5437
|
let termHeight = readTermHeight(term);
|
|
5144
5438
|
let termWidth = readTermWidth(term);
|
|
@@ -5247,6 +5541,12 @@ async function pickSession(term, opts) {
|
|
|
5247
5541
|
term.dim.noFormat(` working on ${shortId2(pendingAction.sessionId)}\u2026`);
|
|
5248
5542
|
return;
|
|
5249
5543
|
}
|
|
5544
|
+
if (mode === "rename" && pendingAction) {
|
|
5545
|
+
term.brightYellow.noFormat(` title: ${renameBuffer}`);
|
|
5546
|
+
term.bgBrightYellow(" ");
|
|
5547
|
+
term.dim.noFormat(" Enter saves \xB7 Esc cancels");
|
|
5548
|
+
return;
|
|
5549
|
+
}
|
|
5250
5550
|
if (transientStatus !== null) {
|
|
5251
5551
|
term.dim.noFormat(` ${transientStatus}`);
|
|
5252
5552
|
return;
|
|
@@ -5364,6 +5664,37 @@ async function pickSession(term, opts) {
|
|
|
5364
5664
|
renderFromScratch();
|
|
5365
5665
|
}
|
|
5366
5666
|
};
|
|
5667
|
+
const performRename = async (title) => {
|
|
5668
|
+
if (!pendingAction) {
|
|
5669
|
+
return;
|
|
5670
|
+
}
|
|
5671
|
+
const target = pendingAction;
|
|
5672
|
+
mode = "busy";
|
|
5673
|
+
paintIndicator();
|
|
5674
|
+
try {
|
|
5675
|
+
await renameSession(opts.config, opts.serviceToken, target.sessionId, title);
|
|
5676
|
+
mode = "normal";
|
|
5677
|
+
pendingAction = null;
|
|
5678
|
+
renameBuffer = "";
|
|
5679
|
+
await refresh(target.sessionId);
|
|
5680
|
+
} catch (err) {
|
|
5681
|
+
mode = "normal";
|
|
5682
|
+
pendingAction = null;
|
|
5683
|
+
renameBuffer = "";
|
|
5684
|
+
transientStatus = `rename failed: ${err.message}`;
|
|
5685
|
+
paintIndicator();
|
|
5686
|
+
}
|
|
5687
|
+
};
|
|
5688
|
+
const performRegen = async (target) => {
|
|
5689
|
+
try {
|
|
5690
|
+
await regenSessionTitle(opts.config, opts.serviceToken, target.sessionId);
|
|
5691
|
+
transientStatus = "title regen queued (press r to refresh)";
|
|
5692
|
+
paintIndicator();
|
|
5693
|
+
} catch (err) {
|
|
5694
|
+
transientStatus = `regen failed: ${err.message}`;
|
|
5695
|
+
paintIndicator();
|
|
5696
|
+
}
|
|
5697
|
+
};
|
|
5367
5698
|
const performAction = async (kind) => {
|
|
5368
5699
|
if (!pendingAction) {
|
|
5369
5700
|
return;
|
|
@@ -5436,6 +5767,52 @@ async function pickSession(term, opts) {
|
|
|
5436
5767
|
renderFromScratch();
|
|
5437
5768
|
return;
|
|
5438
5769
|
}
|
|
5770
|
+
if (mode === "rename") {
|
|
5771
|
+
if (name === "ENTER" || name === "KP_ENTER") {
|
|
5772
|
+
const trimmed = renameBuffer.trim();
|
|
5773
|
+
if (trimmed.length === 0) {
|
|
5774
|
+
mode = "normal";
|
|
5775
|
+
pendingAction = null;
|
|
5776
|
+
renameBuffer = "";
|
|
5777
|
+
paintIndicator();
|
|
5778
|
+
return;
|
|
5779
|
+
}
|
|
5780
|
+
void performRename(trimmed);
|
|
5781
|
+
return;
|
|
5782
|
+
}
|
|
5783
|
+
if (name === "ESCAPE" || name === "CTRL_C") {
|
|
5784
|
+
mode = "normal";
|
|
5785
|
+
pendingAction = null;
|
|
5786
|
+
renameBuffer = "";
|
|
5787
|
+
paintIndicator();
|
|
5788
|
+
return;
|
|
5789
|
+
}
|
|
5790
|
+
if (name === "BACKSPACE") {
|
|
5791
|
+
if (renameBuffer.length > 0) {
|
|
5792
|
+
renameBuffer = renameBuffer.slice(0, -1);
|
|
5793
|
+
paintIndicator();
|
|
5794
|
+
}
|
|
5795
|
+
return;
|
|
5796
|
+
}
|
|
5797
|
+
if (name === "CTRL_U") {
|
|
5798
|
+
renameBuffer = "";
|
|
5799
|
+
paintIndicator();
|
|
5800
|
+
return;
|
|
5801
|
+
}
|
|
5802
|
+
if (name === "CTRL_W") {
|
|
5803
|
+
const trimmedRight = renameBuffer.replace(/\s+$/, "");
|
|
5804
|
+
const lastSpace = trimmedRight.lastIndexOf(" ");
|
|
5805
|
+
renameBuffer = lastSpace >= 0 ? trimmedRight.slice(0, lastSpace) : "";
|
|
5806
|
+
paintIndicator();
|
|
5807
|
+
return;
|
|
5808
|
+
}
|
|
5809
|
+
if (data?.isCharacter) {
|
|
5810
|
+
renameBuffer += name;
|
|
5811
|
+
paintIndicator();
|
|
5812
|
+
return;
|
|
5813
|
+
}
|
|
5814
|
+
return;
|
|
5815
|
+
}
|
|
5439
5816
|
if (mode === "confirm-kill" || mode === "confirm-delete") {
|
|
5440
5817
|
if (data?.isCharacter && (name === "y" || name === "Y")) {
|
|
5441
5818
|
const kind = mode === "confirm-kill" ? "kill" : "delete";
|
|
@@ -5538,6 +5915,29 @@ async function pickSession(term, opts) {
|
|
|
5538
5915
|
paintIndicator();
|
|
5539
5916
|
return;
|
|
5540
5917
|
}
|
|
5918
|
+
if (name === "t" && selectedIdx > 0) {
|
|
5919
|
+
const session = visible[selectedIdx - 1];
|
|
5920
|
+
if (!session) {
|
|
5921
|
+
return;
|
|
5922
|
+
}
|
|
5923
|
+
pendingAction = {
|
|
5924
|
+
sessionId: session.sessionId,
|
|
5925
|
+
cwd: session.cwd,
|
|
5926
|
+
status: session.status
|
|
5927
|
+
};
|
|
5928
|
+
renameBuffer = session.title ?? "";
|
|
5929
|
+
mode = "rename";
|
|
5930
|
+
paintIndicator();
|
|
5931
|
+
return;
|
|
5932
|
+
}
|
|
5933
|
+
if (name === "T" && selectedIdx > 0) {
|
|
5934
|
+
const session = visible[selectedIdx - 1];
|
|
5935
|
+
if (!session || session.status !== "live") {
|
|
5936
|
+
return;
|
|
5937
|
+
}
|
|
5938
|
+
void performRegen({ sessionId: session.sessionId });
|
|
5939
|
+
return;
|
|
5940
|
+
}
|
|
5541
5941
|
if ((name === "d" || name === "D") && selectedIdx > 0) {
|
|
5542
5942
|
const session = visible[selectedIdx - 1];
|
|
5543
5943
|
if (!session) {
|
|
@@ -5668,6 +6068,8 @@ var init_picker = __esm({
|
|
|
5668
6068
|
null,
|
|
5669
6069
|
["k", "kill the selected live session"],
|
|
5670
6070
|
["d", "delete the selected cold session"],
|
|
6071
|
+
["t", "retitle the selected session"],
|
|
6072
|
+
["T", "regenerate title via agent (live session)"],
|
|
5671
6073
|
null,
|
|
5672
6074
|
["c", "create new session"],
|
|
5673
6075
|
["?", "toggle this help"],
|
|
@@ -7886,11 +8288,16 @@ var init_screen = __esm({
|
|
|
7886
8288
|
const elapsedStr = this.banner.status === "busy" && this.banner.elapsedMs !== void 0 && this.banner.elapsedMs >= 1e3 ? formatElapsed(this.banner.elapsedMs) : "";
|
|
7887
8289
|
const right = this.bannerRightContent();
|
|
7888
8290
|
const rightSig = right ? `${right.kind}|${right.text}` : "";
|
|
7889
|
-
const
|
|
8291
|
+
const stalled = this.banner.status === "busy" && this.banner.stalled === true;
|
|
8292
|
+
const sig = `bnr|${w}|${this.banner.status}|${elapsedStr}|${stalled ? "1" : "0"}|${this.banner.queued}|${this.scrollOffset}|${this.banner.currentMode ?? ""}|${this.banner.hint}|` + rightSig;
|
|
7890
8293
|
this.paintRow(row, sig, () => {
|
|
7891
8294
|
const dot = this.banner.status === "busy" ? "\u25CF" : "\u25CB";
|
|
7892
8295
|
if (this.banner.status === "busy") {
|
|
7893
|
-
|
|
8296
|
+
if (stalled) {
|
|
8297
|
+
this.term.brightRed(`${dot} stalled`);
|
|
8298
|
+
} else {
|
|
8299
|
+
this.term.brightYellow(`${dot} ${this.banner.status}`);
|
|
8300
|
+
}
|
|
7894
8301
|
if (elapsedStr) {
|
|
7895
8302
|
this.term(" ").dim(elapsedStr);
|
|
7896
8303
|
}
|
|
@@ -9267,8 +9674,29 @@ var init_completion = __esm({
|
|
|
9267
9674
|
}
|
|
9268
9675
|
});
|
|
9269
9676
|
|
|
9677
|
+
// src/tui/reconnect-state.ts
|
|
9678
|
+
function parseReattachResponse(result) {
|
|
9679
|
+
const out = {};
|
|
9680
|
+
if (!result || typeof result !== "object") {
|
|
9681
|
+
return out;
|
|
9682
|
+
}
|
|
9683
|
+
const r = result;
|
|
9684
|
+
if (typeof r.historyPolicy === "string") {
|
|
9685
|
+
out.appliedPolicy = r.historyPolicy;
|
|
9686
|
+
}
|
|
9687
|
+
if (typeof r.clientId === "string" && r.clientId.length > 0) {
|
|
9688
|
+
out.clientId = r.clientId;
|
|
9689
|
+
}
|
|
9690
|
+
return out;
|
|
9691
|
+
}
|
|
9692
|
+
var init_reconnect_state = __esm({
|
|
9693
|
+
"src/tui/reconnect-state.ts"() {
|
|
9694
|
+
"use strict";
|
|
9695
|
+
}
|
|
9696
|
+
});
|
|
9697
|
+
|
|
9270
9698
|
// src/tui/format.ts
|
|
9271
|
-
import
|
|
9699
|
+
import chalk2 from "chalk";
|
|
9272
9700
|
import { highlight, supportsLanguage } from "cli-highlight";
|
|
9273
9701
|
function formatEvent(event) {
|
|
9274
9702
|
switch (event.kind) {
|
|
@@ -9561,12 +9989,22 @@ function formatToolLine2(state) {
|
|
|
9561
9989
|
} else {
|
|
9562
9990
|
title = `${initial} \xB7 ${latest}`;
|
|
9563
9991
|
}
|
|
9564
|
-
|
|
9565
|
-
|
|
9566
|
-
|
|
9567
|
-
|
|
9568
|
-
|
|
9569
|
-
|
|
9992
|
+
const lines = [
|
|
9993
|
+
{
|
|
9994
|
+
prefix: ` ${toolStatusIcon(state.status)} `,
|
|
9995
|
+
prefixStyle: toolIconStyle(state.status),
|
|
9996
|
+
body: title,
|
|
9997
|
+
bodyStyle: toolStatusStyle(state.status)
|
|
9998
|
+
}
|
|
9999
|
+
];
|
|
10000
|
+
if (state.status === "failed" && state.errorText) {
|
|
10001
|
+
lines.push({
|
|
10002
|
+
prefix: " ",
|
|
10003
|
+
body: sanitizeSingleLine(state.errorText),
|
|
10004
|
+
bodyStyle: "tool-status-fail"
|
|
10005
|
+
});
|
|
10006
|
+
}
|
|
10007
|
+
return lines;
|
|
9570
10008
|
}
|
|
9571
10009
|
function toolStatusIcon(status) {
|
|
9572
10010
|
switch (status) {
|
|
@@ -9665,7 +10103,8 @@ var highlightChalk, HIGHLIGHT_THEME;
|
|
|
9665
10103
|
var init_format = __esm({
|
|
9666
10104
|
"src/tui/format.ts"() {
|
|
9667
10105
|
"use strict";
|
|
9668
|
-
|
|
10106
|
+
init_render_update();
|
|
10107
|
+
highlightChalk = new chalk2.Instance({ level: 3 });
|
|
9669
10108
|
HIGHLIGHT_THEME = {
|
|
9670
10109
|
keyword: highlightChalk.blueBright,
|
|
9671
10110
|
built_in: highlightChalk.cyan,
|
|
@@ -9727,8 +10166,9 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
9727
10166
|
term.grabInput(false);
|
|
9728
10167
|
process.exit(0);
|
|
9729
10168
|
}
|
|
9730
|
-
const
|
|
9731
|
-
term
|
|
10169
|
+
const launchLabelBase = ctx.sessionId === "__new__" ? "Starting new session\u2026" : "Resuming session\u2026";
|
|
10170
|
+
const installStatus = createInstallStatusLine(term, launchLabelBase);
|
|
10171
|
+
installStatus.write(launchLabelBase);
|
|
9732
10172
|
const protocol = config.daemon.tls ? "wss" : "ws";
|
|
9733
10173
|
const wsUrl = `${protocol}://${config.daemon.host}:${config.daemon.port}/acp`;
|
|
9734
10174
|
const subprotocols = ["acp.v1", `hydra-acp-token.${serviceToken}`];
|
|
@@ -9754,6 +10194,13 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
9754
10194
|
});
|
|
9755
10195
|
const conn = new JsonRpcConnection(stream);
|
|
9756
10196
|
await stream.start();
|
|
10197
|
+
conn.onNotification(AGENT_INSTALL_PROGRESS_METHOD, (raw) => {
|
|
10198
|
+
const parsed = AgentInstallProgressParams.safeParse(raw);
|
|
10199
|
+
if (!parsed.success) {
|
|
10200
|
+
return;
|
|
10201
|
+
}
|
|
10202
|
+
installStatus.applyProgress(parsed.data);
|
|
10203
|
+
});
|
|
9757
10204
|
let bufferedEvents = [];
|
|
9758
10205
|
let applyRenderEvent = null;
|
|
9759
10206
|
let teardownStarted = false;
|
|
@@ -9771,34 +10218,46 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
9771
10218
|
let currentHeadMessageId;
|
|
9772
10219
|
let sessionBusySince = null;
|
|
9773
10220
|
let sessionElapsedTimer = null;
|
|
10221
|
+
let lastUpdateAt = null;
|
|
10222
|
+
let upstreamInterruptedSeen = false;
|
|
9774
10223
|
const adjustPendingTurns = (delta) => {
|
|
9775
10224
|
const before = pendingTurns;
|
|
9776
10225
|
pendingTurns = Math.max(0, pendingTurns + delta);
|
|
9777
10226
|
const screenReady = typeof screenRef !== "undefined" && screenRef !== null;
|
|
9778
10227
|
if (before === 0 && pendingTurns > 0) {
|
|
9779
10228
|
sessionBusySince = Date.now();
|
|
10229
|
+
lastUpdateAt = Date.now();
|
|
9780
10230
|
dispatcherRef?.setTurnRunning(true);
|
|
9781
10231
|
if (screenReady) {
|
|
9782
|
-
screenRef.setBanner({ status: "busy", elapsedMs: 0 });
|
|
10232
|
+
screenRef.setBanner({ status: "busy", elapsedMs: 0, stalled: false });
|
|
9783
10233
|
}
|
|
9784
10234
|
if (sessionElapsedTimer === null && screenReady) {
|
|
9785
10235
|
sessionElapsedTimer = setInterval(() => {
|
|
9786
10236
|
if (sessionBusySince === null || screenRef === null) {
|
|
9787
10237
|
return;
|
|
9788
10238
|
}
|
|
9789
|
-
|
|
10239
|
+
const idleMs = lastUpdateAt === null ? 0 : Date.now() - lastUpdateAt;
|
|
10240
|
+
screenRef.setBanner({
|
|
10241
|
+
elapsedMs: Date.now() - sessionBusySince,
|
|
10242
|
+
stalled: idleMs >= STALL_THRESHOLD_MS
|
|
10243
|
+
});
|
|
9790
10244
|
renderToolsBlock();
|
|
9791
10245
|
}, 1e3);
|
|
9792
10246
|
}
|
|
9793
10247
|
} else if (before > 0 && pendingTurns === 0) {
|
|
9794
10248
|
sessionBusySince = null;
|
|
10249
|
+
lastUpdateAt = null;
|
|
9795
10250
|
dispatcherRef?.setTurnRunning(false);
|
|
9796
10251
|
if (sessionElapsedTimer !== null) {
|
|
9797
10252
|
clearInterval(sessionElapsedTimer);
|
|
9798
10253
|
sessionElapsedTimer = null;
|
|
9799
10254
|
}
|
|
9800
10255
|
if (screenReady) {
|
|
9801
|
-
screenRef.setBanner({
|
|
10256
|
+
screenRef.setBanner({
|
|
10257
|
+
status: "ready",
|
|
10258
|
+
elapsedMs: void 0,
|
|
10259
|
+
stalled: false
|
|
10260
|
+
});
|
|
9802
10261
|
}
|
|
9803
10262
|
}
|
|
9804
10263
|
void delta;
|
|
@@ -9819,6 +10278,7 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
9819
10278
|
const { update } = params ?? {};
|
|
9820
10279
|
const event = mapUpdate(update);
|
|
9821
10280
|
debugLogUpdate(update, event);
|
|
10281
|
+
lastUpdateAt = Date.now();
|
|
9822
10282
|
const rawTag = update?.sessionUpdate;
|
|
9823
10283
|
if (typeof rawTag === "string" && !STATE_UPDATE_KINDS2.has(rawTag)) {
|
|
9824
10284
|
const u = update ?? {};
|
|
@@ -10388,6 +10848,7 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
10388
10848
|
};
|
|
10389
10849
|
const sessionbarAgent = resolvedAgentId || agentInfoName || "?";
|
|
10390
10850
|
const usage = { ...initialUsage ?? {} };
|
|
10851
|
+
installStatus.finalize();
|
|
10391
10852
|
screen.start();
|
|
10392
10853
|
screen.setSessionbar({
|
|
10393
10854
|
agent: sessionbarAgent,
|
|
@@ -11270,7 +11731,7 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
11270
11731
|
for (const id of visibleIds) {
|
|
11271
11732
|
const state = toolStates.get(id);
|
|
11272
11733
|
if (state) {
|
|
11273
|
-
lines.push(formatToolLine2(state));
|
|
11734
|
+
lines.push(...formatToolLine2(state));
|
|
11274
11735
|
}
|
|
11275
11736
|
}
|
|
11276
11737
|
screen.upsertLines("tools", lines);
|
|
@@ -11281,7 +11742,7 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
11281
11742
|
toolsBlockStopReason = null;
|
|
11282
11743
|
renderToolsBlock();
|
|
11283
11744
|
};
|
|
11284
|
-
const recordToolCall = (id, title, status) => {
|
|
11745
|
+
const recordToolCall = (id, title, status, errorText) => {
|
|
11285
11746
|
const wasNew = !toolStates.has(id);
|
|
11286
11747
|
const existing = toolStates.get(id);
|
|
11287
11748
|
const state = existing ?? {
|
|
@@ -11298,6 +11759,9 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
11298
11759
|
if (!existing) {
|
|
11299
11760
|
state.status = status ?? "pending";
|
|
11300
11761
|
}
|
|
11762
|
+
if (errorText !== void 0) {
|
|
11763
|
+
state.errorText = errorText;
|
|
11764
|
+
}
|
|
11301
11765
|
toolStates.set(id, state);
|
|
11302
11766
|
if (wasNew) {
|
|
11303
11767
|
if (toolsBlockStartedAt === null) {
|
|
@@ -11389,7 +11853,7 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
11389
11853
|
}
|
|
11390
11854
|
if (event.kind === "tool-call") {
|
|
11391
11855
|
closeAgentText();
|
|
11392
|
-
recordToolCall(event.toolCallId, event.title, event.status);
|
|
11856
|
+
recordToolCall(event.toolCallId, event.title, event.status, void 0);
|
|
11393
11857
|
renderToolsBlock();
|
|
11394
11858
|
return;
|
|
11395
11859
|
}
|
|
@@ -11404,7 +11868,15 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
11404
11868
|
}
|
|
11405
11869
|
if (event.kind === "tool-call-update") {
|
|
11406
11870
|
closeAgentText();
|
|
11407
|
-
recordToolCall(
|
|
11871
|
+
recordToolCall(
|
|
11872
|
+
event.toolCallId,
|
|
11873
|
+
event.title,
|
|
11874
|
+
event.status,
|
|
11875
|
+
event.errorText
|
|
11876
|
+
);
|
|
11877
|
+
if (event.upstreamInterrupted) {
|
|
11878
|
+
upstreamInterruptedSeen = true;
|
|
11879
|
+
}
|
|
11408
11880
|
renderToolsBlock();
|
|
11409
11881
|
return;
|
|
11410
11882
|
}
|
|
@@ -11418,7 +11890,10 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
11418
11890
|
if (event.kind === "turn-complete") {
|
|
11419
11891
|
currentHeadMessageId = void 0;
|
|
11420
11892
|
closeAgentText();
|
|
11421
|
-
|
|
11893
|
+
let effectiveStopReason = event.amended ? "amended" : event.stopReason;
|
|
11894
|
+
if (!event.amended && upstreamInterruptedSeen && (effectiveStopReason === void 0 || effectiveStopReason === "end_turn")) {
|
|
11895
|
+
effectiveStopReason = "error";
|
|
11896
|
+
}
|
|
11422
11897
|
if (lastPlanEvent !== null && effectiveStopReason !== void 0 && effectiveStopReason !== "end_turn") {
|
|
11423
11898
|
const lines = formatEvent({ ...lastPlanEvent, stopped: true });
|
|
11424
11899
|
if (lines.length > 0) {
|
|
@@ -11448,6 +11923,7 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
11448
11923
|
toolsBlockEndedAt = null;
|
|
11449
11924
|
toolsBlockStopReason = null;
|
|
11450
11925
|
toolsExpanded = false;
|
|
11926
|
+
upstreamInterruptedSeen = false;
|
|
11451
11927
|
screen.ensureSeparator();
|
|
11452
11928
|
}
|
|
11453
11929
|
};
|
|
@@ -11557,9 +12033,10 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
11557
12033
|
if (resp.error) {
|
|
11558
12034
|
throw new Error(resp.error.message);
|
|
11559
12035
|
}
|
|
11560
|
-
const
|
|
11561
|
-
|
|
11562
|
-
|
|
12036
|
+
const fields = parseReattachResponse(resp.result);
|
|
12037
|
+
appliedPolicy = fields.appliedPolicy;
|
|
12038
|
+
if (fields.clientId !== void 0) {
|
|
12039
|
+
ownClientId = fields.clientId;
|
|
11563
12040
|
}
|
|
11564
12041
|
} catch (err) {
|
|
11565
12042
|
attachErr = err;
|
|
@@ -11685,6 +12162,95 @@ function writeDebugLine(payload) {
|
|
|
11685
12162
|
} catch {
|
|
11686
12163
|
}
|
|
11687
12164
|
}
|
|
12165
|
+
function createInstallStatusLine(term, baseLabel) {
|
|
12166
|
+
let finalized = false;
|
|
12167
|
+
let lastText = "";
|
|
12168
|
+
let osc94Active = false;
|
|
12169
|
+
const writeOsc94 = (state) => {
|
|
12170
|
+
if (finalized) {
|
|
12171
|
+
return;
|
|
12172
|
+
}
|
|
12173
|
+
if (state === 3 && osc94Active) {
|
|
12174
|
+
return;
|
|
12175
|
+
}
|
|
12176
|
+
if (state === 0 && !osc94Active) {
|
|
12177
|
+
return;
|
|
12178
|
+
}
|
|
12179
|
+
osc94Active = state === 3;
|
|
12180
|
+
process.stdout.write(`\x1B]9;4;${state}\x1B\\`);
|
|
12181
|
+
};
|
|
12182
|
+
const redraw = (text) => {
|
|
12183
|
+
if (finalized) {
|
|
12184
|
+
return;
|
|
12185
|
+
}
|
|
12186
|
+
process.stdout.write("\r");
|
|
12187
|
+
term.eraseLineAfter();
|
|
12188
|
+
term.brightYellow(text);
|
|
12189
|
+
lastText = text;
|
|
12190
|
+
};
|
|
12191
|
+
const formatProgressText = (event) => {
|
|
12192
|
+
const idVer = `${event.agentId}@${event.version}`;
|
|
12193
|
+
if (event.source === "npm") {
|
|
12194
|
+
if (event.phase === "install_start" || event.phase === "download_start") {
|
|
12195
|
+
return `${baseLabel} installing ${idVer} via npm\u2026`;
|
|
12196
|
+
}
|
|
12197
|
+
if (event.phase === "installed") {
|
|
12198
|
+
return `${baseLabel} ${idVer} installed`;
|
|
12199
|
+
}
|
|
12200
|
+
return `${baseLabel} installing ${idVer} via npm\u2026`;
|
|
12201
|
+
}
|
|
12202
|
+
if (event.phase === "download_start" || event.phase === "download_progress") {
|
|
12203
|
+
const received = event.receivedBytes ?? 0;
|
|
12204
|
+
const total = event.totalBytes ?? 0;
|
|
12205
|
+
const rxMb = (received / 1e6).toFixed(1);
|
|
12206
|
+
if (total > 0) {
|
|
12207
|
+
const totalMb = (total / 1e6).toFixed(1);
|
|
12208
|
+
const pct = Math.min(100, Math.floor(received / total * 100));
|
|
12209
|
+
return `${baseLabel} downloading ${idVer} ${rxMb}/${totalMb} MB (${pct}%)`;
|
|
12210
|
+
}
|
|
12211
|
+
return `${baseLabel} downloading ${idVer} ${rxMb} MB`;
|
|
12212
|
+
}
|
|
12213
|
+
if (event.phase === "download_done") {
|
|
12214
|
+
return `${baseLabel} downloaded ${idVer}, verifying\u2026`;
|
|
12215
|
+
}
|
|
12216
|
+
if (event.phase === "extract") {
|
|
12217
|
+
return `${baseLabel} extracting ${idVer}\u2026`;
|
|
12218
|
+
}
|
|
12219
|
+
if (event.phase === "installed") {
|
|
12220
|
+
return `${baseLabel} ${idVer} installed`;
|
|
12221
|
+
}
|
|
12222
|
+
return lastText || baseLabel;
|
|
12223
|
+
};
|
|
12224
|
+
return {
|
|
12225
|
+
write(text) {
|
|
12226
|
+
if (finalized) {
|
|
12227
|
+
return;
|
|
12228
|
+
}
|
|
12229
|
+
term.brightYellow(text);
|
|
12230
|
+
lastText = text;
|
|
12231
|
+
},
|
|
12232
|
+
applyProgress(event) {
|
|
12233
|
+
if (finalized) {
|
|
12234
|
+
return;
|
|
12235
|
+
}
|
|
12236
|
+
const isActive = event.phase === "download_start" || event.phase === "download_progress" || event.phase === "install_start" || event.phase === "extract" || event.phase === "download_done";
|
|
12237
|
+
if (isActive) {
|
|
12238
|
+
writeOsc94(3);
|
|
12239
|
+
} else if (event.phase === "installed") {
|
|
12240
|
+
writeOsc94(0);
|
|
12241
|
+
}
|
|
12242
|
+
redraw(formatProgressText(event));
|
|
12243
|
+
},
|
|
12244
|
+
finalize() {
|
|
12245
|
+
if (finalized) {
|
|
12246
|
+
return;
|
|
12247
|
+
}
|
|
12248
|
+
finalized = true;
|
|
12249
|
+
writeOsc94(0);
|
|
12250
|
+
process.stdout.write("\n");
|
|
12251
|
+
}
|
|
12252
|
+
};
|
|
12253
|
+
}
|
|
11688
12254
|
function rotateIfBig(target) {
|
|
11689
12255
|
try {
|
|
11690
12256
|
const stat4 = statSync(target);
|
|
@@ -11695,7 +12261,7 @@ function rotateIfBig(target) {
|
|
|
11695
12261
|
} catch {
|
|
11696
12262
|
}
|
|
11697
12263
|
}
|
|
11698
|
-
var HELP_ENTRIES_TAIL, logMaxBytes;
|
|
12264
|
+
var STALL_THRESHOLD_MS, HELP_ENTRIES_TAIL, logMaxBytes;
|
|
11699
12265
|
var init_app = __esm({
|
|
11700
12266
|
"src/tui/app.ts"() {
|
|
11701
12267
|
"use strict";
|
|
@@ -11717,8 +12283,10 @@ var init_app = __esm({
|
|
|
11717
12283
|
init_attachments();
|
|
11718
12284
|
init_clipboard();
|
|
11719
12285
|
init_completion();
|
|
12286
|
+
init_reconnect_state();
|
|
11720
12287
|
init_render_update();
|
|
11721
12288
|
init_format();
|
|
12289
|
+
STALL_THRESHOLD_MS = 12e4;
|
|
11722
12290
|
HELP_ENTRIES_TAIL = [
|
|
11723
12291
|
["Alt+Enter", "newline in prompt"],
|
|
11724
12292
|
["Shift+Tab", "cycle agent modes (plan / accept-edits / etc.)"],
|
|
@@ -11880,6 +12448,7 @@ init_config();
|
|
|
11880
12448
|
init_service_token();
|
|
11881
12449
|
import * as fsp7 from "fs/promises";
|
|
11882
12450
|
import { setTimeout as sleep2 } from "timers/promises";
|
|
12451
|
+
import chalk from "chalk";
|
|
11883
12452
|
|
|
11884
12453
|
// src/daemon/server.ts
|
|
11885
12454
|
init_config();
|
|
@@ -11948,8 +12517,10 @@ async function ensureBinary(args) {
|
|
|
11948
12517
|
}
|
|
11949
12518
|
await downloadAndExtract({
|
|
11950
12519
|
agentId: args.agentId,
|
|
12520
|
+
version: args.version,
|
|
11951
12521
|
archiveUrl: args.target.archive,
|
|
11952
|
-
installDir
|
|
12522
|
+
installDir,
|
|
12523
|
+
onProgress: args.onProgress
|
|
11953
12524
|
});
|
|
11954
12525
|
if (!await fileExists(cmdPath)) {
|
|
11955
12526
|
throw new Error(
|
|
@@ -11969,9 +12540,16 @@ async function downloadAndExtract(args) {
|
|
|
11969
12540
|
const archivePath = await downloadTo({
|
|
11970
12541
|
url: args.archiveUrl,
|
|
11971
12542
|
dir: tempDir,
|
|
11972
|
-
agentId: args.agentId
|
|
12543
|
+
agentId: args.agentId,
|
|
12544
|
+
version: args.version,
|
|
12545
|
+
onProgress: args.onProgress
|
|
11973
12546
|
});
|
|
11974
12547
|
logSink(`hydra-acp: extracting ${args.agentId}`);
|
|
12548
|
+
safeEmit(args.onProgress, {
|
|
12549
|
+
phase: "extract",
|
|
12550
|
+
agentId: args.agentId,
|
|
12551
|
+
version: args.version
|
|
12552
|
+
});
|
|
11975
12553
|
await extract(archivePath, tempDir);
|
|
11976
12554
|
await fsp.unlink(archivePath).catch(() => void 0);
|
|
11977
12555
|
try {
|
|
@@ -11982,16 +12560,35 @@ async function downloadAndExtract(args) {
|
|
|
11982
12560
|
await fsp.rm(tempDir, { recursive: true, force: true }).catch(
|
|
11983
12561
|
() => void 0
|
|
11984
12562
|
);
|
|
12563
|
+
safeEmit(args.onProgress, {
|
|
12564
|
+
phase: "installed",
|
|
12565
|
+
agentId: args.agentId,
|
|
12566
|
+
version: args.version
|
|
12567
|
+
});
|
|
11985
12568
|
return;
|
|
11986
12569
|
}
|
|
11987
12570
|
throw err;
|
|
11988
12571
|
}
|
|
11989
12572
|
logSink(`hydra-acp: installed ${args.agentId} to ${args.installDir}`);
|
|
12573
|
+
safeEmit(args.onProgress, {
|
|
12574
|
+
phase: "installed",
|
|
12575
|
+
agentId: args.agentId,
|
|
12576
|
+
version: args.version
|
|
12577
|
+
});
|
|
11990
12578
|
} catch (err) {
|
|
11991
12579
|
await fsp.rm(tempDir, { recursive: true, force: true }).catch(() => void 0);
|
|
11992
12580
|
throw err;
|
|
11993
12581
|
}
|
|
11994
12582
|
}
|
|
12583
|
+
function safeEmit(cb, event) {
|
|
12584
|
+
if (!cb) {
|
|
12585
|
+
return;
|
|
12586
|
+
}
|
|
12587
|
+
try {
|
|
12588
|
+
cb(event);
|
|
12589
|
+
} catch {
|
|
12590
|
+
}
|
|
12591
|
+
}
|
|
11995
12592
|
async function downloadTo(args) {
|
|
11996
12593
|
const filename = inferArchiveName(args.url);
|
|
11997
12594
|
const dest = path2.join(args.dir, filename);
|
|
@@ -12004,17 +12601,34 @@ async function downloadTo(args) {
|
|
|
12004
12601
|
const total = Number(response.headers.get("content-length") ?? "0");
|
|
12005
12602
|
const out = fs4.createWriteStream(dest);
|
|
12006
12603
|
const nodeStream = Readable.fromWeb(response.body);
|
|
12604
|
+
safeEmit(args.onProgress, {
|
|
12605
|
+
phase: "download_start",
|
|
12606
|
+
agentId: args.agentId,
|
|
12607
|
+
version: args.version,
|
|
12608
|
+
totalBytes: total
|
|
12609
|
+
});
|
|
12007
12610
|
let received = 0;
|
|
12008
|
-
let
|
|
12009
|
-
|
|
12611
|
+
let lastLogEmit = Date.now();
|
|
12612
|
+
let lastCbEmit = 0;
|
|
12613
|
+
const LOG_INTERVAL_MS = 2e3;
|
|
12614
|
+
const CB_INTERVAL_MS = 150;
|
|
12010
12615
|
nodeStream.on("data", (chunk) => {
|
|
12011
12616
|
received += chunk.length;
|
|
12012
12617
|
const now = Date.now();
|
|
12013
|
-
if (now -
|
|
12014
|
-
|
|
12618
|
+
if (now - lastCbEmit >= CB_INTERVAL_MS) {
|
|
12619
|
+
lastCbEmit = now;
|
|
12620
|
+
safeEmit(args.onProgress, {
|
|
12621
|
+
phase: "download_progress",
|
|
12622
|
+
agentId: args.agentId,
|
|
12623
|
+
version: args.version,
|
|
12624
|
+
receivedBytes: received,
|
|
12625
|
+
totalBytes: total
|
|
12626
|
+
});
|
|
12627
|
+
}
|
|
12628
|
+
if (now - lastLogEmit >= LOG_INTERVAL_MS) {
|
|
12629
|
+
lastLogEmit = now;
|
|
12630
|
+
logSink(formatProgress(args.agentId, received, total));
|
|
12015
12631
|
}
|
|
12016
|
-
lastEmit = now;
|
|
12017
|
-
logSink(formatProgress(args.agentId, received, total));
|
|
12018
12632
|
});
|
|
12019
12633
|
await new Promise((resolve5, reject) => {
|
|
12020
12634
|
nodeStream.on("error", reject);
|
|
@@ -12029,6 +12643,13 @@ async function downloadTo(args) {
|
|
|
12029
12643
|
/* done */
|
|
12030
12644
|
true
|
|
12031
12645
|
));
|
|
12646
|
+
safeEmit(args.onProgress, {
|
|
12647
|
+
phase: "download_done",
|
|
12648
|
+
agentId: args.agentId,
|
|
12649
|
+
version: args.version,
|
|
12650
|
+
receivedBytes: received,
|
|
12651
|
+
totalBytes: total
|
|
12652
|
+
});
|
|
12032
12653
|
return dest;
|
|
12033
12654
|
}
|
|
12034
12655
|
function formatProgress(agentId, received, total, done = false) {
|
|
@@ -12128,9 +12749,11 @@ async function ensureNpmPackage(args) {
|
|
|
12128
12749
|
}
|
|
12129
12750
|
await installInto({
|
|
12130
12751
|
agentId: args.agentId,
|
|
12752
|
+
version: args.version,
|
|
12131
12753
|
packageSpec: args.packageSpec,
|
|
12132
12754
|
installDir,
|
|
12133
|
-
registry: args.registry
|
|
12755
|
+
registry: args.registry,
|
|
12756
|
+
onProgress: args.onProgress
|
|
12134
12757
|
});
|
|
12135
12758
|
if (!await fileExists2(binPath)) {
|
|
12136
12759
|
throw new Error(
|
|
@@ -12146,6 +12769,12 @@ async function installInto(args) {
|
|
|
12146
12769
|
logSink2(
|
|
12147
12770
|
`hydra-acp: installing ${args.packageSpec} for ${args.agentId} into ${tempDir}`
|
|
12148
12771
|
);
|
|
12772
|
+
safeEmit2(args.onProgress, {
|
|
12773
|
+
phase: "install_start",
|
|
12774
|
+
agentId: args.agentId,
|
|
12775
|
+
version: args.version,
|
|
12776
|
+
packageSpec: args.packageSpec
|
|
12777
|
+
});
|
|
12149
12778
|
await runNpmInstall({
|
|
12150
12779
|
packageSpec: args.packageSpec,
|
|
12151
12780
|
cwd: tempDir,
|
|
@@ -12159,11 +12788,21 @@ async function installInto(args) {
|
|
|
12159
12788
|
await fsp2.rm(tempDir, { recursive: true, force: true }).catch(
|
|
12160
12789
|
() => void 0
|
|
12161
12790
|
);
|
|
12791
|
+
safeEmit2(args.onProgress, {
|
|
12792
|
+
phase: "installed",
|
|
12793
|
+
agentId: args.agentId,
|
|
12794
|
+
version: args.version
|
|
12795
|
+
});
|
|
12162
12796
|
return;
|
|
12163
12797
|
}
|
|
12164
12798
|
throw err;
|
|
12165
12799
|
}
|
|
12166
12800
|
logSink2(`hydra-acp: installed ${args.agentId} to ${args.installDir}`);
|
|
12801
|
+
safeEmit2(args.onProgress, {
|
|
12802
|
+
phase: "installed",
|
|
12803
|
+
agentId: args.agentId,
|
|
12804
|
+
version: args.version
|
|
12805
|
+
});
|
|
12167
12806
|
} catch (err) {
|
|
12168
12807
|
await fsp2.rm(tempDir, { recursive: true, force: true }).catch(
|
|
12169
12808
|
() => void 0
|
|
@@ -12171,44 +12810,87 @@ async function installInto(args) {
|
|
|
12171
12810
|
throw err;
|
|
12172
12811
|
}
|
|
12173
12812
|
}
|
|
12813
|
+
function safeEmit2(cb, event) {
|
|
12814
|
+
if (!cb) {
|
|
12815
|
+
return;
|
|
12816
|
+
}
|
|
12817
|
+
try {
|
|
12818
|
+
cb(event);
|
|
12819
|
+
} catch {
|
|
12820
|
+
}
|
|
12821
|
+
}
|
|
12822
|
+
var ETXTBSY_RETRIES = 5;
|
|
12823
|
+
var ETXTBSY_BACKOFF_MS = 25;
|
|
12174
12824
|
function runNpmInstall(args) {
|
|
12175
|
-
return
|
|
12176
|
-
|
|
12177
|
-
|
|
12178
|
-
|
|
12179
|
-
|
|
12180
|
-
|
|
12181
|
-
|
|
12182
|
-
|
|
12183
|
-
|
|
12184
|
-
|
|
12185
|
-
|
|
12186
|
-
|
|
12187
|
-
|
|
12188
|
-
|
|
12189
|
-
|
|
12190
|
-
|
|
12191
|
-
|
|
12192
|
-
|
|
12193
|
-
|
|
12194
|
-
|
|
12195
|
-
|
|
12196
|
-
|
|
12197
|
-
child.on("exit", (code, signal) => {
|
|
12198
|
-
if (code === 0) {
|
|
12199
|
-
resolve5();
|
|
12825
|
+
return runNpmInstallOnce(args, 0);
|
|
12826
|
+
}
|
|
12827
|
+
async function runNpmInstallOnce(args, attempt) {
|
|
12828
|
+
try {
|
|
12829
|
+
await new Promise((resolve5, reject) => {
|
|
12830
|
+
const registryArgs = args.registry ? ["--registry", args.registry] : [];
|
|
12831
|
+
let child;
|
|
12832
|
+
try {
|
|
12833
|
+
child = spawn2(
|
|
12834
|
+
"npm",
|
|
12835
|
+
[
|
|
12836
|
+
"install",
|
|
12837
|
+
"--no-audit",
|
|
12838
|
+
"--no-fund",
|
|
12839
|
+
"--silent",
|
|
12840
|
+
...registryArgs,
|
|
12841
|
+
args.packageSpec
|
|
12842
|
+
],
|
|
12843
|
+
{ cwd: args.cwd, stdio: ["ignore", "pipe", "pipe"] }
|
|
12844
|
+
);
|
|
12845
|
+
} catch (err) {
|
|
12846
|
+
reject(err);
|
|
12200
12847
|
return;
|
|
12201
12848
|
}
|
|
12202
|
-
|
|
12203
|
-
|
|
12204
|
-
|
|
12205
|
-
|
|
12206
|
-
|
|
12849
|
+
let stderrTail = "";
|
|
12850
|
+
child.stdout?.on("data", (chunk) => {
|
|
12851
|
+
void chunk;
|
|
12852
|
+
});
|
|
12853
|
+
child.stderr?.setEncoding("utf8");
|
|
12854
|
+
child.stderr?.on("data", (chunk) => {
|
|
12855
|
+
stderrTail = (stderrTail + chunk).slice(-4096);
|
|
12856
|
+
});
|
|
12857
|
+
child.on("error", (err) => {
|
|
12858
|
+
const e = err;
|
|
12859
|
+
if (e.code === "ENOENT") {
|
|
12860
|
+
reject(
|
|
12861
|
+
new Error(
|
|
12862
|
+
`npm not found on PATH (install Node.js / npm, or use a binary-distributed agent)`
|
|
12863
|
+
)
|
|
12864
|
+
);
|
|
12865
|
+
return;
|
|
12866
|
+
}
|
|
12867
|
+
reject(err);
|
|
12868
|
+
});
|
|
12869
|
+
child.on("exit", (code, signal) => {
|
|
12870
|
+
if (code === 0) {
|
|
12871
|
+
resolve5();
|
|
12872
|
+
return;
|
|
12873
|
+
}
|
|
12874
|
+
const reason = code !== null ? `exit code ${code}` : `signal ${signal ?? "unknown"}`;
|
|
12875
|
+
const tail = stderrTail.trim();
|
|
12876
|
+
reject(
|
|
12877
|
+
new Error(
|
|
12878
|
+
tail ? `npm install ${args.packageSpec} failed (${reason})
|
|
12207
12879
|
stderr: ${tail}` : `npm install ${args.packageSpec} failed (${reason})`
|
|
12208
|
-
|
|
12209
|
-
|
|
12880
|
+
)
|
|
12881
|
+
);
|
|
12882
|
+
});
|
|
12210
12883
|
});
|
|
12211
|
-
})
|
|
12884
|
+
} catch (err) {
|
|
12885
|
+
const code = err.code;
|
|
12886
|
+
if (code === "ETXTBSY" && attempt < ETXTBSY_RETRIES) {
|
|
12887
|
+
await new Promise(
|
|
12888
|
+
(r) => setTimeout(r, ETXTBSY_BACKOFF_MS * (attempt + 1))
|
|
12889
|
+
);
|
|
12890
|
+
return runNpmInstallOnce(args, attempt + 1);
|
|
12891
|
+
}
|
|
12892
|
+
throw err;
|
|
12893
|
+
}
|
|
12212
12894
|
}
|
|
12213
12895
|
async function fileExists2(p) {
|
|
12214
12896
|
try {
|
|
@@ -12406,12 +13088,14 @@ async function planSpawn(agent, callerArgs = [], options = {}) {
|
|
|
12406
13088
|
};
|
|
12407
13089
|
}
|
|
12408
13090
|
const bin = npx.bin ?? npxPackageBasename(agent) ?? npx.package;
|
|
13091
|
+
const npmCb = options.onInstallProgress;
|
|
12409
13092
|
const binPath = await ensureNpmPackage({
|
|
12410
13093
|
agentId: agent.id,
|
|
12411
13094
|
version,
|
|
12412
13095
|
packageSpec: npx.package,
|
|
12413
13096
|
bin,
|
|
12414
|
-
registry: options.npmRegistry
|
|
13097
|
+
registry: options.npmRegistry,
|
|
13098
|
+
onProgress: npmCb ? (e) => npmCb({ source: "npm", ...e }) : void 0
|
|
12415
13099
|
});
|
|
12416
13100
|
return {
|
|
12417
13101
|
command: binPath,
|
|
@@ -12427,10 +13111,12 @@ async function planSpawn(agent, callerArgs = [], options = {}) {
|
|
|
12427
13111
|
`Agent ${agent.id} has no binary distribution for ${currentPlatformKey() ?? "this platform"}.`
|
|
12428
13112
|
);
|
|
12429
13113
|
}
|
|
13114
|
+
const binCb = options.onInstallProgress;
|
|
12430
13115
|
const cmdPath = await ensureBinary({
|
|
12431
13116
|
agentId: agent.id,
|
|
12432
13117
|
version,
|
|
12433
|
-
target
|
|
13118
|
+
target,
|
|
13119
|
+
onProgress: binCb ? (e) => binCb({ source: "binary", ...e }) : void 0
|
|
12434
13120
|
});
|
|
12435
13121
|
const tail = callerArgs.length > 0 ? callerArgs : target.args ?? [];
|
|
12436
13122
|
return {
|
|
@@ -12848,7 +13534,8 @@ var SessionManager = class {
|
|
|
12848
13534
|
cwd: params.cwd,
|
|
12849
13535
|
agentArgs: params.agentArgs,
|
|
12850
13536
|
mcpServers: params.mcpServers,
|
|
12851
|
-
model: params.model
|
|
13537
|
+
model: params.model,
|
|
13538
|
+
onInstallProgress: params.onInstallProgress
|
|
12852
13539
|
});
|
|
12853
13540
|
const session = new Session({
|
|
12854
13541
|
cwd: params.cwd,
|
|
@@ -12865,7 +13552,8 @@ var SessionManager = class {
|
|
|
12865
13552
|
historyMaxEntries: this.sessionHistoryMaxEntries,
|
|
12866
13553
|
currentModel: fresh.initialModel,
|
|
12867
13554
|
currentMode: fresh.initialMode,
|
|
12868
|
-
agentModes: fresh.initialModes
|
|
13555
|
+
agentModes: fresh.initialModes,
|
|
13556
|
+
agentModels: fresh.initialModels
|
|
12869
13557
|
});
|
|
12870
13558
|
await this.attachManagerHooks(session);
|
|
12871
13559
|
return session;
|
|
@@ -12910,7 +13598,10 @@ var SessionManager = class {
|
|
|
12910
13598
|
if (params.upstreamSessionId === "") {
|
|
12911
13599
|
return this.doResurrectFromImport(params);
|
|
12912
13600
|
}
|
|
12913
|
-
const plan = await planSpawn(agentDef, params.agentArgs ?? [], {
|
|
13601
|
+
const plan = await planSpawn(agentDef, params.agentArgs ?? [], {
|
|
13602
|
+
npmRegistry: this.npmRegistry,
|
|
13603
|
+
onInstallProgress: params.onInstallProgress
|
|
13604
|
+
});
|
|
12914
13605
|
const agent = this.spawner({
|
|
12915
13606
|
agentId: params.agentId,
|
|
12916
13607
|
cwd: params.cwd,
|
|
@@ -12968,6 +13659,7 @@ var SessionManager = class {
|
|
|
12968
13659
|
currentUsage: params.currentUsage,
|
|
12969
13660
|
agentCommands: params.agentCommands,
|
|
12970
13661
|
agentModes: params.agentModes ?? nonEmptyOrUndefined(extractInitialModes(loadResult ?? {})),
|
|
13662
|
+
agentModels: params.agentModels ?? nonEmptyOrUndefined(extractInitialModels(loadResult ?? {})),
|
|
12971
13663
|
// Only gate the first-prompt title heuristic when we actually have
|
|
12972
13664
|
// a title to preserve. A title-less session (lost to a write race
|
|
12973
13665
|
// or never seeded) should re-derive from the next prompt rather
|
|
@@ -12991,7 +13683,8 @@ var SessionManager = class {
|
|
|
12991
13683
|
agentId: params.agentId,
|
|
12992
13684
|
cwd,
|
|
12993
13685
|
agentArgs: params.agentArgs,
|
|
12994
|
-
mcpServers: []
|
|
13686
|
+
mcpServers: [],
|
|
13687
|
+
onInstallProgress: params.onInstallProgress
|
|
12995
13688
|
});
|
|
12996
13689
|
const session = new Session({
|
|
12997
13690
|
sessionId: params.hydraSessionId,
|
|
@@ -13014,6 +13707,7 @@ var SessionManager = class {
|
|
|
13014
13707
|
currentUsage: params.currentUsage,
|
|
13015
13708
|
agentCommands: params.agentCommands,
|
|
13016
13709
|
agentModes: params.agentModes ?? fresh.initialModes,
|
|
13710
|
+
agentModels: params.agentModels ?? fresh.initialModels,
|
|
13017
13711
|
firstPromptSeeded: !!params.title,
|
|
13018
13712
|
createdAt: params.createdAt ? new Date(params.createdAt).getTime() : void 0
|
|
13019
13713
|
});
|
|
@@ -13043,7 +13737,10 @@ var SessionManager = class {
|
|
|
13043
13737
|
err.code = JsonRpcErrorCodes.AgentNotInstalled;
|
|
13044
13738
|
throw err;
|
|
13045
13739
|
}
|
|
13046
|
-
const plan = await planSpawn(agentDef, params.agentArgs ?? [], {
|
|
13740
|
+
const plan = await planSpawn(agentDef, params.agentArgs ?? [], {
|
|
13741
|
+
npmRegistry: this.npmRegistry,
|
|
13742
|
+
onInstallProgress: params.onInstallProgress
|
|
13743
|
+
});
|
|
13047
13744
|
const agent = this.spawner({
|
|
13048
13745
|
agentId: params.agentId,
|
|
13049
13746
|
cwd: params.cwd,
|
|
@@ -13069,15 +13766,25 @@ var SessionManager = class {
|
|
|
13069
13766
|
);
|
|
13070
13767
|
}
|
|
13071
13768
|
let initialModel = extractInitialModel(newResult);
|
|
13769
|
+
const initialModels = extractInitialModels(newResult);
|
|
13072
13770
|
const desired = params.model ?? this.defaultModels[params.agentId];
|
|
13073
13771
|
if (desired && desired !== initialModel) {
|
|
13074
|
-
|
|
13075
|
-
|
|
13076
|
-
|
|
13077
|
-
|
|
13078
|
-
|
|
13079
|
-
|
|
13080
|
-
|
|
13772
|
+
const validates = initialModels.length === 0 || initialModels.some((m) => m.modelId === desired);
|
|
13773
|
+
if (validates) {
|
|
13774
|
+
try {
|
|
13775
|
+
await agent.connection.request("session/set_model", {
|
|
13776
|
+
sessionId: sessionIdRaw,
|
|
13777
|
+
modelId: desired
|
|
13778
|
+
});
|
|
13779
|
+
initialModel = desired;
|
|
13780
|
+
} catch {
|
|
13781
|
+
}
|
|
13782
|
+
} else {
|
|
13783
|
+
const known = initialModels.map((m) => m.modelId).join(", ");
|
|
13784
|
+
process.stderr.write(
|
|
13785
|
+
`hydra-acp: defaultModels[${params.agentId}]=${JSON.stringify(desired)} is not in the agent's availableModels (${known}); skipping session/set_model
|
|
13786
|
+
`
|
|
13787
|
+
);
|
|
13081
13788
|
}
|
|
13082
13789
|
}
|
|
13083
13790
|
const initialModes = extractInitialModes(newResult);
|
|
@@ -13087,6 +13794,7 @@ var SessionManager = class {
|
|
|
13087
13794
|
upstreamSessionId: sessionIdRaw,
|
|
13088
13795
|
agentMeta: newResult._meta,
|
|
13089
13796
|
initialModel,
|
|
13797
|
+
initialModels: initialModels.length > 0 ? initialModels : void 0,
|
|
13090
13798
|
initialModes: initialModes.length > 0 ? initialModes : void 0,
|
|
13091
13799
|
initialMode
|
|
13092
13800
|
};
|
|
@@ -13149,6 +13857,15 @@ var SessionManager = class {
|
|
|
13149
13857
|
}))
|
|
13150
13858
|
}).catch(() => void 0);
|
|
13151
13859
|
});
|
|
13860
|
+
session.onAgentModelsChange((models) => {
|
|
13861
|
+
void this.persistSnapshot(session.sessionId, {
|
|
13862
|
+
agentModels: models.map((m) => ({
|
|
13863
|
+
modelId: m.modelId,
|
|
13864
|
+
...m.name !== void 0 ? { name: m.name } : {},
|
|
13865
|
+
...m.description !== void 0 ? { description: m.description } : {}
|
|
13866
|
+
}))
|
|
13867
|
+
}).catch(() => void 0);
|
|
13868
|
+
});
|
|
13152
13869
|
this.sessions.set(session.sessionId, session);
|
|
13153
13870
|
await this.enqueueMetaWrite(session.sessionId, async () => {
|
|
13154
13871
|
const existing = await this.store.read(session.sessionId);
|
|
@@ -13192,6 +13909,7 @@ var SessionManager = class {
|
|
|
13192
13909
|
currentUsage: persistedUsageToSnapshot(record.currentUsage),
|
|
13193
13910
|
agentCommands: record.agentCommands,
|
|
13194
13911
|
agentModes: record.agentModes,
|
|
13912
|
+
agentModels: record.agentModels,
|
|
13195
13913
|
createdAt: record.createdAt
|
|
13196
13914
|
};
|
|
13197
13915
|
}
|
|
@@ -13435,6 +14153,26 @@ var SessionManager = class {
|
|
|
13435
14153
|
const record = await this.store.read(sessionId).catch(() => void 0);
|
|
13436
14154
|
return record !== void 0;
|
|
13437
14155
|
}
|
|
14156
|
+
// Public retitle entry point that works on live AND cold sessions.
|
|
14157
|
+
// - Live: routes through Session.retitle so attached clients receive
|
|
14158
|
+
// a session_info_update broadcast (and persistTitle fires from the
|
|
14159
|
+
// onTitleChange handler, just like /hydra title).
|
|
14160
|
+
// - Cold: writes the new title straight into meta.json — there's
|
|
14161
|
+
// nothing in memory to broadcast to, but a later resurrect / list
|
|
14162
|
+
// will pick up the new title.
|
|
14163
|
+
// Returns false when no record exists at all (live or on disk).
|
|
14164
|
+
async setTitle(sessionId, title) {
|
|
14165
|
+
const live = this.get(sessionId);
|
|
14166
|
+
if (live) {
|
|
14167
|
+
await live.retitle(title);
|
|
14168
|
+
return true;
|
|
14169
|
+
}
|
|
14170
|
+
if (!await this.hasRecord(sessionId)) {
|
|
14171
|
+
return false;
|
|
14172
|
+
}
|
|
14173
|
+
await this.persistTitle(sessionId, title);
|
|
14174
|
+
return true;
|
|
14175
|
+
}
|
|
13438
14176
|
// Persist a title update from Session.setTitle. The on-disk record
|
|
13439
14177
|
// was written at create time; updating it here keeps the session
|
|
13440
14178
|
// record's title in sync with what was broadcast to clients so a
|
|
@@ -13487,6 +14225,7 @@ var SessionManager = class {
|
|
|
13487
14225
|
...update.currentUsage !== void 0 ? { currentUsage: update.currentUsage } : {},
|
|
13488
14226
|
...update.agentCommands !== void 0 ? { agentCommands: update.agentCommands } : {},
|
|
13489
14227
|
...update.agentModes !== void 0 ? { agentModes: update.agentModes } : {},
|
|
14228
|
+
...update.agentModels !== void 0 ? { agentModels: update.agentModels } : {},
|
|
13490
14229
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
13491
14230
|
});
|
|
13492
14231
|
});
|
|
@@ -13588,6 +14327,18 @@ function mergeForPersistence(session, existing) {
|
|
|
13588
14327
|
return out;
|
|
13589
14328
|
}) : void 0;
|
|
13590
14329
|
const agentModes = persistedModes ?? existing?.agentModes;
|
|
14330
|
+
const sessionModels = session.availableModels();
|
|
14331
|
+
const persistedModels = sessionModels.length > 0 ? sessionModels.map((m) => {
|
|
14332
|
+
const out = { modelId: m.modelId };
|
|
14333
|
+
if (m.name !== void 0) {
|
|
14334
|
+
out.name = m.name;
|
|
14335
|
+
}
|
|
14336
|
+
if (m.description !== void 0) {
|
|
14337
|
+
out.description = m.description;
|
|
14338
|
+
}
|
|
14339
|
+
return out;
|
|
14340
|
+
}) : void 0;
|
|
14341
|
+
const agentModels = persistedModels ?? existing?.agentModels;
|
|
13591
14342
|
return recordFromMemorySession({
|
|
13592
14343
|
sessionId: session.sessionId,
|
|
13593
14344
|
lineageId: existing?.lineageId ?? generateLineageId(),
|
|
@@ -13604,6 +14355,7 @@ function mergeForPersistence(session, existing) {
|
|
|
13604
14355
|
currentUsage: usageSnapshotToPersisted(session.currentUsage) ?? existing?.currentUsage,
|
|
13605
14356
|
agentCommands,
|
|
13606
14357
|
agentModes,
|
|
14358
|
+
agentModels,
|
|
13607
14359
|
createdAt: existing?.createdAt ?? new Date(session.createdAt).toISOString()
|
|
13608
14360
|
});
|
|
13609
14361
|
}
|
|
@@ -13669,6 +14421,40 @@ function asString(value) {
|
|
|
13669
14421
|
function nonEmptyOrUndefined(arr) {
|
|
13670
14422
|
return arr.length > 0 ? arr : void 0;
|
|
13671
14423
|
}
|
|
14424
|
+
function extractInitialModels(result) {
|
|
14425
|
+
const direct = parseModelsList(result.availableModels);
|
|
14426
|
+
if (direct.length > 0) {
|
|
14427
|
+
return direct;
|
|
14428
|
+
}
|
|
14429
|
+
const models = result.models;
|
|
14430
|
+
if (models && typeof models === "object" && !Array.isArray(models)) {
|
|
14431
|
+
const fromModelsObj = parseModelsList(
|
|
14432
|
+
models.availableModels
|
|
14433
|
+
);
|
|
14434
|
+
if (fromModelsObj.length > 0) {
|
|
14435
|
+
return fromModelsObj;
|
|
14436
|
+
}
|
|
14437
|
+
}
|
|
14438
|
+
const meta = result._meta;
|
|
14439
|
+
if (meta && typeof meta === "object" && !Array.isArray(meta)) {
|
|
14440
|
+
for (const [key, value] of Object.entries(
|
|
14441
|
+
meta
|
|
14442
|
+
)) {
|
|
14443
|
+
if (key === "hydra-acp") {
|
|
14444
|
+
continue;
|
|
14445
|
+
}
|
|
14446
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
14447
|
+
const fromMeta = parseModelsList(
|
|
14448
|
+
value.availableModels
|
|
14449
|
+
);
|
|
14450
|
+
if (fromMeta.length > 0) {
|
|
14451
|
+
return fromMeta;
|
|
14452
|
+
}
|
|
14453
|
+
}
|
|
14454
|
+
}
|
|
14455
|
+
}
|
|
14456
|
+
return [];
|
|
14457
|
+
}
|
|
13672
14458
|
function extractInitialModes(result) {
|
|
13673
14459
|
const direct = parseModesList(result.availableModes);
|
|
13674
14460
|
if (direct.length > 0) {
|
|
@@ -14646,6 +15432,35 @@ function registerSessionRoutes(app, manager, defaults) {
|
|
|
14646
15432
|
}
|
|
14647
15433
|
reply.code(204).send();
|
|
14648
15434
|
});
|
|
15435
|
+
app.patch("/v1/sessions/:id", async (request, reply) => {
|
|
15436
|
+
const raw = request.params.id;
|
|
15437
|
+
const id = await manager.resolveCanonicalId(raw) ?? raw;
|
|
15438
|
+
const body = request.body ?? {};
|
|
15439
|
+
if (body.regen === true) {
|
|
15440
|
+
const session = manager.get(id);
|
|
15441
|
+
if (!session) {
|
|
15442
|
+
reply.code(409).send({ error: "regen requires a live session" });
|
|
15443
|
+
return;
|
|
15444
|
+
}
|
|
15445
|
+
void session.retitleFromAgent().catch((err) => {
|
|
15446
|
+
app.log.warn(
|
|
15447
|
+
`title regen failed for ${id}: ${err.message}`
|
|
15448
|
+
);
|
|
15449
|
+
});
|
|
15450
|
+
reply.code(202).send();
|
|
15451
|
+
return;
|
|
15452
|
+
}
|
|
15453
|
+
if (typeof body.title !== "string" || body.title.trim().length === 0) {
|
|
15454
|
+
reply.code(400).send({ error: "title must be a non-empty string" });
|
|
15455
|
+
return;
|
|
15456
|
+
}
|
|
15457
|
+
const ok = await manager.setTitle(id, body.title);
|
|
15458
|
+
if (!ok) {
|
|
15459
|
+
reply.code(404).send({ error: "session not found" });
|
|
15460
|
+
return;
|
|
15461
|
+
}
|
|
15462
|
+
reply.code(204).send();
|
|
15463
|
+
});
|
|
14649
15464
|
app.delete("/v1/sessions/:id", async (request, reply) => {
|
|
14650
15465
|
const raw = request.params.id;
|
|
14651
15466
|
const id = await manager.resolveCanonicalId(raw) ?? raw;
|
|
@@ -15188,7 +16003,8 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
15188
16003
|
mcpServers: params.mcpServers,
|
|
15189
16004
|
title: hydraMeta.name,
|
|
15190
16005
|
agentArgs: hydraMeta.agentArgs,
|
|
15191
|
-
model: hydraMeta.model
|
|
16006
|
+
model: hydraMeta.model,
|
|
16007
|
+
onInstallProgress: makeInstallProgressForwarder(connection)
|
|
15192
16008
|
});
|
|
15193
16009
|
const client = bindClientToSession(connection, session, state);
|
|
15194
16010
|
const { entries: replay } = await session.attach(client, "full");
|
|
@@ -15204,6 +16020,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
15204
16020
|
})();
|
|
15205
16021
|
});
|
|
15206
16022
|
const modesPayload = buildModesPayload(session);
|
|
16023
|
+
const modelsPayload = buildModelsPayload(session);
|
|
15207
16024
|
return {
|
|
15208
16025
|
sessionId: session.sessionId,
|
|
15209
16026
|
// session/new is implicitly an attach; mirror session/attach's
|
|
@@ -15212,6 +16029,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
15212
16029
|
// events without an extra round-trip.
|
|
15213
16030
|
clientId: client.clientId,
|
|
15214
16031
|
...modesPayload ? { modes: modesPayload } : {},
|
|
16032
|
+
...modelsPayload ? { models: modelsPayload } : {},
|
|
15215
16033
|
_meta: buildResponseMeta(session)
|
|
15216
16034
|
};
|
|
15217
16035
|
});
|
|
@@ -15247,7 +16065,10 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
15247
16065
|
err.code = JsonRpcErrorCodes.SessionNotFound;
|
|
15248
16066
|
throw err;
|
|
15249
16067
|
}
|
|
15250
|
-
session = await deps.manager.resurrect(
|
|
16068
|
+
session = await deps.manager.resurrect({
|
|
16069
|
+
...resurrectParams,
|
|
16070
|
+
onInstallProgress: makeInstallProgressForwarder(connection)
|
|
16071
|
+
});
|
|
15251
16072
|
}
|
|
15252
16073
|
const client = bindClientToSession(
|
|
15253
16074
|
connection,
|
|
@@ -15273,6 +16094,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
15273
16094
|
}
|
|
15274
16095
|
session.replayPendingPermissions(client);
|
|
15275
16096
|
const modesPayload = buildModesPayload(session);
|
|
16097
|
+
const modelsPayload = buildModelsPayload(session);
|
|
15276
16098
|
return {
|
|
15277
16099
|
sessionId: session.sessionId,
|
|
15278
16100
|
clientId: client.clientId,
|
|
@@ -15284,6 +16106,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
15284
16106
|
historyPolicy: appliedPolicy,
|
|
15285
16107
|
replayed: replay.length,
|
|
15286
16108
|
...modesPayload ? { modes: modesPayload } : {},
|
|
16109
|
+
...modelsPayload ? { models: modelsPayload } : {},
|
|
15287
16110
|
_meta: buildResponseMeta(session)
|
|
15288
16111
|
};
|
|
15289
16112
|
});
|
|
@@ -15439,15 +16262,39 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
15439
16262
|
}
|
|
15440
16263
|
session.replayPendingPermissions(client);
|
|
15441
16264
|
const modesPayload = buildModesPayload(session);
|
|
16265
|
+
const modelsPayload = buildModelsPayload(session);
|
|
15442
16266
|
return {
|
|
15443
16267
|
sessionId: session.sessionId,
|
|
15444
16268
|
// Same as session/new: include clientId so the deferred-echo
|
|
15445
16269
|
// path in queue-aware clients can recognize own broadcasts.
|
|
15446
16270
|
clientId: client.clientId,
|
|
15447
16271
|
...modesPayload ? { modes: modesPayload } : {},
|
|
16272
|
+
...modelsPayload ? { models: modelsPayload } : {},
|
|
15448
16273
|
_meta: buildResponseMeta(session)
|
|
15449
16274
|
};
|
|
15450
16275
|
});
|
|
16276
|
+
connection.onRequest("session/set_model", async (rawParams) => {
|
|
16277
|
+
const decision = decideSetModel(rawParams, deps.manager);
|
|
16278
|
+
if (decision.kind === "error") {
|
|
16279
|
+
app.log.warn(decision.logMessage);
|
|
16280
|
+
const err = new Error(decision.message);
|
|
16281
|
+
err.code = decision.code;
|
|
16282
|
+
throw err;
|
|
16283
|
+
}
|
|
16284
|
+
if (decision.kind === "no_op") {
|
|
16285
|
+
app.log.warn(decision.logMessage);
|
|
16286
|
+
await connection.notify("session/update", {
|
|
16287
|
+
sessionId: decision.sessionId,
|
|
16288
|
+
update: {
|
|
16289
|
+
sessionUpdate: "current_model_update",
|
|
16290
|
+
currentModel: decision.currentModel
|
|
16291
|
+
}
|
|
16292
|
+
}).catch(() => void 0);
|
|
16293
|
+
return null;
|
|
16294
|
+
}
|
|
16295
|
+
app.log.info(decision.logMessage);
|
|
16296
|
+
return decision.session.forwardRequest("session/set_model", rawParams);
|
|
16297
|
+
});
|
|
15451
16298
|
connection.setDefaultHandler(async (rawParams, method) => {
|
|
15452
16299
|
if (!method.startsWith("session/") || rawParams === null || typeof rawParams !== "object") {
|
|
15453
16300
|
const err = new Error(`Method not found: ${method}`);
|
|
@@ -15470,6 +16317,26 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
15470
16317
|
});
|
|
15471
16318
|
});
|
|
15472
16319
|
}
|
|
16320
|
+
function makeInstallProgressForwarder(connection) {
|
|
16321
|
+
return (event) => {
|
|
16322
|
+
const payload = {
|
|
16323
|
+
agentId: event.agentId,
|
|
16324
|
+
version: event.version,
|
|
16325
|
+
source: event.source,
|
|
16326
|
+
phase: event.phase
|
|
16327
|
+
};
|
|
16328
|
+
if ("receivedBytes" in event) {
|
|
16329
|
+
payload.receivedBytes = event.receivedBytes;
|
|
16330
|
+
}
|
|
16331
|
+
if ("totalBytes" in event) {
|
|
16332
|
+
payload.totalBytes = event.totalBytes;
|
|
16333
|
+
}
|
|
16334
|
+
if ("packageSpec" in event) {
|
|
16335
|
+
payload.packageSpec = event.packageSpec;
|
|
16336
|
+
}
|
|
16337
|
+
void connection.notify(AGENT_INSTALL_PROGRESS_METHOD, payload).catch(() => void 0);
|
|
16338
|
+
};
|
|
16339
|
+
}
|
|
15473
16340
|
function buildModesPayload(session) {
|
|
15474
16341
|
const modes = session.availableModes();
|
|
15475
16342
|
if (modes.length === 0) {
|
|
@@ -15490,6 +16357,94 @@ function buildModesPayload(session) {
|
|
|
15490
16357
|
const currentModeId = session.currentMode ?? modes[0].id;
|
|
15491
16358
|
return { currentModeId, availableModes };
|
|
15492
16359
|
}
|
|
16360
|
+
function buildModelsPayload(session) {
|
|
16361
|
+
const models = session.availableModels();
|
|
16362
|
+
if (models.length === 0) {
|
|
16363
|
+
return void 0;
|
|
16364
|
+
}
|
|
16365
|
+
const availableModels = models.map((m) => {
|
|
16366
|
+
const out = {
|
|
16367
|
+
modelId: m.modelId
|
|
16368
|
+
};
|
|
16369
|
+
if (m.name !== void 0) {
|
|
16370
|
+
out.name = m.name;
|
|
16371
|
+
}
|
|
16372
|
+
if (m.description !== void 0) {
|
|
16373
|
+
out.description = m.description;
|
|
16374
|
+
}
|
|
16375
|
+
return out;
|
|
16376
|
+
});
|
|
16377
|
+
const currentModelId = session.currentModel ?? models[0].modelId;
|
|
16378
|
+
return { currentModelId, availableModels };
|
|
16379
|
+
}
|
|
16380
|
+
function decideSetModel(rawParams, manager) {
|
|
16381
|
+
if (!rawParams || typeof rawParams !== "object") {
|
|
16382
|
+
return {
|
|
16383
|
+
kind: "error",
|
|
16384
|
+
code: JsonRpcErrorCodes.InvalidParams,
|
|
16385
|
+
message: "session/set_model requires params",
|
|
16386
|
+
logMessage: "session/set_model rejected: params not an object"
|
|
16387
|
+
};
|
|
16388
|
+
}
|
|
16389
|
+
const params = rawParams;
|
|
16390
|
+
if (typeof params.sessionId !== "string") {
|
|
16391
|
+
return {
|
|
16392
|
+
kind: "error",
|
|
16393
|
+
code: JsonRpcErrorCodes.InvalidParams,
|
|
16394
|
+
message: "session/set_model requires string sessionId",
|
|
16395
|
+
logMessage: "session/set_model rejected: missing/non-string sessionId"
|
|
16396
|
+
};
|
|
16397
|
+
}
|
|
16398
|
+
if (typeof params.modelId !== "string") {
|
|
16399
|
+
return {
|
|
16400
|
+
kind: "error",
|
|
16401
|
+
code: JsonRpcErrorCodes.InvalidParams,
|
|
16402
|
+
message: "session/set_model requires string modelId",
|
|
16403
|
+
logMessage: `session/set_model rejected: missing/non-string modelId sessionId=${params.sessionId}`
|
|
16404
|
+
};
|
|
16405
|
+
}
|
|
16406
|
+
const session = manager.get(params.sessionId);
|
|
16407
|
+
if (!session) {
|
|
16408
|
+
return {
|
|
16409
|
+
kind: "error",
|
|
16410
|
+
code: JsonRpcErrorCodes.SessionNotFound,
|
|
16411
|
+
message: `session ${params.sessionId} not found`,
|
|
16412
|
+
logMessage: `session/set_model rejected: session not found sessionId=${params.sessionId}`
|
|
16413
|
+
};
|
|
16414
|
+
}
|
|
16415
|
+
const advertised = session.availableModels();
|
|
16416
|
+
if (advertised.length === 0) {
|
|
16417
|
+
return {
|
|
16418
|
+
kind: "ok",
|
|
16419
|
+
session,
|
|
16420
|
+
logMessage: `session/set_model passthrough (no availableModels) sessionId=${params.sessionId} modelId=${JSON.stringify(params.modelId)}`
|
|
16421
|
+
};
|
|
16422
|
+
}
|
|
16423
|
+
const match = advertised.find((m) => m.modelId === params.modelId);
|
|
16424
|
+
if (!match) {
|
|
16425
|
+
const known = advertised.map((m) => m.modelId).join(", ");
|
|
16426
|
+
if (session.currentModel !== void 0 && session.currentModel.length > 0) {
|
|
16427
|
+
return {
|
|
16428
|
+
kind: "no_op",
|
|
16429
|
+
session,
|
|
16430
|
+
sessionId: params.sessionId,
|
|
16431
|
+
currentModel: session.currentModel,
|
|
16432
|
+
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}]`
|
|
16433
|
+
};
|
|
16434
|
+
}
|
|
16435
|
+
return {
|
|
16436
|
+
kind: "error",
|
|
16437
|
+
code: JsonRpcErrorCodes.InvalidParams,
|
|
16438
|
+
message: `model "${params.modelId}" is not in this session's availableModels (agent ${session.agentId}); known models: ${known}`,
|
|
16439
|
+
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)`
|
|
16440
|
+
};
|
|
16441
|
+
}
|
|
16442
|
+
return {
|
|
16443
|
+
kind: "ok",
|
|
16444
|
+
session,
|
|
16445
|
+
logMessage: `session/set_model accepted sessionId=${params.sessionId} modelId=${JSON.stringify(params.modelId)}`
|
|
16446
|
+
};
|
|
16447
|
+
}
|
|
15493
16448
|
function buildResponseMeta(session) {
|
|
15494
16449
|
const ours = {
|
|
15495
16450
|
upstreamSessionId: session.upstreamSessionId,
|
|
@@ -15519,6 +16474,10 @@ function buildResponseMeta(session) {
|
|
|
15519
16474
|
if (modes.length > 0) {
|
|
15520
16475
|
ours.availableModes = modes;
|
|
15521
16476
|
}
|
|
16477
|
+
const models = session.availableModels();
|
|
16478
|
+
if (models.length > 0) {
|
|
16479
|
+
ours.availableModels = models;
|
|
16480
|
+
}
|
|
15522
16481
|
if (session.turnStartedAt !== void 0) {
|
|
15523
16482
|
ours.turnStartedAt = session.turnStartedAt;
|
|
15524
16483
|
}
|
|
@@ -15754,6 +16713,7 @@ function ensureLoopbackOrTls(config) {
|
|
|
15754
16713
|
|
|
15755
16714
|
// src/cli/commands/daemon.ts
|
|
15756
16715
|
init_daemon_bootstrap();
|
|
16716
|
+
init_hydra_version();
|
|
15757
16717
|
|
|
15758
16718
|
// src/cli/commands/log-tail.ts
|
|
15759
16719
|
import * as fs16 from "fs";
|
|
@@ -15984,6 +16944,8 @@ async function runDaemonStatus() {
|
|
|
15984
16944
|
const info = await readPidFile();
|
|
15985
16945
|
if (!info) {
|
|
15986
16946
|
process.stdout.write("Daemon: not running\n");
|
|
16947
|
+
process.stdout.write(`CLI version: ${HYDRA_VERSION}
|
|
16948
|
+
`);
|
|
15987
16949
|
return;
|
|
15988
16950
|
}
|
|
15989
16951
|
const alive = isProcessAlive(info.pid);
|
|
@@ -15991,6 +16953,52 @@ async function runDaemonStatus() {
|
|
|
15991
16953
|
`Daemon: ${alive ? "running" : "stale pid file"} pid=${info.pid} host=${info.host} port=${info.port} started=${info.startedAt}
|
|
15992
16954
|
`
|
|
15993
16955
|
);
|
|
16956
|
+
let daemonVersion;
|
|
16957
|
+
if (alive) {
|
|
16958
|
+
try {
|
|
16959
|
+
const config = await loadConfig();
|
|
16960
|
+
daemonVersion = await fetchDaemonVersion(config);
|
|
16961
|
+
} catch {
|
|
16962
|
+
}
|
|
16963
|
+
}
|
|
16964
|
+
if (daemonVersion === void 0) {
|
|
16965
|
+
process.stdout.write(`CLI version: ${HYDRA_VERSION}
|
|
16966
|
+
`);
|
|
16967
|
+
if (alive) {
|
|
16968
|
+
process.stdout.write(
|
|
16969
|
+
"Daemon version: unknown (health endpoint unreachable)\n"
|
|
16970
|
+
);
|
|
16971
|
+
}
|
|
16972
|
+
return;
|
|
16973
|
+
}
|
|
16974
|
+
if (daemonVersion === HYDRA_VERSION) {
|
|
16975
|
+
process.stdout.write(`Version: ${HYDRA_VERSION}
|
|
16976
|
+
`);
|
|
16977
|
+
return;
|
|
16978
|
+
}
|
|
16979
|
+
process.stdout.write(`CLI version: ${HYDRA_VERSION}
|
|
16980
|
+
`);
|
|
16981
|
+
process.stdout.write(`Daemon version: ${daemonVersion}
|
|
16982
|
+
`);
|
|
16983
|
+
process.stdout.write(
|
|
16984
|
+
chalk.yellow(
|
|
16985
|
+
"Version mismatch \u2014 run `hydra-acp daemon restart` to upgrade the daemon.\n"
|
|
16986
|
+
)
|
|
16987
|
+
);
|
|
16988
|
+
}
|
|
16989
|
+
async function fetchDaemonVersion(config) {
|
|
16990
|
+
const protocol = config.daemon.tls ? "https" : "http";
|
|
16991
|
+
const url = `${protocol}://${config.daemon.host}:${config.daemon.port}/v1/health`;
|
|
16992
|
+
try {
|
|
16993
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(1e3) });
|
|
16994
|
+
if (!response.ok) {
|
|
16995
|
+
return void 0;
|
|
16996
|
+
}
|
|
16997
|
+
const body = await response.json();
|
|
16998
|
+
return typeof body.version === "string" ? body.version : void 0;
|
|
16999
|
+
} catch {
|
|
17000
|
+
return void 0;
|
|
17001
|
+
}
|
|
15994
17002
|
}
|
|
15995
17003
|
async function readPidFile() {
|
|
15996
17004
|
try {
|