@hydra-acp/cli 0.1.56 → 0.1.58
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 +40 -51
- package/dist/cli.js +730 -316
- package/dist/index.d.ts +51 -1
- package/dist/index.js +266 -77
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -104,7 +104,11 @@ var paths = {
|
|
|
104
104
|
// line, append-only so concurrent TUIs don't lose each other's
|
|
105
105
|
// writes.
|
|
106
106
|
globalTuiHistoryFile: () => path.join(hydraHome(), "prompt-history"),
|
|
107
|
-
tuiLogFile: () => path.join(hydraHome(), "tui.log")
|
|
107
|
+
tuiLogFile: () => path.join(hydraHome(), "tui.log"),
|
|
108
|
+
// Diagnostic dump of every JSON-RPC message that crosses a `hydra-acp
|
|
109
|
+
// shim` process. Append-only NDJSON. One file shared by every shim;
|
|
110
|
+
// each line carries the writing process's pid for disambiguation.
|
|
111
|
+
shimWireLogFile: () => path.join(hydraHome(), "shim-wire.log")
|
|
108
112
|
};
|
|
109
113
|
|
|
110
114
|
// src/core/service-token.ts
|
|
@@ -1441,6 +1445,12 @@ function extractHydraMeta(meta) {
|
|
|
1441
1445
|
if (typeof obj.mcpStdin === "boolean") {
|
|
1442
1446
|
out.mcpStdin = obj.mcpStdin;
|
|
1443
1447
|
}
|
|
1448
|
+
if (typeof obj.interactive === "boolean") {
|
|
1449
|
+
out.interactive = obj.interactive;
|
|
1450
|
+
}
|
|
1451
|
+
if (typeof obj.ancillary === "boolean") {
|
|
1452
|
+
out.ancillary = obj.ancillary;
|
|
1453
|
+
}
|
|
1444
1454
|
if (typeof obj.promptAmending === "boolean") {
|
|
1445
1455
|
out.promptAmending = obj.promptAmending;
|
|
1446
1456
|
}
|
|
@@ -1561,10 +1571,14 @@ var SessionListEntry = z3.object({
|
|
|
1561
1571
|
// local session, an import is a cross-machine takeover.
|
|
1562
1572
|
forkedFromSessionId: z3.string().optional(),
|
|
1563
1573
|
forkedFromMessageId: z3.string().optional(),
|
|
1564
|
-
// clientInfo from the process that issued session/new.
|
|
1565
|
-
//
|
|
1566
|
-
// override flag surface them.
|
|
1574
|
+
// clientInfo from the process that issued session/new. Carried for
|
|
1575
|
+
// log/display; the effective filtering signal is `interactive` below.
|
|
1567
1576
|
originatingClient: z3.object({ name: z3.string(), version: z3.string().optional() }).optional(),
|
|
1577
|
+
// Tristate filter signal computed by effectiveInteractive(): explicit
|
|
1578
|
+
// when the record stored a value, else inferred (legacy cat hint or
|
|
1579
|
+
// history-presence). Clients can use this to render a hint glyph
|
|
1580
|
+
// (e.g. dim non-interactive rows when the user toggles them in).
|
|
1581
|
+
interactive: z3.boolean().optional(),
|
|
1568
1582
|
updatedAt: z3.string(),
|
|
1569
1583
|
attachedClients: z3.number().int().nonnegative(),
|
|
1570
1584
|
status: z3.enum(["live", "cold"]).default("live"),
|
|
@@ -1622,7 +1636,10 @@ function sessionListEntryToWire(entry) {
|
|
|
1622
1636
|
}
|
|
1623
1637
|
var SessionPromptParams = z3.object({
|
|
1624
1638
|
sessionId: z3.string(),
|
|
1625
|
-
prompt: z3.array(z3.unknown())
|
|
1639
|
+
prompt: z3.array(z3.unknown()),
|
|
1640
|
+
// Hydra extensions ride under _meta["hydra-acp"] (e.g. `ancillary` to
|
|
1641
|
+
// mark a non-promoting turn). Kept so Session.prompt can read them.
|
|
1642
|
+
_meta: z3.record(z3.unknown()).optional()
|
|
1626
1643
|
});
|
|
1627
1644
|
var SessionCancelParams = z3.object({
|
|
1628
1645
|
sessionId: z3.string()
|
|
@@ -2672,12 +2689,6 @@ var HYDRA_COMMANDS = [
|
|
|
2672
2689
|
}
|
|
2673
2690
|
];
|
|
2674
2691
|
var VERB_INDEX = new Map(HYDRA_COMMANDS.map((c) => [c.verb, c]));
|
|
2675
|
-
function hydraCommandsAsAdvertised() {
|
|
2676
|
-
return HYDRA_COMMANDS.map((c) => ({
|
|
2677
|
-
name: c.argsHint ? `${c.name} ${c.argsHint}` : c.name,
|
|
2678
|
-
description: c.description
|
|
2679
|
-
}));
|
|
2680
|
-
}
|
|
2681
2692
|
|
|
2682
2693
|
// src/core/coalesce-replay.ts
|
|
2683
2694
|
function coalesceReplay(entries) {
|
|
@@ -2888,6 +2899,12 @@ var Session = class {
|
|
|
2888
2899
|
forkedFromSessionId;
|
|
2889
2900
|
forkedFromMessageId;
|
|
2890
2901
|
originatingClient;
|
|
2902
|
+
// Tristate. Mutates from undefined → true on first prompt (or directly
|
|
2903
|
+
// false if init.interactive === false). Persisted via interactiveHandlers.
|
|
2904
|
+
_interactive;
|
|
2905
|
+
get interactive() {
|
|
2906
|
+
return this._interactive;
|
|
2907
|
+
}
|
|
2891
2908
|
title;
|
|
2892
2909
|
// Snapshot state delivered to attaching clients via the attach
|
|
2893
2910
|
// response _meta rather than via history replay (which would be
|
|
@@ -3016,6 +3033,7 @@ var Session = class {
|
|
|
3016
3033
|
agentModelsHandlers = [];
|
|
3017
3034
|
modelHandlers = [];
|
|
3018
3035
|
modeHandlers = [];
|
|
3036
|
+
interactiveHandlers = [];
|
|
3019
3037
|
usageHandlers = [];
|
|
3020
3038
|
cumulativeCost = 0;
|
|
3021
3039
|
// Total cost across all agent lives. costAmount in the returned snapshot
|
|
@@ -3099,6 +3117,7 @@ var Session = class {
|
|
|
3099
3117
|
if (init.firstPromptSeeded) {
|
|
3100
3118
|
this._firstPromptSeeded = true;
|
|
3101
3119
|
}
|
|
3120
|
+
this._interactive = init.interactive;
|
|
3102
3121
|
this.historyStore = init.historyStore;
|
|
3103
3122
|
this.historyMaxEntries = init.historyMaxEntries ?? DEFAULT_HISTORY_MAX_ENTRIES;
|
|
3104
3123
|
this.compactEvery = Math.max(1, Math.floor(this.historyMaxEntries * 0.2));
|
|
@@ -3361,19 +3380,25 @@ var Session = class {
|
|
|
3361
3380
|
return this.loadReplay(historyPolicy, opts);
|
|
3362
3381
|
}
|
|
3363
3382
|
async loadReplay(historyPolicy, opts) {
|
|
3364
|
-
const
|
|
3383
|
+
const raw = await this.getHistorySnapshot();
|
|
3365
3384
|
const state = this.buildStateSnapshotReplay();
|
|
3366
3385
|
if (historyPolicy === "after_message") {
|
|
3367
|
-
const cutoff = opts.afterMessageId ? findMessageIdIndex(
|
|
3386
|
+
const cutoff = opts.afterMessageId ? findMessageIdIndex(raw, opts.afterMessageId) : -1;
|
|
3368
3387
|
if (cutoff < 0) {
|
|
3369
|
-
return {
|
|
3388
|
+
return {
|
|
3389
|
+
entries: [...state, ...coalesceReplay(raw)],
|
|
3390
|
+
appliedPolicy: "full"
|
|
3391
|
+
};
|
|
3370
3392
|
}
|
|
3371
3393
|
return {
|
|
3372
|
-
entries: [...state, ...
|
|
3394
|
+
entries: [...state, ...coalesceReplay(raw.slice(cutoff + 1))],
|
|
3373
3395
|
appliedPolicy: "after_message"
|
|
3374
3396
|
};
|
|
3375
3397
|
}
|
|
3376
|
-
return {
|
|
3398
|
+
return {
|
|
3399
|
+
entries: [...state, ...coalesceReplay(raw)],
|
|
3400
|
+
appliedPolicy: "full"
|
|
3401
|
+
};
|
|
3377
3402
|
}
|
|
3378
3403
|
// Synthesizes one session/update notification per cached STATE_UPDATE_KIND
|
|
3379
3404
|
// so an attaching client receives the current snapshot through the
|
|
@@ -3554,6 +3579,18 @@ var Session = class {
|
|
|
3554
3579
|
const messageId = generateMessageId();
|
|
3555
3580
|
this.maybeSeedTitleFromPrompt(params);
|
|
3556
3581
|
this._firstPromptSeeded = true;
|
|
3582
|
+
const ancillary = extractHydraMeta(
|
|
3583
|
+
(params ?? {})._meta
|
|
3584
|
+
).ancillary === true;
|
|
3585
|
+
if (!ancillary && this._interactive === void 0) {
|
|
3586
|
+
this._interactive = true;
|
|
3587
|
+
for (const handler of this.interactiveHandlers) {
|
|
3588
|
+
try {
|
|
3589
|
+
handler(true);
|
|
3590
|
+
} catch {
|
|
3591
|
+
}
|
|
3592
|
+
}
|
|
3593
|
+
}
|
|
3557
3594
|
return this.enqueueUserPrompt(client, params, messageId);
|
|
3558
3595
|
}
|
|
3559
3596
|
// DEVIATION FROM RFD #533: this broadcast is deliberately deferred
|
|
@@ -4336,13 +4373,7 @@ var Session = class {
|
|
|
4336
4373
|
this.logger?.info(
|
|
4337
4374
|
`live config_option_update(model): sessionId=${this.sessionId} ${JSON.stringify(this.currentModel)} \u2192 ${JSON.stringify(trimmed)}`
|
|
4338
4375
|
);
|
|
4339
|
-
this.
|
|
4340
|
-
for (const handler of this.modelHandlers) {
|
|
4341
|
-
try {
|
|
4342
|
-
handler(trimmed);
|
|
4343
|
-
} catch {
|
|
4344
|
-
}
|
|
4345
|
-
}
|
|
4376
|
+
this.applyModelChange(trimmed);
|
|
4346
4377
|
}
|
|
4347
4378
|
}
|
|
4348
4379
|
break;
|
|
@@ -4538,24 +4569,38 @@ var Session = class {
|
|
|
4538
4569
|
onModeChange(handler) {
|
|
4539
4570
|
this.modeHandlers.push(handler);
|
|
4540
4571
|
}
|
|
4572
|
+
onInteractiveChange(handler) {
|
|
4573
|
+
this.interactiveHandlers.push(handler);
|
|
4574
|
+
}
|
|
4541
4575
|
// Apply a model change initiated by a client request (session/set_model)
|
|
4542
4576
|
// when the agent doesn't emit a current_model_update notification, or
|
|
4543
4577
|
// emits a non-spec shape (e.g. config_option_update). Fires modelHandlers
|
|
4544
4578
|
// (persistence) and broadcasts a synthetic current_model_update so all
|
|
4545
4579
|
// attached clients — including the originator — repaint immediately.
|
|
4580
|
+
//
|
|
4581
|
+
// The broadcast fires even when `modelId` already equals currentModel.
|
|
4582
|
+
// claude-acp emits a stale current_model_update (with the pre-change
|
|
4583
|
+
// value) during set_model processing and a separate config_option_update
|
|
4584
|
+
// with the new value; the configOption path updates currentModel here
|
|
4585
|
+
// before our synthetic broadcast would run, so a value-equality guard
|
|
4586
|
+
// would suppress the corrective broadcast and leave attached clients
|
|
4587
|
+
// (notably the TUI, which doesn't render config_option_update) showing
|
|
4588
|
+
// the stale model from the agent's earlier notification.
|
|
4546
4589
|
applyModelChange(modelId) {
|
|
4547
4590
|
const trimmed = modelId.trim();
|
|
4548
|
-
if (!trimmed
|
|
4591
|
+
if (!trimmed) {
|
|
4549
4592
|
return;
|
|
4550
4593
|
}
|
|
4551
|
-
this.
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4594
|
+
if (trimmed !== this.currentModel) {
|
|
4595
|
+
this.logger?.info(
|
|
4596
|
+
`applyModelChange: sessionId=${this.sessionId} ${JSON.stringify(this.currentModel)} \u2192 ${JSON.stringify(trimmed)}`
|
|
4597
|
+
);
|
|
4598
|
+
this.currentModel = trimmed;
|
|
4599
|
+
for (const handler of this.modelHandlers) {
|
|
4600
|
+
try {
|
|
4601
|
+
handler(trimmed);
|
|
4602
|
+
} catch {
|
|
4603
|
+
}
|
|
4559
4604
|
}
|
|
4560
4605
|
}
|
|
4561
4606
|
const update = {
|
|
@@ -4572,23 +4617,39 @@ var Session = class {
|
|
|
4572
4617
|
}
|
|
4573
4618
|
// Apply a mode change initiated by a client request (session/set_mode)
|
|
4574
4619
|
// when the agent doesn't emit a current_mode_update notification on its
|
|
4575
|
-
// own. Fires modeHandlers
|
|
4576
|
-
//
|
|
4620
|
+
// own. Fires modeHandlers (persistence) and broadcasts a synthetic
|
|
4621
|
+
// current_mode_update so all attached clients — including the originator
|
|
4622
|
+
// — repaint immediately, mirroring applyModelChange. Without the
|
|
4623
|
+
// broadcast, peer clients (e.g. the TUI when set_mode was issued by Zed
|
|
4624
|
+
// through the shim) would stay on the prior mode.
|
|
4577
4625
|
applyModeChange(modeId) {
|
|
4578
4626
|
const trimmed = modeId.trim();
|
|
4579
|
-
if (!trimmed
|
|
4627
|
+
if (!trimmed) {
|
|
4580
4628
|
return;
|
|
4581
4629
|
}
|
|
4582
|
-
this.
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4630
|
+
if (trimmed !== this.currentMode) {
|
|
4631
|
+
this.logger?.info(
|
|
4632
|
+
`applyModeChange: sessionId=${this.sessionId} ${JSON.stringify(this.currentMode)} \u2192 ${JSON.stringify(trimmed)}`
|
|
4633
|
+
);
|
|
4634
|
+
this.currentMode = trimmed;
|
|
4635
|
+
for (const handler of this.modeHandlers) {
|
|
4636
|
+
try {
|
|
4637
|
+
handler(trimmed);
|
|
4638
|
+
} catch {
|
|
4639
|
+
}
|
|
4590
4640
|
}
|
|
4591
4641
|
}
|
|
4642
|
+
const update = {
|
|
4643
|
+
sessionUpdate: "current_mode_update",
|
|
4644
|
+
currentModeId: trimmed
|
|
4645
|
+
};
|
|
4646
|
+
if (this.agentAdvertisedModes.length > 0) {
|
|
4647
|
+
update.availableModes = [...this.agentAdvertisedModes];
|
|
4648
|
+
}
|
|
4649
|
+
this.recordAndBroadcast("session/update", {
|
|
4650
|
+
sessionId: this.upstreamSessionId,
|
|
4651
|
+
update
|
|
4652
|
+
});
|
|
4592
4653
|
}
|
|
4593
4654
|
onUsageChange(handler) {
|
|
4594
4655
|
this.usageHandlers.push(handler);
|
|
@@ -4600,8 +4661,8 @@ var Session = class {
|
|
|
4600
4661
|
// entries, then whatever the agent advertised.
|
|
4601
4662
|
mergedAvailableCommands() {
|
|
4602
4663
|
const out = [
|
|
4603
|
-
|
|
4604
|
-
{ name: "model
|
|
4664
|
+
{ name: "hydra", description: "Hydra session command (kill, restart, title, agent <agent>)" },
|
|
4665
|
+
{ name: "model", description: "Switch model; omit arg to list available models" },
|
|
4605
4666
|
{ name: "sessions", description: "List all sessions" },
|
|
4606
4667
|
{ name: "help", description: "Show available commands" }
|
|
4607
4668
|
];
|
|
@@ -6216,10 +6277,17 @@ var SessionRecord = z5.object({
|
|
|
6216
6277
|
// ended at. Kept so future UI can show "branched from turn N of session X".
|
|
6217
6278
|
forkedFromSessionId: z5.string().optional(),
|
|
6218
6279
|
forkedFromMessageId: z5.string().optional(),
|
|
6219
|
-
// clientInfo from the process that issued session/new.
|
|
6220
|
-
//
|
|
6221
|
-
//
|
|
6280
|
+
// clientInfo from the process that issued session/new. Display only
|
|
6281
|
+
// since the `interactive` flag below; kept on the record for log
|
|
6282
|
+
// attribution and as the legacy hint inside effectiveInteractive
|
|
6283
|
+
// (pre-flag cat sessions can be recognised from this field).
|
|
6222
6284
|
originatingClient: PersistedOriginatingClient.optional(),
|
|
6285
|
+
// Tristate: true once the session has had a real turn, false when
|
|
6286
|
+
// explicitly created as ancillary (e.g. `hydra cat`), undefined for
|
|
6287
|
+
// pre-flag records / freshly-created sessions that haven't decided
|
|
6288
|
+
// yet. effectiveInteractive() in session-manager.ts is the single
|
|
6289
|
+
// resolver — every filter site goes through it.
|
|
6290
|
+
interactive: z5.boolean().optional(),
|
|
6223
6291
|
createdAt: z5.string(),
|
|
6224
6292
|
updatedAt: z5.string()
|
|
6225
6293
|
});
|
|
@@ -6338,6 +6406,7 @@ function recordFromMemorySession(args) {
|
|
|
6338
6406
|
forkedFromSessionId: args.forkedFromSessionId,
|
|
6339
6407
|
forkedFromMessageId: args.forkedFromMessageId,
|
|
6340
6408
|
originatingClient: args.originatingClient,
|
|
6409
|
+
interactive: args.interactive,
|
|
6341
6410
|
createdAt: args.createdAt ?? now,
|
|
6342
6411
|
updatedAt: args.updatedAt ?? now
|
|
6343
6412
|
};
|
|
@@ -7193,6 +7262,14 @@ var BundleSession = z6.object({
|
|
|
7193
7262
|
currentUsage: PersistedUsage.optional(),
|
|
7194
7263
|
agentCommands: z6.array(PersistedAgentCommand).optional(),
|
|
7195
7264
|
agentModes: z6.array(PersistedAgentMode).optional(),
|
|
7265
|
+
// Raw interactive tristate (NOT the resolved effectiveInteractive) so
|
|
7266
|
+
// the value stays promotable on the destination: a cat/empty source
|
|
7267
|
+
// arrives as undefined and a real turn there can still flip it to
|
|
7268
|
+
// true. Carried alongside originatingClient so the importer's
|
|
7269
|
+
// effectiveInteractive can re-apply the cat-name hint at read time
|
|
7270
|
+
// without freezing a sticky `false` into the record.
|
|
7271
|
+
interactive: z6.boolean().optional(),
|
|
7272
|
+
originatingClient: PersistedOriginatingClient.optional(),
|
|
7196
7273
|
createdAt: z6.string(),
|
|
7197
7274
|
updatedAt: z6.string()
|
|
7198
7275
|
});
|
|
@@ -7236,6 +7313,8 @@ function encodeBundle(params) {
|
|
|
7236
7313
|
...params.record.currentUsage !== void 0 ? { currentUsage: params.record.currentUsage } : {},
|
|
7237
7314
|
...params.record.agentCommands !== void 0 ? { agentCommands: params.record.agentCommands } : {},
|
|
7238
7315
|
...params.record.agentModes !== void 0 ? { agentModes: params.record.agentModes } : {},
|
|
7316
|
+
...params.record.interactive !== void 0 ? { interactive: params.record.interactive } : {},
|
|
7317
|
+
...params.record.originatingClient !== void 0 ? { originatingClient: params.record.originatingClient } : {},
|
|
7239
7318
|
createdAt: params.record.createdAt,
|
|
7240
7319
|
updatedAt: params.record.updatedAt
|
|
7241
7320
|
},
|
|
@@ -7271,6 +7350,7 @@ var SessionManager = class {
|
|
|
7271
7350
|
this.logger = options.logger;
|
|
7272
7351
|
this.npmRegistry = options.npmRegistry;
|
|
7273
7352
|
this.extensionCommands = options.extensionCommands;
|
|
7353
|
+
this.defaultCwd = options.defaultCwd ?? "~";
|
|
7274
7354
|
this.synopsisCoordinator = new SynopsisCoordinator({
|
|
7275
7355
|
registry: this.registry,
|
|
7276
7356
|
store: this.store,
|
|
@@ -7304,6 +7384,7 @@ var SessionManager = class {
|
|
|
7304
7384
|
logger;
|
|
7305
7385
|
npmRegistry;
|
|
7306
7386
|
extensionCommands;
|
|
7387
|
+
defaultCwd;
|
|
7307
7388
|
// Background queue for ephemeral-agent synopsis generation. Runs
|
|
7308
7389
|
// out-of-band so session close is instant; persists synopsis/title
|
|
7309
7390
|
// via the same enqueueMetaWrite path the in-session handlers used.
|
|
@@ -7363,6 +7444,7 @@ var SessionManager = class {
|
|
|
7363
7444
|
transformChain: params.transformChain,
|
|
7364
7445
|
parentSessionId: params.parentSessionId,
|
|
7365
7446
|
originatingClient: params.originatingClient,
|
|
7447
|
+
interactive: params.interactive,
|
|
7366
7448
|
extensionCommands: this.extensionCommands,
|
|
7367
7449
|
scheduleSynopsis: () => this.synopsisCoordinator.schedule(session.sessionId)
|
|
7368
7450
|
});
|
|
@@ -7409,6 +7491,9 @@ var SessionManager = class {
|
|
|
7409
7491
|
if (params.upstreamSessionId === "") {
|
|
7410
7492
|
return this.doResurrectFromImport(params);
|
|
7411
7493
|
}
|
|
7494
|
+
if (!await this.dirExists(params.cwd)) {
|
|
7495
|
+
return this.doResurrectFromImport(params);
|
|
7496
|
+
}
|
|
7412
7497
|
const plan = await planSpawn(agentDef, params.agentArgs ?? [], {
|
|
7413
7498
|
npmRegistry: this.npmRegistry,
|
|
7414
7499
|
onInstallProgress: params.onInstallProgress
|
|
@@ -7536,6 +7621,7 @@ var SessionManager = class {
|
|
|
7536
7621
|
firstPromptSeeded: !!params.title,
|
|
7537
7622
|
createdAt: params.createdAt ? new Date(params.createdAt).getTime() : void 0,
|
|
7538
7623
|
originatingClient: params.originatingClient,
|
|
7624
|
+
interactive: params.interactive,
|
|
7539
7625
|
forkedFromSessionId: params.forkedFromSessionId,
|
|
7540
7626
|
forkedFromMessageId: params.forkedFromMessageId,
|
|
7541
7627
|
extensionCommands: this.extensionCommands,
|
|
@@ -7552,7 +7638,7 @@ var SessionManager = class {
|
|
|
7552
7638
|
// so subsequent resurrects of this session use the normal session/load
|
|
7553
7639
|
// path.
|
|
7554
7640
|
async doResurrectFromImport(params) {
|
|
7555
|
-
const cwd = await this.
|
|
7641
|
+
const cwd = await this.resolveResurrectCwd(params.cwd);
|
|
7556
7642
|
const fresh = await this.bootstrapAgent({
|
|
7557
7643
|
agentId: params.agentId,
|
|
7558
7644
|
cwd,
|
|
@@ -7607,6 +7693,7 @@ var SessionManager = class {
|
|
|
7607
7693
|
firstPromptSeeded: !!params.title,
|
|
7608
7694
|
createdAt: params.createdAt ? new Date(params.createdAt).getTime() : void 0,
|
|
7609
7695
|
originatingClient: params.originatingClient,
|
|
7696
|
+
interactive: params.interactive,
|
|
7610
7697
|
forkedFromSessionId: params.forkedFromSessionId,
|
|
7611
7698
|
forkedFromMessageId: params.forkedFromMessageId,
|
|
7612
7699
|
extensionCommands: this.extensionCommands,
|
|
@@ -7616,15 +7703,50 @@ var SessionManager = class {
|
|
|
7616
7703
|
void session.seedFromImport().catch(() => void 0);
|
|
7617
7704
|
return session;
|
|
7618
7705
|
}
|
|
7619
|
-
async
|
|
7706
|
+
async dirExists(cwd) {
|
|
7620
7707
|
try {
|
|
7621
|
-
|
|
7622
|
-
if (stat2.isDirectory()) {
|
|
7623
|
-
return cwd;
|
|
7624
|
-
}
|
|
7708
|
+
return (await fs13.stat(cwd)).isDirectory();
|
|
7625
7709
|
} catch {
|
|
7710
|
+
return false;
|
|
7711
|
+
}
|
|
7712
|
+
}
|
|
7713
|
+
// When the last client detaches from a session that resolves to
|
|
7714
|
+
// non-interactive — e.g. a `hydra cat` run, born interactive:undefined
|
|
7715
|
+
// with originatingClient hydra-acp-cat, whose every prompt is ancillary
|
|
7716
|
+
// — close it so its agent process doesn't linger until the (default 1h)
|
|
7717
|
+
// idle timeout fires. The cold record is kept, so the rare refine-in-TUI
|
|
7718
|
+
// still works via the resurrect/reseed path. Sessions promoted to
|
|
7719
|
+
// interactive (driven by a real, non-ancillary prompt) resolve to true
|
|
7720
|
+
// and are left running.
|
|
7721
|
+
async reapIfOrphanedNonInteractive(sessionId) {
|
|
7722
|
+
const session = this.sessions.get(sessionId);
|
|
7723
|
+
if (!session || session.attachedCount > 0) {
|
|
7724
|
+
return;
|
|
7626
7725
|
}
|
|
7627
|
-
|
|
7726
|
+
const interactive = effectiveInteractive(
|
|
7727
|
+
{
|
|
7728
|
+
interactive: session.interactive,
|
|
7729
|
+
...session.originatingClient ? { originatingClient: session.originatingClient } : {}
|
|
7730
|
+
},
|
|
7731
|
+
true
|
|
7732
|
+
);
|
|
7733
|
+
if (interactive !== false) {
|
|
7734
|
+
return;
|
|
7735
|
+
}
|
|
7736
|
+
this.logger?.info(
|
|
7737
|
+
`reaping orphaned non-interactive session ${sessionId} (agent killed, cold record kept)`
|
|
7738
|
+
);
|
|
7739
|
+
await session.close({ deleteRecord: false }).catch(() => void 0);
|
|
7740
|
+
}
|
|
7741
|
+
// Resolve a recorded cwd for resurrect: use it if it still exists,
|
|
7742
|
+
// otherwise fall back to the configured defaultCwd. Covers both bundles
|
|
7743
|
+
// imported from another machine and local sessions (e.g. `cat`) whose
|
|
7744
|
+
// recorded dir was cleaned up, so the reseed spawn never ENOENTs.
|
|
7745
|
+
async resolveResurrectCwd(cwd) {
|
|
7746
|
+
if (await this.dirExists(cwd)) {
|
|
7747
|
+
return cwd;
|
|
7748
|
+
}
|
|
7749
|
+
return expandHome(this.defaultCwd);
|
|
7628
7750
|
}
|
|
7629
7751
|
// Pull every session the agent itself remembers (across all cwds) and
|
|
7630
7752
|
// persist a cold hydra record for each one we don't already track.
|
|
@@ -7713,6 +7835,10 @@ var SessionManager = class {
|
|
|
7713
7835
|
agentId,
|
|
7714
7836
|
cwd: entry.cwd,
|
|
7715
7837
|
pendingHistorySync: true,
|
|
7838
|
+
// `hydra agent sync` is a user-explicit "show me agent-side
|
|
7839
|
+
// sessions" action; the rows are meant to be visible immediately
|
|
7840
|
+
// even before the first resurrect populates history.jsonl.
|
|
7841
|
+
interactive: true,
|
|
7716
7842
|
createdAt: ts,
|
|
7717
7843
|
updatedAt: ts
|
|
7718
7844
|
};
|
|
@@ -7879,6 +8005,11 @@ var SessionManager = class {
|
|
|
7879
8005
|
() => void 0
|
|
7880
8006
|
);
|
|
7881
8007
|
});
|
|
8008
|
+
session.onInteractiveChange((interactive) => {
|
|
8009
|
+
void this.persistSnapshot(session.sessionId, { interactive }).catch(
|
|
8010
|
+
() => void 0
|
|
8011
|
+
);
|
|
8012
|
+
});
|
|
7882
8013
|
session.onUsageChange((usage) => {
|
|
7883
8014
|
void this.persistSnapshot(session.sessionId, {
|
|
7884
8015
|
currentUsage: usageSnapshotToPersisted(usage)
|
|
@@ -7972,6 +8103,7 @@ var SessionManager = class {
|
|
|
7972
8103
|
createdAt: record.createdAt,
|
|
7973
8104
|
pendingHistorySync: record.pendingHistorySync,
|
|
7974
8105
|
originatingClient: record.originatingClient,
|
|
8106
|
+
interactive: record.interactive,
|
|
7975
8107
|
forkedFromSessionId: record.forkedFromSessionId,
|
|
7976
8108
|
forkedFromMessageId: record.forkedFromMessageId
|
|
7977
8109
|
};
|
|
@@ -8059,12 +8191,27 @@ var SessionManager = class {
|
|
|
8059
8191
|
async list(filter = {}) {
|
|
8060
8192
|
const entries = [];
|
|
8061
8193
|
const liveIds = /* @__PURE__ */ new Set();
|
|
8194
|
+
const includeRow = (interactive) => {
|
|
8195
|
+
if (filter.includeNonInteractive) return true;
|
|
8196
|
+
return interactive === true;
|
|
8197
|
+
};
|
|
8062
8198
|
for (const session of this.sessions.values()) {
|
|
8063
8199
|
if (filter.cwd && session.cwd !== filter.cwd) {
|
|
8064
8200
|
continue;
|
|
8065
8201
|
}
|
|
8066
8202
|
liveIds.add(session.sessionId);
|
|
8067
|
-
const
|
|
8203
|
+
const hist = await historyStatus(session.sessionId);
|
|
8204
|
+
const interactive = effectiveInteractive(
|
|
8205
|
+
{
|
|
8206
|
+
interactive: session.interactive,
|
|
8207
|
+
...session.originatingClient ? { originatingClient: session.originatingClient } : {}
|
|
8208
|
+
},
|
|
8209
|
+
hist.hasContent
|
|
8210
|
+
);
|
|
8211
|
+
if (!includeRow(interactive)) {
|
|
8212
|
+
continue;
|
|
8213
|
+
}
|
|
8214
|
+
const used = hist.mtime ?? new Date(session.updatedAt).toISOString();
|
|
8068
8215
|
entries.push({
|
|
8069
8216
|
sessionId: session.sessionId,
|
|
8070
8217
|
upstreamSessionId: session.upstreamSessionId,
|
|
@@ -8077,6 +8224,7 @@ var SessionManager = class {
|
|
|
8077
8224
|
forkedFromSessionId: session.forkedFromSessionId,
|
|
8078
8225
|
forkedFromMessageId: session.forkedFromMessageId,
|
|
8079
8226
|
originatingClient: session.originatingClient,
|
|
8227
|
+
interactive,
|
|
8080
8228
|
updatedAt: used,
|
|
8081
8229
|
attachedClients: session.attachedCount,
|
|
8082
8230
|
status: "live",
|
|
@@ -8091,7 +8239,12 @@ var SessionManager = class {
|
|
|
8091
8239
|
if (filter.cwd && r.cwd !== filter.cwd) {
|
|
8092
8240
|
continue;
|
|
8093
8241
|
}
|
|
8094
|
-
const
|
|
8242
|
+
const hist = await historyStatus(r.sessionId);
|
|
8243
|
+
const interactive = effectiveInteractive(r, hist.hasContent);
|
|
8244
|
+
if (!includeRow(interactive)) {
|
|
8245
|
+
continue;
|
|
8246
|
+
}
|
|
8247
|
+
const used = hist.mtime ?? r.updatedAt;
|
|
8095
8248
|
entries.push({
|
|
8096
8249
|
sessionId: r.sessionId,
|
|
8097
8250
|
upstreamSessionId: r.upstreamSessionId,
|
|
@@ -8109,6 +8262,7 @@ var SessionManager = class {
|
|
|
8109
8262
|
forkedFromSessionId: r.forkedFromSessionId,
|
|
8110
8263
|
forkedFromMessageId: r.forkedFromMessageId,
|
|
8111
8264
|
originatingClient: r.originatingClient,
|
|
8265
|
+
interactive,
|
|
8112
8266
|
updatedAt: used,
|
|
8113
8267
|
attachedClients: 0,
|
|
8114
8268
|
status: "cold",
|
|
@@ -8343,8 +8497,18 @@ var SessionManager = class {
|
|
|
8343
8497
|
currentUsage: args.bundle.session.currentUsage,
|
|
8344
8498
|
agentCommands: args.bundle.session.agentCommands,
|
|
8345
8499
|
agentModes: args.bundle.session.agentModes,
|
|
8500
|
+
// Carry the source's raw interactive tristate and originating
|
|
8501
|
+
// client rather than forcing true. A real conversation arrives
|
|
8502
|
+
// as true (visible immediately); an empty source arrives as
|
|
8503
|
+
// undefined (hidden until a turn lands here); a cat source
|
|
8504
|
+
// arrives as undefined + cat originatingClient, so
|
|
8505
|
+
// effectiveInteractive hides it via the hint while leaving it
|
|
8506
|
+
// promotable. Legacy bundles (pre-flag) carry neither and fall
|
|
8507
|
+
// back to effectiveInteractive's history-presence inference.
|
|
8508
|
+
interactive: args.bundle.session.interactive,
|
|
8509
|
+
originatingClient: args.bundle.session.originatingClient,
|
|
8346
8510
|
createdAt: args.preservedCreatedAt ?? now,
|
|
8347
|
-
// Fallback path for
|
|
8511
|
+
// Fallback path for historyStatus (used when the history file
|
|
8348
8512
|
// is missing). Keep this consistent with the utimes stamp above.
|
|
8349
8513
|
updatedAt: args.bundle.session.updatedAt
|
|
8350
8514
|
});
|
|
@@ -8453,6 +8617,8 @@ var SessionManager = class {
|
|
|
8453
8617
|
...update.agentCommands !== void 0 ? { agentCommands: update.agentCommands } : {},
|
|
8454
8618
|
...update.agentModes !== void 0 ? { agentModes: update.agentModes } : {},
|
|
8455
8619
|
...update.agentModels !== void 0 ? { agentModels: update.agentModels } : {},
|
|
8620
|
+
...update.interactive !== void 0 ? { interactive: update.interactive } : {},
|
|
8621
|
+
...update.cwd !== void 0 ? { cwd: update.cwd } : {},
|
|
8456
8622
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
8457
8623
|
});
|
|
8458
8624
|
});
|
|
@@ -8629,6 +8795,7 @@ function mergeForPersistence(session, existing) {
|
|
|
8629
8795
|
forkedFromSessionId: session.forkedFromSessionId ?? existing?.forkedFromSessionId,
|
|
8630
8796
|
forkedFromMessageId: session.forkedFromMessageId ?? existing?.forkedFromMessageId,
|
|
8631
8797
|
originatingClient: session.originatingClient ?? existing?.originatingClient,
|
|
8798
|
+
interactive: session.interactive ?? existing?.interactive,
|
|
8632
8799
|
createdAt: existing?.createdAt ?? new Date(session.createdAt).toISOString()
|
|
8633
8800
|
});
|
|
8634
8801
|
}
|
|
@@ -8935,13 +9102,25 @@ async function loadPromptHistorySafely(sessionId) {
|
|
|
8935
9102
|
return [];
|
|
8936
9103
|
}
|
|
8937
9104
|
}
|
|
8938
|
-
async function
|
|
9105
|
+
async function historyStatus(sessionId) {
|
|
8939
9106
|
try {
|
|
8940
9107
|
const st = await fs13.stat(paths.historyFile(sessionId));
|
|
8941
|
-
return
|
|
9108
|
+
return {
|
|
9109
|
+
mtime: new Date(st.mtimeMs).toISOString(),
|
|
9110
|
+
hasContent: st.size > 0
|
|
9111
|
+
};
|
|
8942
9112
|
} catch {
|
|
8943
|
-
return
|
|
9113
|
+
return { hasContent: false };
|
|
9114
|
+
}
|
|
9115
|
+
}
|
|
9116
|
+
function effectiveInteractive(record, hasContent) {
|
|
9117
|
+
if (record.interactive !== void 0) {
|
|
9118
|
+
return record.interactive;
|
|
8944
9119
|
}
|
|
9120
|
+
if (record.originatingClient?.name === HYDRA_CAT_CLIENT_NAME) {
|
|
9121
|
+
return false;
|
|
9122
|
+
}
|
|
9123
|
+
return hasContent ? true : void 0;
|
|
8945
9124
|
}
|
|
8946
9125
|
|
|
8947
9126
|
// src/core/child-supervisor.ts
|
|
@@ -11168,17 +11347,21 @@ function resolveHydraHost(defaults) {
|
|
|
11168
11347
|
function registerSessionRoutes(app, manager, defaults) {
|
|
11169
11348
|
app.get("/v1/sessions", async (request) => {
|
|
11170
11349
|
const query = request.query;
|
|
11171
|
-
const
|
|
11350
|
+
const includeNonInteractive = query?.includeNonInteractive === "1" || query?.includeNonInteractive === "true";
|
|
11351
|
+
const sessions = await manager.list({
|
|
11352
|
+
cwd: query?.cwd,
|
|
11353
|
+
includeNonInteractive
|
|
11354
|
+
});
|
|
11172
11355
|
return { sessions };
|
|
11173
11356
|
});
|
|
11174
|
-
app.
|
|
11175
|
-
const
|
|
11176
|
-
const q =
|
|
11357
|
+
app.post("/v1/sessions/search", async (request, reply) => {
|
|
11358
|
+
const body = request.body ?? {};
|
|
11359
|
+
const q = typeof body.q === "string" ? body.q : "";
|
|
11177
11360
|
if (q.trim().length === 0) {
|
|
11178
11361
|
reply.code(400).send({ error: "q is required" });
|
|
11179
11362
|
return reply;
|
|
11180
11363
|
}
|
|
11181
|
-
const ids =
|
|
11364
|
+
const ids = Array.isArray(body.sessionIds) ? body.sessionIds.filter((s) => typeof s === "string" && s.length > 0) : void 0;
|
|
11182
11365
|
const out = await searchHistories(manager, q, { sessionIds: ids });
|
|
11183
11366
|
return out;
|
|
11184
11367
|
});
|
|
@@ -11772,13 +11955,8 @@ function parseRegisterBody2(body) {
|
|
|
11772
11955
|
}
|
|
11773
11956
|
|
|
11774
11957
|
// src/daemon/routes/config.ts
|
|
11775
|
-
function registerConfigRoutes(app,
|
|
11776
|
-
app.get("/v1/config", async () =>
|
|
11777
|
-
return {
|
|
11778
|
-
defaultAgent: defaults.defaultAgent,
|
|
11779
|
-
defaultCwd: defaults.defaultCwd
|
|
11780
|
-
};
|
|
11781
|
-
});
|
|
11958
|
+
function registerConfigRoutes(app, snapshot) {
|
|
11959
|
+
app.get("/v1/config", async () => snapshot);
|
|
11782
11960
|
}
|
|
11783
11961
|
|
|
11784
11962
|
// src/daemon/routes/auth.ts
|
|
@@ -12343,7 +12521,8 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12343
12521
|
model: hydraMeta.model,
|
|
12344
12522
|
onInstallProgress: makeInstallProgressForwarder(connection),
|
|
12345
12523
|
transformChain,
|
|
12346
|
-
originatingClient: state.clientInfo
|
|
12524
|
+
originatingClient: state.clientInfo,
|
|
12525
|
+
...hydraMeta.interactive !== void 0 ? { interactive: hydraMeta.interactive } : {}
|
|
12347
12526
|
});
|
|
12348
12527
|
} catch (err) {
|
|
12349
12528
|
if (stdinReservation !== void 0) {
|
|
@@ -12470,8 +12649,9 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12470
12649
|
err.code = JsonRpcErrorCodes.SessionNotFound;
|
|
12471
12650
|
throw err;
|
|
12472
12651
|
}
|
|
12652
|
+
const resurrectWithOriginator = resurrectParams.originatingClient ? resurrectParams : { ...resurrectParams, originatingClient: state.clientInfo };
|
|
12473
12653
|
session = await deps.manager.resurrect({
|
|
12474
|
-
...
|
|
12654
|
+
...resurrectWithOriginator,
|
|
12475
12655
|
onInstallProgress: makeInstallProgressForwarder(connection)
|
|
12476
12656
|
});
|
|
12477
12657
|
wireDefaultTransformers(session, deps);
|
|
@@ -12528,6 +12708,9 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12528
12708
|
const session = deps.manager.get(params.sessionId);
|
|
12529
12709
|
session?.detach(att.clientId);
|
|
12530
12710
|
state.attached.delete(params.sessionId);
|
|
12711
|
+
if (session) {
|
|
12712
|
+
void deps.manager.reapIfOrphanedNonInteractive(params.sessionId);
|
|
12713
|
+
}
|
|
12531
12714
|
return { sessionId: params.sessionId, status: "detached" };
|
|
12532
12715
|
});
|
|
12533
12716
|
connection.onRequest("session/list", async (raw) => {
|
|
@@ -13775,7 +13958,8 @@ async function startDaemon(config, serviceToken) {
|
|
|
13775
13958
|
sessionHistoryMaxEntries: config.daemon.sessionHistoryMaxEntries,
|
|
13776
13959
|
logger: agentLogger,
|
|
13777
13960
|
npmRegistry: config.npmRegistry,
|
|
13778
|
-
extensionCommands
|
|
13961
|
+
extensionCommands,
|
|
13962
|
+
defaultCwd: config.defaultCwd
|
|
13779
13963
|
});
|
|
13780
13964
|
const extensions = new ExtensionManager(extensionList(config), void 0, {
|
|
13781
13965
|
tokenRegistry: processRegistry
|
|
@@ -13796,7 +13980,12 @@ async function startDaemon(config, serviceToken) {
|
|
|
13796
13980
|
registerTransformerRoutes(app, transformers);
|
|
13797
13981
|
registerConfigRoutes(app, {
|
|
13798
13982
|
defaultAgent: config.defaultAgent,
|
|
13799
|
-
defaultCwd: config.defaultCwd
|
|
13983
|
+
defaultCwd: config.defaultCwd,
|
|
13984
|
+
defaultModels: { ...config.defaultModels },
|
|
13985
|
+
...config.synopsisAgent !== void 0 ? { synopsisAgent: config.synopsisAgent } : {},
|
|
13986
|
+
...config.synopsisModel !== void 0 ? { synopsisModel: config.synopsisModel } : {},
|
|
13987
|
+
synopsisOnClose: config.synopsisOnClose,
|
|
13988
|
+
defaultTransformers: [...config.defaultTransformers]
|
|
13800
13989
|
});
|
|
13801
13990
|
registerAuthRoutes(app, {
|
|
13802
13991
|
store: sessionTokenStore,
|