@hydra-acp/cli 0.1.25 → 0.1.27
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 +1136 -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"],
|
|
@@ -6589,6 +6991,42 @@ var init_screen = __esm({
|
|
|
6589
6991
|
return;
|
|
6590
6992
|
}
|
|
6591
6993
|
}
|
|
6994
|
+
const csiUCtrlMap = {
|
|
6995
|
+
97: "ctrl-a",
|
|
6996
|
+
98: "ctrl-b",
|
|
6997
|
+
99: "ctrl-c",
|
|
6998
|
+
100: "ctrl-d",
|
|
6999
|
+
101: "ctrl-e",
|
|
7000
|
+
102: "ctrl-f",
|
|
7001
|
+
103: "ctrl-g",
|
|
7002
|
+
107: "ctrl-k",
|
|
7003
|
+
108: "ctrl-l",
|
|
7004
|
+
110: "ctrl-n",
|
|
7005
|
+
111: "ctrl-o",
|
|
7006
|
+
112: "ctrl-p",
|
|
7007
|
+
114: "ctrl-r",
|
|
7008
|
+
115: "ctrl-s",
|
|
7009
|
+
116: "ctrl-t",
|
|
7010
|
+
117: "ctrl-u",
|
|
7011
|
+
118: "ctrl-v",
|
|
7012
|
+
119: "ctrl-w",
|
|
7013
|
+
121: "ctrl-y"
|
|
7014
|
+
};
|
|
7015
|
+
const csiUCtrlRe = /\x1b\[(\d+);5u/;
|
|
7016
|
+
const m = csiUCtrlRe.exec(text);
|
|
7017
|
+
if (m !== null) {
|
|
7018
|
+
const keyName = csiUCtrlMap[parseInt(m[1], 10)];
|
|
7019
|
+
if (keyName !== void 0) {
|
|
7020
|
+
const parts = text.split(m[0]);
|
|
7021
|
+
for (let i = 0; i < parts.length; i++) {
|
|
7022
|
+
if (parts[i].length > 0)
|
|
7023
|
+
this.handleRawStdin(Buffer.from(parts[i], "binary"));
|
|
7024
|
+
if (i < parts.length - 1)
|
|
7025
|
+
this.onKey([{ type: "key", name: keyName }]);
|
|
7026
|
+
}
|
|
7027
|
+
return;
|
|
7028
|
+
}
|
|
7029
|
+
}
|
|
6592
7030
|
}
|
|
6593
7031
|
this.handleRawStdinSegment(text);
|
|
6594
7032
|
}
|
|
@@ -7886,11 +8324,16 @@ var init_screen = __esm({
|
|
|
7886
8324
|
const elapsedStr = this.banner.status === "busy" && this.banner.elapsedMs !== void 0 && this.banner.elapsedMs >= 1e3 ? formatElapsed(this.banner.elapsedMs) : "";
|
|
7887
8325
|
const right = this.bannerRightContent();
|
|
7888
8326
|
const rightSig = right ? `${right.kind}|${right.text}` : "";
|
|
7889
|
-
const
|
|
8327
|
+
const stalled = this.banner.status === "busy" && this.banner.stalled === true;
|
|
8328
|
+
const sig = `bnr|${w}|${this.banner.status}|${elapsedStr}|${stalled ? "1" : "0"}|${this.banner.queued}|${this.scrollOffset}|${this.banner.currentMode ?? ""}|${this.banner.hint}|` + rightSig;
|
|
7890
8329
|
this.paintRow(row, sig, () => {
|
|
7891
8330
|
const dot = this.banner.status === "busy" ? "\u25CF" : "\u25CB";
|
|
7892
8331
|
if (this.banner.status === "busy") {
|
|
7893
|
-
|
|
8332
|
+
if (stalled) {
|
|
8333
|
+
this.term.brightRed(`${dot} stalled`);
|
|
8334
|
+
} else {
|
|
8335
|
+
this.term.brightYellow(`${dot} ${this.banner.status}`);
|
|
8336
|
+
}
|
|
7894
8337
|
if (elapsedStr) {
|
|
7895
8338
|
this.term(" ").dim(elapsedStr);
|
|
7896
8339
|
}
|
|
@@ -9267,8 +9710,29 @@ var init_completion = __esm({
|
|
|
9267
9710
|
}
|
|
9268
9711
|
});
|
|
9269
9712
|
|
|
9713
|
+
// src/tui/reconnect-state.ts
|
|
9714
|
+
function parseReattachResponse(result) {
|
|
9715
|
+
const out = {};
|
|
9716
|
+
if (!result || typeof result !== "object") {
|
|
9717
|
+
return out;
|
|
9718
|
+
}
|
|
9719
|
+
const r = result;
|
|
9720
|
+
if (typeof r.historyPolicy === "string") {
|
|
9721
|
+
out.appliedPolicy = r.historyPolicy;
|
|
9722
|
+
}
|
|
9723
|
+
if (typeof r.clientId === "string" && r.clientId.length > 0) {
|
|
9724
|
+
out.clientId = r.clientId;
|
|
9725
|
+
}
|
|
9726
|
+
return out;
|
|
9727
|
+
}
|
|
9728
|
+
var init_reconnect_state = __esm({
|
|
9729
|
+
"src/tui/reconnect-state.ts"() {
|
|
9730
|
+
"use strict";
|
|
9731
|
+
}
|
|
9732
|
+
});
|
|
9733
|
+
|
|
9270
9734
|
// src/tui/format.ts
|
|
9271
|
-
import
|
|
9735
|
+
import chalk2 from "chalk";
|
|
9272
9736
|
import { highlight, supportsLanguage } from "cli-highlight";
|
|
9273
9737
|
function formatEvent(event) {
|
|
9274
9738
|
switch (event.kind) {
|
|
@@ -9561,12 +10025,22 @@ function formatToolLine2(state) {
|
|
|
9561
10025
|
} else {
|
|
9562
10026
|
title = `${initial} \xB7 ${latest}`;
|
|
9563
10027
|
}
|
|
9564
|
-
|
|
9565
|
-
|
|
9566
|
-
|
|
9567
|
-
|
|
9568
|
-
|
|
9569
|
-
|
|
10028
|
+
const lines = [
|
|
10029
|
+
{
|
|
10030
|
+
prefix: ` ${toolStatusIcon(state.status)} `,
|
|
10031
|
+
prefixStyle: toolIconStyle(state.status),
|
|
10032
|
+
body: title,
|
|
10033
|
+
bodyStyle: toolStatusStyle(state.status)
|
|
10034
|
+
}
|
|
10035
|
+
];
|
|
10036
|
+
if (state.status === "failed" && state.errorText) {
|
|
10037
|
+
lines.push({
|
|
10038
|
+
prefix: " ",
|
|
10039
|
+
body: sanitizeSingleLine(state.errorText),
|
|
10040
|
+
bodyStyle: "tool-status-fail"
|
|
10041
|
+
});
|
|
10042
|
+
}
|
|
10043
|
+
return lines;
|
|
9570
10044
|
}
|
|
9571
10045
|
function toolStatusIcon(status) {
|
|
9572
10046
|
switch (status) {
|
|
@@ -9665,7 +10139,8 @@ var highlightChalk, HIGHLIGHT_THEME;
|
|
|
9665
10139
|
var init_format = __esm({
|
|
9666
10140
|
"src/tui/format.ts"() {
|
|
9667
10141
|
"use strict";
|
|
9668
|
-
|
|
10142
|
+
init_render_update();
|
|
10143
|
+
highlightChalk = new chalk2.Instance({ level: 3 });
|
|
9669
10144
|
HIGHLIGHT_THEME = {
|
|
9670
10145
|
keyword: highlightChalk.blueBright,
|
|
9671
10146
|
built_in: highlightChalk.cyan,
|
|
@@ -9727,8 +10202,9 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
9727
10202
|
term.grabInput(false);
|
|
9728
10203
|
process.exit(0);
|
|
9729
10204
|
}
|
|
9730
|
-
const
|
|
9731
|
-
term
|
|
10205
|
+
const launchLabelBase = ctx.sessionId === "__new__" ? "Starting new session\u2026" : "Resuming session\u2026";
|
|
10206
|
+
const installStatus = createInstallStatusLine(term, launchLabelBase);
|
|
10207
|
+
installStatus.write(launchLabelBase);
|
|
9732
10208
|
const protocol = config.daemon.tls ? "wss" : "ws";
|
|
9733
10209
|
const wsUrl = `${protocol}://${config.daemon.host}:${config.daemon.port}/acp`;
|
|
9734
10210
|
const subprotocols = ["acp.v1", `hydra-acp-token.${serviceToken}`];
|
|
@@ -9754,6 +10230,13 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
9754
10230
|
});
|
|
9755
10231
|
const conn = new JsonRpcConnection(stream);
|
|
9756
10232
|
await stream.start();
|
|
10233
|
+
conn.onNotification(AGENT_INSTALL_PROGRESS_METHOD, (raw) => {
|
|
10234
|
+
const parsed = AgentInstallProgressParams.safeParse(raw);
|
|
10235
|
+
if (!parsed.success) {
|
|
10236
|
+
return;
|
|
10237
|
+
}
|
|
10238
|
+
installStatus.applyProgress(parsed.data);
|
|
10239
|
+
});
|
|
9757
10240
|
let bufferedEvents = [];
|
|
9758
10241
|
let applyRenderEvent = null;
|
|
9759
10242
|
let teardownStarted = false;
|
|
@@ -9771,34 +10254,46 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
9771
10254
|
let currentHeadMessageId;
|
|
9772
10255
|
let sessionBusySince = null;
|
|
9773
10256
|
let sessionElapsedTimer = null;
|
|
10257
|
+
let lastUpdateAt = null;
|
|
10258
|
+
let upstreamInterruptedSeen = false;
|
|
9774
10259
|
const adjustPendingTurns = (delta) => {
|
|
9775
10260
|
const before = pendingTurns;
|
|
9776
10261
|
pendingTurns = Math.max(0, pendingTurns + delta);
|
|
9777
10262
|
const screenReady = typeof screenRef !== "undefined" && screenRef !== null;
|
|
9778
10263
|
if (before === 0 && pendingTurns > 0) {
|
|
9779
10264
|
sessionBusySince = Date.now();
|
|
10265
|
+
lastUpdateAt = Date.now();
|
|
9780
10266
|
dispatcherRef?.setTurnRunning(true);
|
|
9781
10267
|
if (screenReady) {
|
|
9782
|
-
screenRef.setBanner({ status: "busy", elapsedMs: 0 });
|
|
10268
|
+
screenRef.setBanner({ status: "busy", elapsedMs: 0, stalled: false });
|
|
9783
10269
|
}
|
|
9784
10270
|
if (sessionElapsedTimer === null && screenReady) {
|
|
9785
10271
|
sessionElapsedTimer = setInterval(() => {
|
|
9786
10272
|
if (sessionBusySince === null || screenRef === null) {
|
|
9787
10273
|
return;
|
|
9788
10274
|
}
|
|
9789
|
-
|
|
10275
|
+
const idleMs = lastUpdateAt === null ? 0 : Date.now() - lastUpdateAt;
|
|
10276
|
+
screenRef.setBanner({
|
|
10277
|
+
elapsedMs: Date.now() - sessionBusySince,
|
|
10278
|
+
stalled: idleMs >= STALL_THRESHOLD_MS
|
|
10279
|
+
});
|
|
9790
10280
|
renderToolsBlock();
|
|
9791
10281
|
}, 1e3);
|
|
9792
10282
|
}
|
|
9793
10283
|
} else if (before > 0 && pendingTurns === 0) {
|
|
9794
10284
|
sessionBusySince = null;
|
|
10285
|
+
lastUpdateAt = null;
|
|
9795
10286
|
dispatcherRef?.setTurnRunning(false);
|
|
9796
10287
|
if (sessionElapsedTimer !== null) {
|
|
9797
10288
|
clearInterval(sessionElapsedTimer);
|
|
9798
10289
|
sessionElapsedTimer = null;
|
|
9799
10290
|
}
|
|
9800
10291
|
if (screenReady) {
|
|
9801
|
-
screenRef.setBanner({
|
|
10292
|
+
screenRef.setBanner({
|
|
10293
|
+
status: "ready",
|
|
10294
|
+
elapsedMs: void 0,
|
|
10295
|
+
stalled: false
|
|
10296
|
+
});
|
|
9802
10297
|
}
|
|
9803
10298
|
}
|
|
9804
10299
|
void delta;
|
|
@@ -9819,6 +10314,7 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
9819
10314
|
const { update } = params ?? {};
|
|
9820
10315
|
const event = mapUpdate(update);
|
|
9821
10316
|
debugLogUpdate(update, event);
|
|
10317
|
+
lastUpdateAt = Date.now();
|
|
9822
10318
|
const rawTag = update?.sessionUpdate;
|
|
9823
10319
|
if (typeof rawTag === "string" && !STATE_UPDATE_KINDS2.has(rawTag)) {
|
|
9824
10320
|
const u = update ?? {};
|
|
@@ -10388,6 +10884,7 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
10388
10884
|
};
|
|
10389
10885
|
const sessionbarAgent = resolvedAgentId || agentInfoName || "?";
|
|
10390
10886
|
const usage = { ...initialUsage ?? {} };
|
|
10887
|
+
installStatus.finalize();
|
|
10391
10888
|
screen.start();
|
|
10392
10889
|
screen.setSessionbar({
|
|
10393
10890
|
agent: sessionbarAgent,
|
|
@@ -11270,7 +11767,7 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
11270
11767
|
for (const id of visibleIds) {
|
|
11271
11768
|
const state = toolStates.get(id);
|
|
11272
11769
|
if (state) {
|
|
11273
|
-
lines.push(formatToolLine2(state));
|
|
11770
|
+
lines.push(...formatToolLine2(state));
|
|
11274
11771
|
}
|
|
11275
11772
|
}
|
|
11276
11773
|
screen.upsertLines("tools", lines);
|
|
@@ -11281,7 +11778,7 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
11281
11778
|
toolsBlockStopReason = null;
|
|
11282
11779
|
renderToolsBlock();
|
|
11283
11780
|
};
|
|
11284
|
-
const recordToolCall = (id, title, status) => {
|
|
11781
|
+
const recordToolCall = (id, title, status, errorText) => {
|
|
11285
11782
|
const wasNew = !toolStates.has(id);
|
|
11286
11783
|
const existing = toolStates.get(id);
|
|
11287
11784
|
const state = existing ?? {
|
|
@@ -11298,6 +11795,9 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
11298
11795
|
if (!existing) {
|
|
11299
11796
|
state.status = status ?? "pending";
|
|
11300
11797
|
}
|
|
11798
|
+
if (errorText !== void 0) {
|
|
11799
|
+
state.errorText = errorText;
|
|
11800
|
+
}
|
|
11301
11801
|
toolStates.set(id, state);
|
|
11302
11802
|
if (wasNew) {
|
|
11303
11803
|
if (toolsBlockStartedAt === null) {
|
|
@@ -11389,7 +11889,7 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
11389
11889
|
}
|
|
11390
11890
|
if (event.kind === "tool-call") {
|
|
11391
11891
|
closeAgentText();
|
|
11392
|
-
recordToolCall(event.toolCallId, event.title, event.status);
|
|
11892
|
+
recordToolCall(event.toolCallId, event.title, event.status, void 0);
|
|
11393
11893
|
renderToolsBlock();
|
|
11394
11894
|
return;
|
|
11395
11895
|
}
|
|
@@ -11404,7 +11904,15 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
11404
11904
|
}
|
|
11405
11905
|
if (event.kind === "tool-call-update") {
|
|
11406
11906
|
closeAgentText();
|
|
11407
|
-
recordToolCall(
|
|
11907
|
+
recordToolCall(
|
|
11908
|
+
event.toolCallId,
|
|
11909
|
+
event.title,
|
|
11910
|
+
event.status,
|
|
11911
|
+
event.errorText
|
|
11912
|
+
);
|
|
11913
|
+
if (event.upstreamInterrupted) {
|
|
11914
|
+
upstreamInterruptedSeen = true;
|
|
11915
|
+
}
|
|
11408
11916
|
renderToolsBlock();
|
|
11409
11917
|
return;
|
|
11410
11918
|
}
|
|
@@ -11418,7 +11926,10 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
11418
11926
|
if (event.kind === "turn-complete") {
|
|
11419
11927
|
currentHeadMessageId = void 0;
|
|
11420
11928
|
closeAgentText();
|
|
11421
|
-
|
|
11929
|
+
let effectiveStopReason = event.amended ? "amended" : event.stopReason;
|
|
11930
|
+
if (!event.amended && upstreamInterruptedSeen && (effectiveStopReason === void 0 || effectiveStopReason === "end_turn")) {
|
|
11931
|
+
effectiveStopReason = "error";
|
|
11932
|
+
}
|
|
11422
11933
|
if (lastPlanEvent !== null && effectiveStopReason !== void 0 && effectiveStopReason !== "end_turn") {
|
|
11423
11934
|
const lines = formatEvent({ ...lastPlanEvent, stopped: true });
|
|
11424
11935
|
if (lines.length > 0) {
|
|
@@ -11448,6 +11959,7 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
11448
11959
|
toolsBlockEndedAt = null;
|
|
11449
11960
|
toolsBlockStopReason = null;
|
|
11450
11961
|
toolsExpanded = false;
|
|
11962
|
+
upstreamInterruptedSeen = false;
|
|
11451
11963
|
screen.ensureSeparator();
|
|
11452
11964
|
}
|
|
11453
11965
|
};
|
|
@@ -11557,9 +12069,10 @@ async function runSession(term, config, serviceToken, opts, exitHint) {
|
|
|
11557
12069
|
if (resp.error) {
|
|
11558
12070
|
throw new Error(resp.error.message);
|
|
11559
12071
|
}
|
|
11560
|
-
const
|
|
11561
|
-
|
|
11562
|
-
|
|
12072
|
+
const fields = parseReattachResponse(resp.result);
|
|
12073
|
+
appliedPolicy = fields.appliedPolicy;
|
|
12074
|
+
if (fields.clientId !== void 0) {
|
|
12075
|
+
ownClientId = fields.clientId;
|
|
11563
12076
|
}
|
|
11564
12077
|
} catch (err) {
|
|
11565
12078
|
attachErr = err;
|
|
@@ -11685,6 +12198,95 @@ function writeDebugLine(payload) {
|
|
|
11685
12198
|
} catch {
|
|
11686
12199
|
}
|
|
11687
12200
|
}
|
|
12201
|
+
function createInstallStatusLine(term, baseLabel) {
|
|
12202
|
+
let finalized = false;
|
|
12203
|
+
let lastText = "";
|
|
12204
|
+
let osc94Active = false;
|
|
12205
|
+
const writeOsc94 = (state) => {
|
|
12206
|
+
if (finalized) {
|
|
12207
|
+
return;
|
|
12208
|
+
}
|
|
12209
|
+
if (state === 3 && osc94Active) {
|
|
12210
|
+
return;
|
|
12211
|
+
}
|
|
12212
|
+
if (state === 0 && !osc94Active) {
|
|
12213
|
+
return;
|
|
12214
|
+
}
|
|
12215
|
+
osc94Active = state === 3;
|
|
12216
|
+
process.stdout.write(`\x1B]9;4;${state}\x1B\\`);
|
|
12217
|
+
};
|
|
12218
|
+
const redraw = (text) => {
|
|
12219
|
+
if (finalized) {
|
|
12220
|
+
return;
|
|
12221
|
+
}
|
|
12222
|
+
process.stdout.write("\r");
|
|
12223
|
+
term.eraseLineAfter();
|
|
12224
|
+
term.brightYellow(text);
|
|
12225
|
+
lastText = text;
|
|
12226
|
+
};
|
|
12227
|
+
const formatProgressText = (event) => {
|
|
12228
|
+
const idVer = `${event.agentId}@${event.version}`;
|
|
12229
|
+
if (event.source === "npm") {
|
|
12230
|
+
if (event.phase === "install_start" || event.phase === "download_start") {
|
|
12231
|
+
return `${baseLabel} installing ${idVer} via npm\u2026`;
|
|
12232
|
+
}
|
|
12233
|
+
if (event.phase === "installed") {
|
|
12234
|
+
return `${baseLabel} ${idVer} installed`;
|
|
12235
|
+
}
|
|
12236
|
+
return `${baseLabel} installing ${idVer} via npm\u2026`;
|
|
12237
|
+
}
|
|
12238
|
+
if (event.phase === "download_start" || event.phase === "download_progress") {
|
|
12239
|
+
const received = event.receivedBytes ?? 0;
|
|
12240
|
+
const total = event.totalBytes ?? 0;
|
|
12241
|
+
const rxMb = (received / 1e6).toFixed(1);
|
|
12242
|
+
if (total > 0) {
|
|
12243
|
+
const totalMb = (total / 1e6).toFixed(1);
|
|
12244
|
+
const pct = Math.min(100, Math.floor(received / total * 100));
|
|
12245
|
+
return `${baseLabel} downloading ${idVer} ${rxMb}/${totalMb} MB (${pct}%)`;
|
|
12246
|
+
}
|
|
12247
|
+
return `${baseLabel} downloading ${idVer} ${rxMb} MB`;
|
|
12248
|
+
}
|
|
12249
|
+
if (event.phase === "download_done") {
|
|
12250
|
+
return `${baseLabel} downloaded ${idVer}, verifying\u2026`;
|
|
12251
|
+
}
|
|
12252
|
+
if (event.phase === "extract") {
|
|
12253
|
+
return `${baseLabel} extracting ${idVer}\u2026`;
|
|
12254
|
+
}
|
|
12255
|
+
if (event.phase === "installed") {
|
|
12256
|
+
return `${baseLabel} ${idVer} installed`;
|
|
12257
|
+
}
|
|
12258
|
+
return lastText || baseLabel;
|
|
12259
|
+
};
|
|
12260
|
+
return {
|
|
12261
|
+
write(text) {
|
|
12262
|
+
if (finalized) {
|
|
12263
|
+
return;
|
|
12264
|
+
}
|
|
12265
|
+
term.brightYellow(text);
|
|
12266
|
+
lastText = text;
|
|
12267
|
+
},
|
|
12268
|
+
applyProgress(event) {
|
|
12269
|
+
if (finalized) {
|
|
12270
|
+
return;
|
|
12271
|
+
}
|
|
12272
|
+
const isActive = event.phase === "download_start" || event.phase === "download_progress" || event.phase === "install_start" || event.phase === "extract" || event.phase === "download_done";
|
|
12273
|
+
if (isActive) {
|
|
12274
|
+
writeOsc94(3);
|
|
12275
|
+
} else if (event.phase === "installed") {
|
|
12276
|
+
writeOsc94(0);
|
|
12277
|
+
}
|
|
12278
|
+
redraw(formatProgressText(event));
|
|
12279
|
+
},
|
|
12280
|
+
finalize() {
|
|
12281
|
+
if (finalized) {
|
|
12282
|
+
return;
|
|
12283
|
+
}
|
|
12284
|
+
finalized = true;
|
|
12285
|
+
writeOsc94(0);
|
|
12286
|
+
process.stdout.write("\n");
|
|
12287
|
+
}
|
|
12288
|
+
};
|
|
12289
|
+
}
|
|
11688
12290
|
function rotateIfBig(target) {
|
|
11689
12291
|
try {
|
|
11690
12292
|
const stat4 = statSync(target);
|
|
@@ -11695,7 +12297,7 @@ function rotateIfBig(target) {
|
|
|
11695
12297
|
} catch {
|
|
11696
12298
|
}
|
|
11697
12299
|
}
|
|
11698
|
-
var HELP_ENTRIES_TAIL, logMaxBytes;
|
|
12300
|
+
var STALL_THRESHOLD_MS, HELP_ENTRIES_TAIL, logMaxBytes;
|
|
11699
12301
|
var init_app = __esm({
|
|
11700
12302
|
"src/tui/app.ts"() {
|
|
11701
12303
|
"use strict";
|
|
@@ -11717,8 +12319,10 @@ var init_app = __esm({
|
|
|
11717
12319
|
init_attachments();
|
|
11718
12320
|
init_clipboard();
|
|
11719
12321
|
init_completion();
|
|
12322
|
+
init_reconnect_state();
|
|
11720
12323
|
init_render_update();
|
|
11721
12324
|
init_format();
|
|
12325
|
+
STALL_THRESHOLD_MS = 12e4;
|
|
11722
12326
|
HELP_ENTRIES_TAIL = [
|
|
11723
12327
|
["Alt+Enter", "newline in prompt"],
|
|
11724
12328
|
["Shift+Tab", "cycle agent modes (plan / accept-edits / etc.)"],
|
|
@@ -11880,6 +12484,7 @@ init_config();
|
|
|
11880
12484
|
init_service_token();
|
|
11881
12485
|
import * as fsp7 from "fs/promises";
|
|
11882
12486
|
import { setTimeout as sleep2 } from "timers/promises";
|
|
12487
|
+
import chalk from "chalk";
|
|
11883
12488
|
|
|
11884
12489
|
// src/daemon/server.ts
|
|
11885
12490
|
init_config();
|
|
@@ -11948,8 +12553,10 @@ async function ensureBinary(args) {
|
|
|
11948
12553
|
}
|
|
11949
12554
|
await downloadAndExtract({
|
|
11950
12555
|
agentId: args.agentId,
|
|
12556
|
+
version: args.version,
|
|
11951
12557
|
archiveUrl: args.target.archive,
|
|
11952
|
-
installDir
|
|
12558
|
+
installDir,
|
|
12559
|
+
onProgress: args.onProgress
|
|
11953
12560
|
});
|
|
11954
12561
|
if (!await fileExists(cmdPath)) {
|
|
11955
12562
|
throw new Error(
|
|
@@ -11969,9 +12576,16 @@ async function downloadAndExtract(args) {
|
|
|
11969
12576
|
const archivePath = await downloadTo({
|
|
11970
12577
|
url: args.archiveUrl,
|
|
11971
12578
|
dir: tempDir,
|
|
11972
|
-
agentId: args.agentId
|
|
12579
|
+
agentId: args.agentId,
|
|
12580
|
+
version: args.version,
|
|
12581
|
+
onProgress: args.onProgress
|
|
11973
12582
|
});
|
|
11974
12583
|
logSink(`hydra-acp: extracting ${args.agentId}`);
|
|
12584
|
+
safeEmit(args.onProgress, {
|
|
12585
|
+
phase: "extract",
|
|
12586
|
+
agentId: args.agentId,
|
|
12587
|
+
version: args.version
|
|
12588
|
+
});
|
|
11975
12589
|
await extract(archivePath, tempDir);
|
|
11976
12590
|
await fsp.unlink(archivePath).catch(() => void 0);
|
|
11977
12591
|
try {
|
|
@@ -11982,16 +12596,35 @@ async function downloadAndExtract(args) {
|
|
|
11982
12596
|
await fsp.rm(tempDir, { recursive: true, force: true }).catch(
|
|
11983
12597
|
() => void 0
|
|
11984
12598
|
);
|
|
12599
|
+
safeEmit(args.onProgress, {
|
|
12600
|
+
phase: "installed",
|
|
12601
|
+
agentId: args.agentId,
|
|
12602
|
+
version: args.version
|
|
12603
|
+
});
|
|
11985
12604
|
return;
|
|
11986
12605
|
}
|
|
11987
12606
|
throw err;
|
|
11988
12607
|
}
|
|
11989
12608
|
logSink(`hydra-acp: installed ${args.agentId} to ${args.installDir}`);
|
|
12609
|
+
safeEmit(args.onProgress, {
|
|
12610
|
+
phase: "installed",
|
|
12611
|
+
agentId: args.agentId,
|
|
12612
|
+
version: args.version
|
|
12613
|
+
});
|
|
11990
12614
|
} catch (err) {
|
|
11991
12615
|
await fsp.rm(tempDir, { recursive: true, force: true }).catch(() => void 0);
|
|
11992
12616
|
throw err;
|
|
11993
12617
|
}
|
|
11994
12618
|
}
|
|
12619
|
+
function safeEmit(cb, event) {
|
|
12620
|
+
if (!cb) {
|
|
12621
|
+
return;
|
|
12622
|
+
}
|
|
12623
|
+
try {
|
|
12624
|
+
cb(event);
|
|
12625
|
+
} catch {
|
|
12626
|
+
}
|
|
12627
|
+
}
|
|
11995
12628
|
async function downloadTo(args) {
|
|
11996
12629
|
const filename = inferArchiveName(args.url);
|
|
11997
12630
|
const dest = path2.join(args.dir, filename);
|
|
@@ -12004,17 +12637,34 @@ async function downloadTo(args) {
|
|
|
12004
12637
|
const total = Number(response.headers.get("content-length") ?? "0");
|
|
12005
12638
|
const out = fs4.createWriteStream(dest);
|
|
12006
12639
|
const nodeStream = Readable.fromWeb(response.body);
|
|
12640
|
+
safeEmit(args.onProgress, {
|
|
12641
|
+
phase: "download_start",
|
|
12642
|
+
agentId: args.agentId,
|
|
12643
|
+
version: args.version,
|
|
12644
|
+
totalBytes: total
|
|
12645
|
+
});
|
|
12007
12646
|
let received = 0;
|
|
12008
|
-
let
|
|
12009
|
-
|
|
12647
|
+
let lastLogEmit = Date.now();
|
|
12648
|
+
let lastCbEmit = 0;
|
|
12649
|
+
const LOG_INTERVAL_MS = 2e3;
|
|
12650
|
+
const CB_INTERVAL_MS = 150;
|
|
12010
12651
|
nodeStream.on("data", (chunk) => {
|
|
12011
12652
|
received += chunk.length;
|
|
12012
12653
|
const now = Date.now();
|
|
12013
|
-
if (now -
|
|
12014
|
-
|
|
12654
|
+
if (now - lastCbEmit >= CB_INTERVAL_MS) {
|
|
12655
|
+
lastCbEmit = now;
|
|
12656
|
+
safeEmit(args.onProgress, {
|
|
12657
|
+
phase: "download_progress",
|
|
12658
|
+
agentId: args.agentId,
|
|
12659
|
+
version: args.version,
|
|
12660
|
+
receivedBytes: received,
|
|
12661
|
+
totalBytes: total
|
|
12662
|
+
});
|
|
12663
|
+
}
|
|
12664
|
+
if (now - lastLogEmit >= LOG_INTERVAL_MS) {
|
|
12665
|
+
lastLogEmit = now;
|
|
12666
|
+
logSink(formatProgress(args.agentId, received, total));
|
|
12015
12667
|
}
|
|
12016
|
-
lastEmit = now;
|
|
12017
|
-
logSink(formatProgress(args.agentId, received, total));
|
|
12018
12668
|
});
|
|
12019
12669
|
await new Promise((resolve5, reject) => {
|
|
12020
12670
|
nodeStream.on("error", reject);
|
|
@@ -12029,6 +12679,13 @@ async function downloadTo(args) {
|
|
|
12029
12679
|
/* done */
|
|
12030
12680
|
true
|
|
12031
12681
|
));
|
|
12682
|
+
safeEmit(args.onProgress, {
|
|
12683
|
+
phase: "download_done",
|
|
12684
|
+
agentId: args.agentId,
|
|
12685
|
+
version: args.version,
|
|
12686
|
+
receivedBytes: received,
|
|
12687
|
+
totalBytes: total
|
|
12688
|
+
});
|
|
12032
12689
|
return dest;
|
|
12033
12690
|
}
|
|
12034
12691
|
function formatProgress(agentId, received, total, done = false) {
|
|
@@ -12128,9 +12785,11 @@ async function ensureNpmPackage(args) {
|
|
|
12128
12785
|
}
|
|
12129
12786
|
await installInto({
|
|
12130
12787
|
agentId: args.agentId,
|
|
12788
|
+
version: args.version,
|
|
12131
12789
|
packageSpec: args.packageSpec,
|
|
12132
12790
|
installDir,
|
|
12133
|
-
registry: args.registry
|
|
12791
|
+
registry: args.registry,
|
|
12792
|
+
onProgress: args.onProgress
|
|
12134
12793
|
});
|
|
12135
12794
|
if (!await fileExists2(binPath)) {
|
|
12136
12795
|
throw new Error(
|
|
@@ -12146,6 +12805,12 @@ async function installInto(args) {
|
|
|
12146
12805
|
logSink2(
|
|
12147
12806
|
`hydra-acp: installing ${args.packageSpec} for ${args.agentId} into ${tempDir}`
|
|
12148
12807
|
);
|
|
12808
|
+
safeEmit2(args.onProgress, {
|
|
12809
|
+
phase: "install_start",
|
|
12810
|
+
agentId: args.agentId,
|
|
12811
|
+
version: args.version,
|
|
12812
|
+
packageSpec: args.packageSpec
|
|
12813
|
+
});
|
|
12149
12814
|
await runNpmInstall({
|
|
12150
12815
|
packageSpec: args.packageSpec,
|
|
12151
12816
|
cwd: tempDir,
|
|
@@ -12159,11 +12824,21 @@ async function installInto(args) {
|
|
|
12159
12824
|
await fsp2.rm(tempDir, { recursive: true, force: true }).catch(
|
|
12160
12825
|
() => void 0
|
|
12161
12826
|
);
|
|
12827
|
+
safeEmit2(args.onProgress, {
|
|
12828
|
+
phase: "installed",
|
|
12829
|
+
agentId: args.agentId,
|
|
12830
|
+
version: args.version
|
|
12831
|
+
});
|
|
12162
12832
|
return;
|
|
12163
12833
|
}
|
|
12164
12834
|
throw err;
|
|
12165
12835
|
}
|
|
12166
12836
|
logSink2(`hydra-acp: installed ${args.agentId} to ${args.installDir}`);
|
|
12837
|
+
safeEmit2(args.onProgress, {
|
|
12838
|
+
phase: "installed",
|
|
12839
|
+
agentId: args.agentId,
|
|
12840
|
+
version: args.version
|
|
12841
|
+
});
|
|
12167
12842
|
} catch (err) {
|
|
12168
12843
|
await fsp2.rm(tempDir, { recursive: true, force: true }).catch(
|
|
12169
12844
|
() => void 0
|
|
@@ -12171,44 +12846,87 @@ async function installInto(args) {
|
|
|
12171
12846
|
throw err;
|
|
12172
12847
|
}
|
|
12173
12848
|
}
|
|
12849
|
+
function safeEmit2(cb, event) {
|
|
12850
|
+
if (!cb) {
|
|
12851
|
+
return;
|
|
12852
|
+
}
|
|
12853
|
+
try {
|
|
12854
|
+
cb(event);
|
|
12855
|
+
} catch {
|
|
12856
|
+
}
|
|
12857
|
+
}
|
|
12858
|
+
var ETXTBSY_RETRIES = 5;
|
|
12859
|
+
var ETXTBSY_BACKOFF_MS = 25;
|
|
12174
12860
|
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();
|
|
12861
|
+
return runNpmInstallOnce(args, 0);
|
|
12862
|
+
}
|
|
12863
|
+
async function runNpmInstallOnce(args, attempt) {
|
|
12864
|
+
try {
|
|
12865
|
+
await new Promise((resolve5, reject) => {
|
|
12866
|
+
const registryArgs = args.registry ? ["--registry", args.registry] : [];
|
|
12867
|
+
let child;
|
|
12868
|
+
try {
|
|
12869
|
+
child = spawn2(
|
|
12870
|
+
"npm",
|
|
12871
|
+
[
|
|
12872
|
+
"install",
|
|
12873
|
+
"--no-audit",
|
|
12874
|
+
"--no-fund",
|
|
12875
|
+
"--silent",
|
|
12876
|
+
...registryArgs,
|
|
12877
|
+
args.packageSpec
|
|
12878
|
+
],
|
|
12879
|
+
{ cwd: args.cwd, stdio: ["ignore", "pipe", "pipe"] }
|
|
12880
|
+
);
|
|
12881
|
+
} catch (err) {
|
|
12882
|
+
reject(err);
|
|
12200
12883
|
return;
|
|
12201
12884
|
}
|
|
12202
|
-
|
|
12203
|
-
|
|
12204
|
-
|
|
12205
|
-
|
|
12206
|
-
|
|
12885
|
+
let stderrTail = "";
|
|
12886
|
+
child.stdout?.on("data", (chunk) => {
|
|
12887
|
+
void chunk;
|
|
12888
|
+
});
|
|
12889
|
+
child.stderr?.setEncoding("utf8");
|
|
12890
|
+
child.stderr?.on("data", (chunk) => {
|
|
12891
|
+
stderrTail = (stderrTail + chunk).slice(-4096);
|
|
12892
|
+
});
|
|
12893
|
+
child.on("error", (err) => {
|
|
12894
|
+
const e = err;
|
|
12895
|
+
if (e.code === "ENOENT") {
|
|
12896
|
+
reject(
|
|
12897
|
+
new Error(
|
|
12898
|
+
`npm not found on PATH (install Node.js / npm, or use a binary-distributed agent)`
|
|
12899
|
+
)
|
|
12900
|
+
);
|
|
12901
|
+
return;
|
|
12902
|
+
}
|
|
12903
|
+
reject(err);
|
|
12904
|
+
});
|
|
12905
|
+
child.on("exit", (code, signal) => {
|
|
12906
|
+
if (code === 0) {
|
|
12907
|
+
resolve5();
|
|
12908
|
+
return;
|
|
12909
|
+
}
|
|
12910
|
+
const reason = code !== null ? `exit code ${code}` : `signal ${signal ?? "unknown"}`;
|
|
12911
|
+
const tail = stderrTail.trim();
|
|
12912
|
+
reject(
|
|
12913
|
+
new Error(
|
|
12914
|
+
tail ? `npm install ${args.packageSpec} failed (${reason})
|
|
12207
12915
|
stderr: ${tail}` : `npm install ${args.packageSpec} failed (${reason})`
|
|
12208
|
-
|
|
12209
|
-
|
|
12916
|
+
)
|
|
12917
|
+
);
|
|
12918
|
+
});
|
|
12210
12919
|
});
|
|
12211
|
-
})
|
|
12920
|
+
} catch (err) {
|
|
12921
|
+
const code = err.code;
|
|
12922
|
+
if (code === "ETXTBSY" && attempt < ETXTBSY_RETRIES) {
|
|
12923
|
+
await new Promise(
|
|
12924
|
+
(r) => setTimeout(r, ETXTBSY_BACKOFF_MS * (attempt + 1))
|
|
12925
|
+
);
|
|
12926
|
+
return runNpmInstallOnce(args, attempt + 1);
|
|
12927
|
+
}
|
|
12928
|
+
throw err;
|
|
12929
|
+
}
|
|
12212
12930
|
}
|
|
12213
12931
|
async function fileExists2(p) {
|
|
12214
12932
|
try {
|
|
@@ -12406,12 +13124,14 @@ async function planSpawn(agent, callerArgs = [], options = {}) {
|
|
|
12406
13124
|
};
|
|
12407
13125
|
}
|
|
12408
13126
|
const bin = npx.bin ?? npxPackageBasename(agent) ?? npx.package;
|
|
13127
|
+
const npmCb = options.onInstallProgress;
|
|
12409
13128
|
const binPath = await ensureNpmPackage({
|
|
12410
13129
|
agentId: agent.id,
|
|
12411
13130
|
version,
|
|
12412
13131
|
packageSpec: npx.package,
|
|
12413
13132
|
bin,
|
|
12414
|
-
registry: options.npmRegistry
|
|
13133
|
+
registry: options.npmRegistry,
|
|
13134
|
+
onProgress: npmCb ? (e) => npmCb({ source: "npm", ...e }) : void 0
|
|
12415
13135
|
});
|
|
12416
13136
|
return {
|
|
12417
13137
|
command: binPath,
|
|
@@ -12427,10 +13147,12 @@ async function planSpawn(agent, callerArgs = [], options = {}) {
|
|
|
12427
13147
|
`Agent ${agent.id} has no binary distribution for ${currentPlatformKey() ?? "this platform"}.`
|
|
12428
13148
|
);
|
|
12429
13149
|
}
|
|
13150
|
+
const binCb = options.onInstallProgress;
|
|
12430
13151
|
const cmdPath = await ensureBinary({
|
|
12431
13152
|
agentId: agent.id,
|
|
12432
13153
|
version,
|
|
12433
|
-
target
|
|
13154
|
+
target,
|
|
13155
|
+
onProgress: binCb ? (e) => binCb({ source: "binary", ...e }) : void 0
|
|
12434
13156
|
});
|
|
12435
13157
|
const tail = callerArgs.length > 0 ? callerArgs : target.args ?? [];
|
|
12436
13158
|
return {
|
|
@@ -12848,7 +13570,8 @@ var SessionManager = class {
|
|
|
12848
13570
|
cwd: params.cwd,
|
|
12849
13571
|
agentArgs: params.agentArgs,
|
|
12850
13572
|
mcpServers: params.mcpServers,
|
|
12851
|
-
model: params.model
|
|
13573
|
+
model: params.model,
|
|
13574
|
+
onInstallProgress: params.onInstallProgress
|
|
12852
13575
|
});
|
|
12853
13576
|
const session = new Session({
|
|
12854
13577
|
cwd: params.cwd,
|
|
@@ -12865,7 +13588,8 @@ var SessionManager = class {
|
|
|
12865
13588
|
historyMaxEntries: this.sessionHistoryMaxEntries,
|
|
12866
13589
|
currentModel: fresh.initialModel,
|
|
12867
13590
|
currentMode: fresh.initialMode,
|
|
12868
|
-
agentModes: fresh.initialModes
|
|
13591
|
+
agentModes: fresh.initialModes,
|
|
13592
|
+
agentModels: fresh.initialModels
|
|
12869
13593
|
});
|
|
12870
13594
|
await this.attachManagerHooks(session);
|
|
12871
13595
|
return session;
|
|
@@ -12910,7 +13634,10 @@ var SessionManager = class {
|
|
|
12910
13634
|
if (params.upstreamSessionId === "") {
|
|
12911
13635
|
return this.doResurrectFromImport(params);
|
|
12912
13636
|
}
|
|
12913
|
-
const plan = await planSpawn(agentDef, params.agentArgs ?? [], {
|
|
13637
|
+
const plan = await planSpawn(agentDef, params.agentArgs ?? [], {
|
|
13638
|
+
npmRegistry: this.npmRegistry,
|
|
13639
|
+
onInstallProgress: params.onInstallProgress
|
|
13640
|
+
});
|
|
12914
13641
|
const agent = this.spawner({
|
|
12915
13642
|
agentId: params.agentId,
|
|
12916
13643
|
cwd: params.cwd,
|
|
@@ -12968,6 +13695,7 @@ var SessionManager = class {
|
|
|
12968
13695
|
currentUsage: params.currentUsage,
|
|
12969
13696
|
agentCommands: params.agentCommands,
|
|
12970
13697
|
agentModes: params.agentModes ?? nonEmptyOrUndefined(extractInitialModes(loadResult ?? {})),
|
|
13698
|
+
agentModels: params.agentModels ?? nonEmptyOrUndefined(extractInitialModels(loadResult ?? {})),
|
|
12971
13699
|
// Only gate the first-prompt title heuristic when we actually have
|
|
12972
13700
|
// a title to preserve. A title-less session (lost to a write race
|
|
12973
13701
|
// or never seeded) should re-derive from the next prompt rather
|
|
@@ -12991,7 +13719,8 @@ var SessionManager = class {
|
|
|
12991
13719
|
agentId: params.agentId,
|
|
12992
13720
|
cwd,
|
|
12993
13721
|
agentArgs: params.agentArgs,
|
|
12994
|
-
mcpServers: []
|
|
13722
|
+
mcpServers: [],
|
|
13723
|
+
onInstallProgress: params.onInstallProgress
|
|
12995
13724
|
});
|
|
12996
13725
|
const session = new Session({
|
|
12997
13726
|
sessionId: params.hydraSessionId,
|
|
@@ -13014,6 +13743,7 @@ var SessionManager = class {
|
|
|
13014
13743
|
currentUsage: params.currentUsage,
|
|
13015
13744
|
agentCommands: params.agentCommands,
|
|
13016
13745
|
agentModes: params.agentModes ?? fresh.initialModes,
|
|
13746
|
+
agentModels: params.agentModels ?? fresh.initialModels,
|
|
13017
13747
|
firstPromptSeeded: !!params.title,
|
|
13018
13748
|
createdAt: params.createdAt ? new Date(params.createdAt).getTime() : void 0
|
|
13019
13749
|
});
|
|
@@ -13043,7 +13773,10 @@ var SessionManager = class {
|
|
|
13043
13773
|
err.code = JsonRpcErrorCodes.AgentNotInstalled;
|
|
13044
13774
|
throw err;
|
|
13045
13775
|
}
|
|
13046
|
-
const plan = await planSpawn(agentDef, params.agentArgs ?? [], {
|
|
13776
|
+
const plan = await planSpawn(agentDef, params.agentArgs ?? [], {
|
|
13777
|
+
npmRegistry: this.npmRegistry,
|
|
13778
|
+
onInstallProgress: params.onInstallProgress
|
|
13779
|
+
});
|
|
13047
13780
|
const agent = this.spawner({
|
|
13048
13781
|
agentId: params.agentId,
|
|
13049
13782
|
cwd: params.cwd,
|
|
@@ -13069,15 +13802,25 @@ var SessionManager = class {
|
|
|
13069
13802
|
);
|
|
13070
13803
|
}
|
|
13071
13804
|
let initialModel = extractInitialModel(newResult);
|
|
13805
|
+
const initialModels = extractInitialModels(newResult);
|
|
13072
13806
|
const desired = params.model ?? this.defaultModels[params.agentId];
|
|
13073
13807
|
if (desired && desired !== initialModel) {
|
|
13074
|
-
|
|
13075
|
-
|
|
13076
|
-
|
|
13077
|
-
|
|
13078
|
-
|
|
13079
|
-
|
|
13080
|
-
|
|
13808
|
+
const validates = initialModels.length === 0 || initialModels.some((m) => m.modelId === desired);
|
|
13809
|
+
if (validates) {
|
|
13810
|
+
try {
|
|
13811
|
+
await agent.connection.request("session/set_model", {
|
|
13812
|
+
sessionId: sessionIdRaw,
|
|
13813
|
+
modelId: desired
|
|
13814
|
+
});
|
|
13815
|
+
initialModel = desired;
|
|
13816
|
+
} catch {
|
|
13817
|
+
}
|
|
13818
|
+
} else {
|
|
13819
|
+
const known = initialModels.map((m) => m.modelId).join(", ");
|
|
13820
|
+
process.stderr.write(
|
|
13821
|
+
`hydra-acp: defaultModels[${params.agentId}]=${JSON.stringify(desired)} is not in the agent's availableModels (${known}); skipping session/set_model
|
|
13822
|
+
`
|
|
13823
|
+
);
|
|
13081
13824
|
}
|
|
13082
13825
|
}
|
|
13083
13826
|
const initialModes = extractInitialModes(newResult);
|
|
@@ -13087,6 +13830,7 @@ var SessionManager = class {
|
|
|
13087
13830
|
upstreamSessionId: sessionIdRaw,
|
|
13088
13831
|
agentMeta: newResult._meta,
|
|
13089
13832
|
initialModel,
|
|
13833
|
+
initialModels: initialModels.length > 0 ? initialModels : void 0,
|
|
13090
13834
|
initialModes: initialModes.length > 0 ? initialModes : void 0,
|
|
13091
13835
|
initialMode
|
|
13092
13836
|
};
|
|
@@ -13149,6 +13893,15 @@ var SessionManager = class {
|
|
|
13149
13893
|
}))
|
|
13150
13894
|
}).catch(() => void 0);
|
|
13151
13895
|
});
|
|
13896
|
+
session.onAgentModelsChange((models) => {
|
|
13897
|
+
void this.persistSnapshot(session.sessionId, {
|
|
13898
|
+
agentModels: models.map((m) => ({
|
|
13899
|
+
modelId: m.modelId,
|
|
13900
|
+
...m.name !== void 0 ? { name: m.name } : {},
|
|
13901
|
+
...m.description !== void 0 ? { description: m.description } : {}
|
|
13902
|
+
}))
|
|
13903
|
+
}).catch(() => void 0);
|
|
13904
|
+
});
|
|
13152
13905
|
this.sessions.set(session.sessionId, session);
|
|
13153
13906
|
await this.enqueueMetaWrite(session.sessionId, async () => {
|
|
13154
13907
|
const existing = await this.store.read(session.sessionId);
|
|
@@ -13192,6 +13945,7 @@ var SessionManager = class {
|
|
|
13192
13945
|
currentUsage: persistedUsageToSnapshot(record.currentUsage),
|
|
13193
13946
|
agentCommands: record.agentCommands,
|
|
13194
13947
|
agentModes: record.agentModes,
|
|
13948
|
+
agentModels: record.agentModels,
|
|
13195
13949
|
createdAt: record.createdAt
|
|
13196
13950
|
};
|
|
13197
13951
|
}
|
|
@@ -13435,6 +14189,26 @@ var SessionManager = class {
|
|
|
13435
14189
|
const record = await this.store.read(sessionId).catch(() => void 0);
|
|
13436
14190
|
return record !== void 0;
|
|
13437
14191
|
}
|
|
14192
|
+
// Public retitle entry point that works on live AND cold sessions.
|
|
14193
|
+
// - Live: routes through Session.retitle so attached clients receive
|
|
14194
|
+
// a session_info_update broadcast (and persistTitle fires from the
|
|
14195
|
+
// onTitleChange handler, just like /hydra title).
|
|
14196
|
+
// - Cold: writes the new title straight into meta.json — there's
|
|
14197
|
+
// nothing in memory to broadcast to, but a later resurrect / list
|
|
14198
|
+
// will pick up the new title.
|
|
14199
|
+
// Returns false when no record exists at all (live or on disk).
|
|
14200
|
+
async setTitle(sessionId, title) {
|
|
14201
|
+
const live = this.get(sessionId);
|
|
14202
|
+
if (live) {
|
|
14203
|
+
await live.retitle(title);
|
|
14204
|
+
return true;
|
|
14205
|
+
}
|
|
14206
|
+
if (!await this.hasRecord(sessionId)) {
|
|
14207
|
+
return false;
|
|
14208
|
+
}
|
|
14209
|
+
await this.persistTitle(sessionId, title);
|
|
14210
|
+
return true;
|
|
14211
|
+
}
|
|
13438
14212
|
// Persist a title update from Session.setTitle. The on-disk record
|
|
13439
14213
|
// was written at create time; updating it here keeps the session
|
|
13440
14214
|
// record's title in sync with what was broadcast to clients so a
|
|
@@ -13487,6 +14261,7 @@ var SessionManager = class {
|
|
|
13487
14261
|
...update.currentUsage !== void 0 ? { currentUsage: update.currentUsage } : {},
|
|
13488
14262
|
...update.agentCommands !== void 0 ? { agentCommands: update.agentCommands } : {},
|
|
13489
14263
|
...update.agentModes !== void 0 ? { agentModes: update.agentModes } : {},
|
|
14264
|
+
...update.agentModels !== void 0 ? { agentModels: update.agentModels } : {},
|
|
13490
14265
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
13491
14266
|
});
|
|
13492
14267
|
});
|
|
@@ -13588,6 +14363,18 @@ function mergeForPersistence(session, existing) {
|
|
|
13588
14363
|
return out;
|
|
13589
14364
|
}) : void 0;
|
|
13590
14365
|
const agentModes = persistedModes ?? existing?.agentModes;
|
|
14366
|
+
const sessionModels = session.availableModels();
|
|
14367
|
+
const persistedModels = sessionModels.length > 0 ? sessionModels.map((m) => {
|
|
14368
|
+
const out = { modelId: m.modelId };
|
|
14369
|
+
if (m.name !== void 0) {
|
|
14370
|
+
out.name = m.name;
|
|
14371
|
+
}
|
|
14372
|
+
if (m.description !== void 0) {
|
|
14373
|
+
out.description = m.description;
|
|
14374
|
+
}
|
|
14375
|
+
return out;
|
|
14376
|
+
}) : void 0;
|
|
14377
|
+
const agentModels = persistedModels ?? existing?.agentModels;
|
|
13591
14378
|
return recordFromMemorySession({
|
|
13592
14379
|
sessionId: session.sessionId,
|
|
13593
14380
|
lineageId: existing?.lineageId ?? generateLineageId(),
|
|
@@ -13604,6 +14391,7 @@ function mergeForPersistence(session, existing) {
|
|
|
13604
14391
|
currentUsage: usageSnapshotToPersisted(session.currentUsage) ?? existing?.currentUsage,
|
|
13605
14392
|
agentCommands,
|
|
13606
14393
|
agentModes,
|
|
14394
|
+
agentModels,
|
|
13607
14395
|
createdAt: existing?.createdAt ?? new Date(session.createdAt).toISOString()
|
|
13608
14396
|
});
|
|
13609
14397
|
}
|
|
@@ -13669,6 +14457,40 @@ function asString(value) {
|
|
|
13669
14457
|
function nonEmptyOrUndefined(arr) {
|
|
13670
14458
|
return arr.length > 0 ? arr : void 0;
|
|
13671
14459
|
}
|
|
14460
|
+
function extractInitialModels(result) {
|
|
14461
|
+
const direct = parseModelsList(result.availableModels);
|
|
14462
|
+
if (direct.length > 0) {
|
|
14463
|
+
return direct;
|
|
14464
|
+
}
|
|
14465
|
+
const models = result.models;
|
|
14466
|
+
if (models && typeof models === "object" && !Array.isArray(models)) {
|
|
14467
|
+
const fromModelsObj = parseModelsList(
|
|
14468
|
+
models.availableModels
|
|
14469
|
+
);
|
|
14470
|
+
if (fromModelsObj.length > 0) {
|
|
14471
|
+
return fromModelsObj;
|
|
14472
|
+
}
|
|
14473
|
+
}
|
|
14474
|
+
const meta = result._meta;
|
|
14475
|
+
if (meta && typeof meta === "object" && !Array.isArray(meta)) {
|
|
14476
|
+
for (const [key, value] of Object.entries(
|
|
14477
|
+
meta
|
|
14478
|
+
)) {
|
|
14479
|
+
if (key === "hydra-acp") {
|
|
14480
|
+
continue;
|
|
14481
|
+
}
|
|
14482
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
14483
|
+
const fromMeta = parseModelsList(
|
|
14484
|
+
value.availableModels
|
|
14485
|
+
);
|
|
14486
|
+
if (fromMeta.length > 0) {
|
|
14487
|
+
return fromMeta;
|
|
14488
|
+
}
|
|
14489
|
+
}
|
|
14490
|
+
}
|
|
14491
|
+
}
|
|
14492
|
+
return [];
|
|
14493
|
+
}
|
|
13672
14494
|
function extractInitialModes(result) {
|
|
13673
14495
|
const direct = parseModesList(result.availableModes);
|
|
13674
14496
|
if (direct.length > 0) {
|
|
@@ -14646,6 +15468,35 @@ function registerSessionRoutes(app, manager, defaults) {
|
|
|
14646
15468
|
}
|
|
14647
15469
|
reply.code(204).send();
|
|
14648
15470
|
});
|
|
15471
|
+
app.patch("/v1/sessions/:id", async (request, reply) => {
|
|
15472
|
+
const raw = request.params.id;
|
|
15473
|
+
const id = await manager.resolveCanonicalId(raw) ?? raw;
|
|
15474
|
+
const body = request.body ?? {};
|
|
15475
|
+
if (body.regen === true) {
|
|
15476
|
+
const session = manager.get(id);
|
|
15477
|
+
if (!session) {
|
|
15478
|
+
reply.code(409).send({ error: "regen requires a live session" });
|
|
15479
|
+
return;
|
|
15480
|
+
}
|
|
15481
|
+
void session.retitleFromAgent().catch((err) => {
|
|
15482
|
+
app.log.warn(
|
|
15483
|
+
`title regen failed for ${id}: ${err.message}`
|
|
15484
|
+
);
|
|
15485
|
+
});
|
|
15486
|
+
reply.code(202).send();
|
|
15487
|
+
return;
|
|
15488
|
+
}
|
|
15489
|
+
if (typeof body.title !== "string" || body.title.trim().length === 0) {
|
|
15490
|
+
reply.code(400).send({ error: "title must be a non-empty string" });
|
|
15491
|
+
return;
|
|
15492
|
+
}
|
|
15493
|
+
const ok = await manager.setTitle(id, body.title);
|
|
15494
|
+
if (!ok) {
|
|
15495
|
+
reply.code(404).send({ error: "session not found" });
|
|
15496
|
+
return;
|
|
15497
|
+
}
|
|
15498
|
+
reply.code(204).send();
|
|
15499
|
+
});
|
|
14649
15500
|
app.delete("/v1/sessions/:id", async (request, reply) => {
|
|
14650
15501
|
const raw = request.params.id;
|
|
14651
15502
|
const id = await manager.resolveCanonicalId(raw) ?? raw;
|
|
@@ -15188,7 +16039,8 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
15188
16039
|
mcpServers: params.mcpServers,
|
|
15189
16040
|
title: hydraMeta.name,
|
|
15190
16041
|
agentArgs: hydraMeta.agentArgs,
|
|
15191
|
-
model: hydraMeta.model
|
|
16042
|
+
model: hydraMeta.model,
|
|
16043
|
+
onInstallProgress: makeInstallProgressForwarder(connection)
|
|
15192
16044
|
});
|
|
15193
16045
|
const client = bindClientToSession(connection, session, state);
|
|
15194
16046
|
const { entries: replay } = await session.attach(client, "full");
|
|
@@ -15204,6 +16056,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
15204
16056
|
})();
|
|
15205
16057
|
});
|
|
15206
16058
|
const modesPayload = buildModesPayload(session);
|
|
16059
|
+
const modelsPayload = buildModelsPayload(session);
|
|
15207
16060
|
return {
|
|
15208
16061
|
sessionId: session.sessionId,
|
|
15209
16062
|
// session/new is implicitly an attach; mirror session/attach's
|
|
@@ -15212,6 +16065,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
15212
16065
|
// events without an extra round-trip.
|
|
15213
16066
|
clientId: client.clientId,
|
|
15214
16067
|
...modesPayload ? { modes: modesPayload } : {},
|
|
16068
|
+
...modelsPayload ? { models: modelsPayload } : {},
|
|
15215
16069
|
_meta: buildResponseMeta(session)
|
|
15216
16070
|
};
|
|
15217
16071
|
});
|
|
@@ -15247,7 +16101,10 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
15247
16101
|
err.code = JsonRpcErrorCodes.SessionNotFound;
|
|
15248
16102
|
throw err;
|
|
15249
16103
|
}
|
|
15250
|
-
session = await deps.manager.resurrect(
|
|
16104
|
+
session = await deps.manager.resurrect({
|
|
16105
|
+
...resurrectParams,
|
|
16106
|
+
onInstallProgress: makeInstallProgressForwarder(connection)
|
|
16107
|
+
});
|
|
15251
16108
|
}
|
|
15252
16109
|
const client = bindClientToSession(
|
|
15253
16110
|
connection,
|
|
@@ -15273,6 +16130,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
15273
16130
|
}
|
|
15274
16131
|
session.replayPendingPermissions(client);
|
|
15275
16132
|
const modesPayload = buildModesPayload(session);
|
|
16133
|
+
const modelsPayload = buildModelsPayload(session);
|
|
15276
16134
|
return {
|
|
15277
16135
|
sessionId: session.sessionId,
|
|
15278
16136
|
clientId: client.clientId,
|
|
@@ -15284,6 +16142,7 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
15284
16142
|
historyPolicy: appliedPolicy,
|
|
15285
16143
|
replayed: replay.length,
|
|
15286
16144
|
...modesPayload ? { modes: modesPayload } : {},
|
|
16145
|
+
...modelsPayload ? { models: modelsPayload } : {},
|
|
15287
16146
|
_meta: buildResponseMeta(session)
|
|
15288
16147
|
};
|
|
15289
16148
|
});
|
|
@@ -15439,15 +16298,39 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
15439
16298
|
}
|
|
15440
16299
|
session.replayPendingPermissions(client);
|
|
15441
16300
|
const modesPayload = buildModesPayload(session);
|
|
16301
|
+
const modelsPayload = buildModelsPayload(session);
|
|
15442
16302
|
return {
|
|
15443
16303
|
sessionId: session.sessionId,
|
|
15444
16304
|
// Same as session/new: include clientId so the deferred-echo
|
|
15445
16305
|
// path in queue-aware clients can recognize own broadcasts.
|
|
15446
16306
|
clientId: client.clientId,
|
|
15447
16307
|
...modesPayload ? { modes: modesPayload } : {},
|
|
16308
|
+
...modelsPayload ? { models: modelsPayload } : {},
|
|
15448
16309
|
_meta: buildResponseMeta(session)
|
|
15449
16310
|
};
|
|
15450
16311
|
});
|
|
16312
|
+
connection.onRequest("session/set_model", async (rawParams) => {
|
|
16313
|
+
const decision = decideSetModel(rawParams, deps.manager);
|
|
16314
|
+
if (decision.kind === "error") {
|
|
16315
|
+
app.log.warn(decision.logMessage);
|
|
16316
|
+
const err = new Error(decision.message);
|
|
16317
|
+
err.code = decision.code;
|
|
16318
|
+
throw err;
|
|
16319
|
+
}
|
|
16320
|
+
if (decision.kind === "no_op") {
|
|
16321
|
+
app.log.warn(decision.logMessage);
|
|
16322
|
+
await connection.notify("session/update", {
|
|
16323
|
+
sessionId: decision.sessionId,
|
|
16324
|
+
update: {
|
|
16325
|
+
sessionUpdate: "current_model_update",
|
|
16326
|
+
currentModel: decision.currentModel
|
|
16327
|
+
}
|
|
16328
|
+
}).catch(() => void 0);
|
|
16329
|
+
return null;
|
|
16330
|
+
}
|
|
16331
|
+
app.log.info(decision.logMessage);
|
|
16332
|
+
return decision.session.forwardRequest("session/set_model", rawParams);
|
|
16333
|
+
});
|
|
15451
16334
|
connection.setDefaultHandler(async (rawParams, method) => {
|
|
15452
16335
|
if (!method.startsWith("session/") || rawParams === null || typeof rawParams !== "object") {
|
|
15453
16336
|
const err = new Error(`Method not found: ${method}`);
|
|
@@ -15470,6 +16353,26 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
15470
16353
|
});
|
|
15471
16354
|
});
|
|
15472
16355
|
}
|
|
16356
|
+
function makeInstallProgressForwarder(connection) {
|
|
16357
|
+
return (event) => {
|
|
16358
|
+
const payload = {
|
|
16359
|
+
agentId: event.agentId,
|
|
16360
|
+
version: event.version,
|
|
16361
|
+
source: event.source,
|
|
16362
|
+
phase: event.phase
|
|
16363
|
+
};
|
|
16364
|
+
if ("receivedBytes" in event) {
|
|
16365
|
+
payload.receivedBytes = event.receivedBytes;
|
|
16366
|
+
}
|
|
16367
|
+
if ("totalBytes" in event) {
|
|
16368
|
+
payload.totalBytes = event.totalBytes;
|
|
16369
|
+
}
|
|
16370
|
+
if ("packageSpec" in event) {
|
|
16371
|
+
payload.packageSpec = event.packageSpec;
|
|
16372
|
+
}
|
|
16373
|
+
void connection.notify(AGENT_INSTALL_PROGRESS_METHOD, payload).catch(() => void 0);
|
|
16374
|
+
};
|
|
16375
|
+
}
|
|
15473
16376
|
function buildModesPayload(session) {
|
|
15474
16377
|
const modes = session.availableModes();
|
|
15475
16378
|
if (modes.length === 0) {
|
|
@@ -15490,6 +16393,94 @@ function buildModesPayload(session) {
|
|
|
15490
16393
|
const currentModeId = session.currentMode ?? modes[0].id;
|
|
15491
16394
|
return { currentModeId, availableModes };
|
|
15492
16395
|
}
|
|
16396
|
+
function buildModelsPayload(session) {
|
|
16397
|
+
const models = session.availableModels();
|
|
16398
|
+
if (models.length === 0) {
|
|
16399
|
+
return void 0;
|
|
16400
|
+
}
|
|
16401
|
+
const availableModels = models.map((m) => {
|
|
16402
|
+
const out = {
|
|
16403
|
+
modelId: m.modelId
|
|
16404
|
+
};
|
|
16405
|
+
if (m.name !== void 0) {
|
|
16406
|
+
out.name = m.name;
|
|
16407
|
+
}
|
|
16408
|
+
if (m.description !== void 0) {
|
|
16409
|
+
out.description = m.description;
|
|
16410
|
+
}
|
|
16411
|
+
return out;
|
|
16412
|
+
});
|
|
16413
|
+
const currentModelId = session.currentModel ?? models[0].modelId;
|
|
16414
|
+
return { currentModelId, availableModels };
|
|
16415
|
+
}
|
|
16416
|
+
function decideSetModel(rawParams, manager) {
|
|
16417
|
+
if (!rawParams || typeof rawParams !== "object") {
|
|
16418
|
+
return {
|
|
16419
|
+
kind: "error",
|
|
16420
|
+
code: JsonRpcErrorCodes.InvalidParams,
|
|
16421
|
+
message: "session/set_model requires params",
|
|
16422
|
+
logMessage: "session/set_model rejected: params not an object"
|
|
16423
|
+
};
|
|
16424
|
+
}
|
|
16425
|
+
const params = rawParams;
|
|
16426
|
+
if (typeof params.sessionId !== "string") {
|
|
16427
|
+
return {
|
|
16428
|
+
kind: "error",
|
|
16429
|
+
code: JsonRpcErrorCodes.InvalidParams,
|
|
16430
|
+
message: "session/set_model requires string sessionId",
|
|
16431
|
+
logMessage: "session/set_model rejected: missing/non-string sessionId"
|
|
16432
|
+
};
|
|
16433
|
+
}
|
|
16434
|
+
if (typeof params.modelId !== "string") {
|
|
16435
|
+
return {
|
|
16436
|
+
kind: "error",
|
|
16437
|
+
code: JsonRpcErrorCodes.InvalidParams,
|
|
16438
|
+
message: "session/set_model requires string modelId",
|
|
16439
|
+
logMessage: `session/set_model rejected: missing/non-string modelId sessionId=${params.sessionId}`
|
|
16440
|
+
};
|
|
16441
|
+
}
|
|
16442
|
+
const session = manager.get(params.sessionId);
|
|
16443
|
+
if (!session) {
|
|
16444
|
+
return {
|
|
16445
|
+
kind: "error",
|
|
16446
|
+
code: JsonRpcErrorCodes.SessionNotFound,
|
|
16447
|
+
message: `session ${params.sessionId} not found`,
|
|
16448
|
+
logMessage: `session/set_model rejected: session not found sessionId=${params.sessionId}`
|
|
16449
|
+
};
|
|
16450
|
+
}
|
|
16451
|
+
const advertised = session.availableModels();
|
|
16452
|
+
if (advertised.length === 0) {
|
|
16453
|
+
return {
|
|
16454
|
+
kind: "ok",
|
|
16455
|
+
session,
|
|
16456
|
+
logMessage: `session/set_model passthrough (no availableModels) sessionId=${params.sessionId} modelId=${JSON.stringify(params.modelId)}`
|
|
16457
|
+
};
|
|
16458
|
+
}
|
|
16459
|
+
const match = advertised.find((m) => m.modelId === params.modelId);
|
|
16460
|
+
if (!match) {
|
|
16461
|
+
const known = advertised.map((m) => m.modelId).join(", ");
|
|
16462
|
+
if (session.currentModel !== void 0 && session.currentModel.length > 0) {
|
|
16463
|
+
return {
|
|
16464
|
+
kind: "no_op",
|
|
16465
|
+
session,
|
|
16466
|
+
sessionId: params.sessionId,
|
|
16467
|
+
currentModel: session.currentModel,
|
|
16468
|
+
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}]`
|
|
16469
|
+
};
|
|
16470
|
+
}
|
|
16471
|
+
return {
|
|
16472
|
+
kind: "error",
|
|
16473
|
+
code: JsonRpcErrorCodes.InvalidParams,
|
|
16474
|
+
message: `model "${params.modelId}" is not in this session's availableModels (agent ${session.agentId}); known models: ${known}`,
|
|
16475
|
+
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)`
|
|
16476
|
+
};
|
|
16477
|
+
}
|
|
16478
|
+
return {
|
|
16479
|
+
kind: "ok",
|
|
16480
|
+
session,
|
|
16481
|
+
logMessage: `session/set_model accepted sessionId=${params.sessionId} modelId=${JSON.stringify(params.modelId)}`
|
|
16482
|
+
};
|
|
16483
|
+
}
|
|
15493
16484
|
function buildResponseMeta(session) {
|
|
15494
16485
|
const ours = {
|
|
15495
16486
|
upstreamSessionId: session.upstreamSessionId,
|
|
@@ -15519,6 +16510,10 @@ function buildResponseMeta(session) {
|
|
|
15519
16510
|
if (modes.length > 0) {
|
|
15520
16511
|
ours.availableModes = modes;
|
|
15521
16512
|
}
|
|
16513
|
+
const models = session.availableModels();
|
|
16514
|
+
if (models.length > 0) {
|
|
16515
|
+
ours.availableModels = models;
|
|
16516
|
+
}
|
|
15522
16517
|
if (session.turnStartedAt !== void 0) {
|
|
15523
16518
|
ours.turnStartedAt = session.turnStartedAt;
|
|
15524
16519
|
}
|
|
@@ -15754,6 +16749,7 @@ function ensureLoopbackOrTls(config) {
|
|
|
15754
16749
|
|
|
15755
16750
|
// src/cli/commands/daemon.ts
|
|
15756
16751
|
init_daemon_bootstrap();
|
|
16752
|
+
init_hydra_version();
|
|
15757
16753
|
|
|
15758
16754
|
// src/cli/commands/log-tail.ts
|
|
15759
16755
|
import * as fs16 from "fs";
|
|
@@ -15984,6 +16980,8 @@ async function runDaemonStatus() {
|
|
|
15984
16980
|
const info = await readPidFile();
|
|
15985
16981
|
if (!info) {
|
|
15986
16982
|
process.stdout.write("Daemon: not running\n");
|
|
16983
|
+
process.stdout.write(`CLI version: ${HYDRA_VERSION}
|
|
16984
|
+
`);
|
|
15987
16985
|
return;
|
|
15988
16986
|
}
|
|
15989
16987
|
const alive = isProcessAlive(info.pid);
|
|
@@ -15991,6 +16989,52 @@ async function runDaemonStatus() {
|
|
|
15991
16989
|
`Daemon: ${alive ? "running" : "stale pid file"} pid=${info.pid} host=${info.host} port=${info.port} started=${info.startedAt}
|
|
15992
16990
|
`
|
|
15993
16991
|
);
|
|
16992
|
+
let daemonVersion;
|
|
16993
|
+
if (alive) {
|
|
16994
|
+
try {
|
|
16995
|
+
const config = await loadConfig();
|
|
16996
|
+
daemonVersion = await fetchDaemonVersion(config);
|
|
16997
|
+
} catch {
|
|
16998
|
+
}
|
|
16999
|
+
}
|
|
17000
|
+
if (daemonVersion === void 0) {
|
|
17001
|
+
process.stdout.write(`CLI version: ${HYDRA_VERSION}
|
|
17002
|
+
`);
|
|
17003
|
+
if (alive) {
|
|
17004
|
+
process.stdout.write(
|
|
17005
|
+
"Daemon version: unknown (health endpoint unreachable)\n"
|
|
17006
|
+
);
|
|
17007
|
+
}
|
|
17008
|
+
return;
|
|
17009
|
+
}
|
|
17010
|
+
if (daemonVersion === HYDRA_VERSION) {
|
|
17011
|
+
process.stdout.write(`Version: ${HYDRA_VERSION}
|
|
17012
|
+
`);
|
|
17013
|
+
return;
|
|
17014
|
+
}
|
|
17015
|
+
process.stdout.write(`CLI version: ${HYDRA_VERSION}
|
|
17016
|
+
`);
|
|
17017
|
+
process.stdout.write(`Daemon version: ${daemonVersion}
|
|
17018
|
+
`);
|
|
17019
|
+
process.stdout.write(
|
|
17020
|
+
chalk.yellow(
|
|
17021
|
+
"Version mismatch \u2014 run `hydra-acp daemon restart` to upgrade the daemon.\n"
|
|
17022
|
+
)
|
|
17023
|
+
);
|
|
17024
|
+
}
|
|
17025
|
+
async function fetchDaemonVersion(config) {
|
|
17026
|
+
const protocol = config.daemon.tls ? "https" : "http";
|
|
17027
|
+
const url = `${protocol}://${config.daemon.host}:${config.daemon.port}/v1/health`;
|
|
17028
|
+
try {
|
|
17029
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(1e3) });
|
|
17030
|
+
if (!response.ok) {
|
|
17031
|
+
return void 0;
|
|
17032
|
+
}
|
|
17033
|
+
const body = await response.json();
|
|
17034
|
+
return typeof body.version === "string" ? body.version : void 0;
|
|
17035
|
+
} catch {
|
|
17036
|
+
return void 0;
|
|
17037
|
+
}
|
|
15994
17038
|
}
|
|
15995
17039
|
async function readPidFile() {
|
|
15996
17040
|
try {
|