@hydra-acp/cli 0.1.59 → 0.1.61
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/dist/cli.js +1245 -237
- package/dist/index.d.ts +23 -6
- package/dist/index.js +177 -33
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -156,6 +156,7 @@ declare const HydraConfig: z.ZodObject<{
|
|
|
156
156
|
maxToolItems: z.ZodDefault<z.ZodNumber>;
|
|
157
157
|
maxPlanItems: z.ZodDefault<z.ZodNumber>;
|
|
158
158
|
showFileUpdates: z.ZodDefault<z.ZodEnum<["none", "edit", "diff"]>>;
|
|
159
|
+
sessionColumns: z.ZodOptional<z.ZodArray<z.ZodEnum<["session", "upstream", "host", "state", "agent", "model", "age", "cwd", "title", "cost"]>, "atleastone">>;
|
|
159
160
|
}, "strip", z.ZodTypeAny, {
|
|
160
161
|
repaintThrottleMs: number;
|
|
161
162
|
maxScrollbackLines: number;
|
|
@@ -169,6 +170,7 @@ declare const HydraConfig: z.ZodObject<{
|
|
|
169
170
|
maxToolItems: number;
|
|
170
171
|
maxPlanItems: number;
|
|
171
172
|
showFileUpdates: "diff" | "none" | "edit";
|
|
173
|
+
sessionColumns?: ["agent" | "model" | "cwd" | "session" | "upstream" | "state" | "age" | "title" | "host" | "cost", ...("agent" | "model" | "cwd" | "session" | "upstream" | "state" | "age" | "title" | "host" | "cost")[]] | undefined;
|
|
172
174
|
}, {
|
|
173
175
|
repaintThrottleMs?: number | undefined;
|
|
174
176
|
maxScrollbackLines?: number | undefined;
|
|
@@ -182,6 +184,7 @@ declare const HydraConfig: z.ZodObject<{
|
|
|
182
184
|
maxToolItems?: number | undefined;
|
|
183
185
|
maxPlanItems?: number | undefined;
|
|
184
186
|
showFileUpdates?: "diff" | "none" | "edit" | undefined;
|
|
187
|
+
sessionColumns?: ["agent" | "model" | "cwd" | "session" | "upstream" | "state" | "age" | "title" | "host" | "cost", ...("agent" | "model" | "cwd" | "session" | "upstream" | "state" | "age" | "title" | "host" | "cost")[]] | undefined;
|
|
185
188
|
}>>;
|
|
186
189
|
}, "strip", z.ZodTypeAny, {
|
|
187
190
|
tui: {
|
|
@@ -197,6 +200,7 @@ declare const HydraConfig: z.ZodObject<{
|
|
|
197
200
|
maxToolItems: number;
|
|
198
201
|
maxPlanItems: number;
|
|
199
202
|
showFileUpdates: "diff" | "none" | "edit";
|
|
203
|
+
sessionColumns?: ["agent" | "model" | "cwd" | "session" | "upstream" | "state" | "age" | "title" | "host" | "cost", ...("agent" | "model" | "cwd" | "session" | "upstream" | "state" | "age" | "title" | "host" | "cost")[]] | undefined;
|
|
200
204
|
};
|
|
201
205
|
daemon: {
|
|
202
206
|
host: string;
|
|
@@ -251,6 +255,7 @@ declare const HydraConfig: z.ZodObject<{
|
|
|
251
255
|
maxToolItems?: number | undefined;
|
|
252
256
|
maxPlanItems?: number | undefined;
|
|
253
257
|
showFileUpdates?: "diff" | "none" | "edit" | undefined;
|
|
258
|
+
sessionColumns?: ["agent" | "model" | "cwd" | "session" | "upstream" | "state" | "age" | "title" | "host" | "cost", ...("agent" | "model" | "cwd" | "session" | "upstream" | "state" | "age" | "title" | "host" | "cost")[]] | undefined;
|
|
254
259
|
} | undefined;
|
|
255
260
|
daemon?: {
|
|
256
261
|
host?: string | undefined;
|
|
@@ -1419,6 +1424,8 @@ declare const SessionAttachParams: z.ZodObject<{
|
|
|
1419
1424
|
version?: string | undefined;
|
|
1420
1425
|
}>>;
|
|
1421
1426
|
readonly: z.ZodOptional<z.ZodBoolean>;
|
|
1427
|
+
replayMode: z.ZodOptional<z.ZodEnum<["instant", "drip"]>>;
|
|
1428
|
+
dripSpeed: z.ZodOptional<z.ZodNumber>;
|
|
1422
1429
|
_meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
1423
1430
|
}, "strip", z.ZodTypeAny, {
|
|
1424
1431
|
sessionId: string;
|
|
@@ -1430,6 +1437,8 @@ declare const SessionAttachParams: z.ZodObject<{
|
|
|
1430
1437
|
} | undefined;
|
|
1431
1438
|
afterMessageId?: string | undefined;
|
|
1432
1439
|
clientId?: string | undefined;
|
|
1440
|
+
replayMode?: "drip" | "instant" | undefined;
|
|
1441
|
+
dripSpeed?: number | undefined;
|
|
1433
1442
|
_meta?: Record<string, unknown> | undefined;
|
|
1434
1443
|
}, {
|
|
1435
1444
|
sessionId: string;
|
|
@@ -1441,6 +1450,8 @@ declare const SessionAttachParams: z.ZodObject<{
|
|
|
1441
1450
|
historyPolicy?: "none" | "full" | "pending_only" | "after_message" | undefined;
|
|
1442
1451
|
afterMessageId?: string | undefined;
|
|
1443
1452
|
clientId?: string | undefined;
|
|
1453
|
+
replayMode?: "drip" | "instant" | undefined;
|
|
1454
|
+
dripSpeed?: number | undefined;
|
|
1444
1455
|
_meta?: Record<string, unknown> | undefined;
|
|
1445
1456
|
}>;
|
|
1446
1457
|
type SessionAttachParams = z.infer<typeof SessionAttachParams>;
|
|
@@ -1518,6 +1529,7 @@ declare const SessionListEntry: z.ZodObject<{
|
|
|
1518
1529
|
attachedClients: z.ZodNumber;
|
|
1519
1530
|
status: z.ZodDefault<z.ZodEnum<["live", "cold"]>>;
|
|
1520
1531
|
busy: z.ZodDefault<z.ZodBoolean>;
|
|
1532
|
+
awaitingInput: z.ZodDefault<z.ZodBoolean>;
|
|
1521
1533
|
_meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
1522
1534
|
}, "strip", z.ZodTypeAny, {
|
|
1523
1535
|
sessionId: string;
|
|
@@ -1526,9 +1538,10 @@ declare const SessionListEntry: z.ZodObject<{
|
|
|
1526
1538
|
updatedAt: string;
|
|
1527
1539
|
attachedClients: number;
|
|
1528
1540
|
busy: boolean;
|
|
1541
|
+
awaitingInput: boolean;
|
|
1542
|
+
title?: string | undefined;
|
|
1529
1543
|
agentId?: string | undefined;
|
|
1530
1544
|
upstreamSessionId?: string | undefined;
|
|
1531
|
-
title?: string | undefined;
|
|
1532
1545
|
_meta?: Record<string, unknown> | undefined;
|
|
1533
1546
|
currentModel?: string | undefined;
|
|
1534
1547
|
currentUsage?: {
|
|
@@ -1553,9 +1566,9 @@ declare const SessionListEntry: z.ZodObject<{
|
|
|
1553
1566
|
updatedAt: string;
|
|
1554
1567
|
attachedClients: number;
|
|
1555
1568
|
status?: "live" | "cold" | undefined;
|
|
1569
|
+
title?: string | undefined;
|
|
1556
1570
|
agentId?: string | undefined;
|
|
1557
1571
|
upstreamSessionId?: string | undefined;
|
|
1558
|
-
title?: string | undefined;
|
|
1559
1572
|
_meta?: Record<string, unknown> | undefined;
|
|
1560
1573
|
currentModel?: string | undefined;
|
|
1561
1574
|
currentUsage?: {
|
|
@@ -1575,6 +1588,7 @@ declare const SessionListEntry: z.ZodObject<{
|
|
|
1575
1588
|
version?: string | undefined;
|
|
1576
1589
|
} | undefined;
|
|
1577
1590
|
busy?: boolean | undefined;
|
|
1591
|
+
awaitingInput?: boolean | undefined;
|
|
1578
1592
|
}>;
|
|
1579
1593
|
type SessionListEntry = z.infer<typeof SessionListEntry>;
|
|
1580
1594
|
declare const SessionListResult: z.ZodObject<{
|
|
@@ -2197,10 +2211,12 @@ declare class Session {
|
|
|
2197
2211
|
version?: string;
|
|
2198
2212
|
}>;
|
|
2199
2213
|
get turnStartedAt(): number | undefined;
|
|
2214
|
+
get awaitingInput(): boolean;
|
|
2200
2215
|
getHistorySnapshot(): Promise<CachedNotification[]>;
|
|
2201
2216
|
onBroadcast(handler: (entry: CachedNotification) => void): () => void;
|
|
2202
2217
|
attach(client: AttachedClient, historyPolicy: HistoryPolicy, opts?: {
|
|
2203
2218
|
afterMessageId?: string;
|
|
2219
|
+
raw?: boolean;
|
|
2204
2220
|
}): Promise<{
|
|
2205
2221
|
entries: CachedNotification[];
|
|
2206
2222
|
appliedPolicy: HistoryPolicy;
|
|
@@ -2276,6 +2292,7 @@ declare class Session {
|
|
|
2276
2292
|
private handleSessionsCommand;
|
|
2277
2293
|
private handleHelpCommand;
|
|
2278
2294
|
private handleModelCommand;
|
|
2295
|
+
private handleModeCommand;
|
|
2279
2296
|
private runTitleCommand;
|
|
2280
2297
|
private runInternalPrompt;
|
|
2281
2298
|
private runAgentCommand;
|
|
@@ -2710,8 +2727,8 @@ declare const Bundle: z.ZodObject<{
|
|
|
2710
2727
|
updatedAt: string;
|
|
2711
2728
|
createdAt: string;
|
|
2712
2729
|
lineageId: string;
|
|
2713
|
-
upstreamSessionId?: string | undefined;
|
|
2714
2730
|
title?: string | undefined;
|
|
2731
|
+
upstreamSessionId?: string | undefined;
|
|
2715
2732
|
currentModel?: string | undefined;
|
|
2716
2733
|
currentMode?: string | undefined;
|
|
2717
2734
|
currentUsage?: {
|
|
@@ -2751,8 +2768,8 @@ declare const Bundle: z.ZodObject<{
|
|
|
2751
2768
|
updatedAt: string;
|
|
2752
2769
|
createdAt: string;
|
|
2753
2770
|
lineageId: string;
|
|
2754
|
-
upstreamSessionId?: string | undefined;
|
|
2755
2771
|
title?: string | undefined;
|
|
2772
|
+
upstreamSessionId?: string | undefined;
|
|
2756
2773
|
currentModel?: string | undefined;
|
|
2757
2774
|
currentMode?: string | undefined;
|
|
2758
2775
|
currentUsage?: {
|
|
@@ -2809,8 +2826,8 @@ declare const Bundle: z.ZodObject<{
|
|
|
2809
2826
|
updatedAt: string;
|
|
2810
2827
|
createdAt: string;
|
|
2811
2828
|
lineageId: string;
|
|
2812
|
-
upstreamSessionId?: string | undefined;
|
|
2813
2829
|
title?: string | undefined;
|
|
2830
|
+
upstreamSessionId?: string | undefined;
|
|
2814
2831
|
currentModel?: string | undefined;
|
|
2815
2832
|
currentMode?: string | undefined;
|
|
2816
2833
|
currentUsage?: {
|
|
@@ -2865,8 +2882,8 @@ declare const Bundle: z.ZodObject<{
|
|
|
2865
2882
|
updatedAt: string;
|
|
2866
2883
|
createdAt: string;
|
|
2867
2884
|
lineageId: string;
|
|
2868
|
-
upstreamSessionId?: string | undefined;
|
|
2869
2885
|
title?: string | undefined;
|
|
2886
|
+
upstreamSessionId?: string | undefined;
|
|
2870
2887
|
currentModel?: string | undefined;
|
|
2871
2888
|
currentMode?: string | undefined;
|
|
2872
2889
|
currentUsage?: {
|
package/dist/index.js
CHANGED
|
@@ -293,7 +293,7 @@ var TuiConfig = z.object({
|
|
|
293
293
|
// Width cap on the cwd column in the `sessions list` output and the
|
|
294
294
|
// TUI picker. Set higher if you keep deeply-nested working directories
|
|
295
295
|
// and want them visible; the elastic title column shrinks to make room.
|
|
296
|
-
cwdColumnMaxWidth: z.number().int().positive().default(
|
|
296
|
+
cwdColumnMaxWidth: z.number().int().positive().default(32),
|
|
297
297
|
// When true (default), emit OSC 9;4 progress-bar control codes so the
|
|
298
298
|
// host terminal can show an indeterminate busy indicator (taskbar pulse
|
|
299
299
|
// on Windows Terminal, dock badge on KDE/Konsole, etc.) while a turn is
|
|
@@ -344,7 +344,28 @@ var TuiConfig = z.object({
|
|
|
344
344
|
// The diff payload is extracted from the ACP wire (content[]
|
|
345
345
|
// type:"diff" entries, falling back to rawInput shapes), so any agent
|
|
346
346
|
// that emits one of those shapes gets the treatment.
|
|
347
|
-
showFileUpdates: z.enum(["none", "edit", "diff"]).default("edit")
|
|
347
|
+
showFileUpdates: z.enum(["none", "edit", "diff"]).default("edit"),
|
|
348
|
+
// Columns shown in the `sessions list` output and the TUI picker, in
|
|
349
|
+
// the given order — so this controls both which columns appear and
|
|
350
|
+
// their left-to-right order. Valid names: session, upstream, host,
|
|
351
|
+
// state, agent, model, age, cwd, title, cost. Omit to use the built-in
|
|
352
|
+
// default (session, state, agent, age, cwd, title, cost — UPSTREAM,
|
|
353
|
+
// HOST, and MODEL hidden). The CLI's `--columns` flag overrides this
|
|
354
|
+
// per-invocation. Duplicate or unknown names are rejected.
|
|
355
|
+
sessionColumns: z.array(
|
|
356
|
+
z.enum([
|
|
357
|
+
"session",
|
|
358
|
+
"upstream",
|
|
359
|
+
"host",
|
|
360
|
+
"state",
|
|
361
|
+
"agent",
|
|
362
|
+
"model",
|
|
363
|
+
"age",
|
|
364
|
+
"cwd",
|
|
365
|
+
"title",
|
|
366
|
+
"cost"
|
|
367
|
+
])
|
|
368
|
+
).nonempty().optional()
|
|
348
369
|
});
|
|
349
370
|
var ExtensionName = z.string().min(1).regex(/^[A-Za-z0-9._-]+$/, "extension name must be filename-safe");
|
|
350
371
|
var ExtensionBody = z.object({
|
|
@@ -365,7 +386,7 @@ var TransformerBody = z.object({
|
|
|
365
386
|
var HydraConfig = z.object({
|
|
366
387
|
daemon: DaemonConfig.default({}),
|
|
367
388
|
registry: RegistryConfig.default({ url: REGISTRY_URL_DEFAULT, ttlHours: 24 }),
|
|
368
|
-
defaultAgent: z.string().default("
|
|
389
|
+
defaultAgent: z.string().default("opencode"),
|
|
369
390
|
// Optional per-agent default model id. When a brand-new agent process
|
|
370
391
|
// is spawned (session/new path), hydra issues session/set_model with
|
|
371
392
|
// the matching entry so the user lands on their preferred model from
|
|
@@ -413,7 +434,7 @@ var HydraConfig = z.object({
|
|
|
413
434
|
maxScrollbackLines: 1e4,
|
|
414
435
|
mouse: false,
|
|
415
436
|
logMaxBytes: 5 * 1024 * 1024,
|
|
416
|
-
cwdColumnMaxWidth:
|
|
437
|
+
cwdColumnMaxWidth: 32,
|
|
417
438
|
progressIndicator: true,
|
|
418
439
|
defaultEnterAction: "amend",
|
|
419
440
|
showThoughts: true,
|
|
@@ -1358,6 +1379,17 @@ var SessionAttachParams = z3.object({
|
|
|
1358
1379
|
// to a cold session does not resurrect or spawn an agent — just
|
|
1359
1380
|
// streams history from disk. Used by the TUI's view-only mode.
|
|
1360
1381
|
readonly: z3.boolean().optional(),
|
|
1382
|
+
// Debug-only replay pacing. When "drip", the daemon skips chunk
|
|
1383
|
+
// coalescing and re-emits each recorded session/update individually,
|
|
1384
|
+
// spacing them by their original recordedAt deltas (scaled by
|
|
1385
|
+
// dripSpeed, with a per-gap cap) so a session's streaming render can
|
|
1386
|
+
// be reproduced at its real granularity for flicker investigation.
|
|
1387
|
+
// Omitted/"instant" preserves the normal coalesced, as-fast-as-possible
|
|
1388
|
+
// replay.
|
|
1389
|
+
replayMode: z3.enum(["instant", "drip"]).optional(),
|
|
1390
|
+
// Multiplier applied to original inter-entry gaps in drip mode. >1
|
|
1391
|
+
// compresses time (faster), <1 stretches it. Defaults to 1.
|
|
1392
|
+
dripSpeed: z3.number().positive().optional(),
|
|
1361
1393
|
_meta: z3.record(z3.unknown()).optional()
|
|
1362
1394
|
});
|
|
1363
1395
|
var HYDRA_META_KEY = "hydra-acp";
|
|
@@ -1586,6 +1618,11 @@ var SessionListEntry = z3.object({
|
|
|
1586
1618
|
// Always false for cold sessions. Lets pickers render a busy dot
|
|
1587
1619
|
// without having to attach.
|
|
1588
1620
|
busy: z3.boolean().default(false),
|
|
1621
|
+
// True when the agent is blocked on the user (an outstanding
|
|
1622
|
+
// session/request_permission, which also covers agent-posed
|
|
1623
|
+
// questions). Always false for cold sessions. Lets pickers render a
|
|
1624
|
+
// distinct "waiting on you" glyph instead of the busy dot.
|
|
1625
|
+
awaitingInput: z3.boolean().default(false),
|
|
1589
1626
|
_meta: z3.record(z3.unknown()).optional()
|
|
1590
1627
|
});
|
|
1591
1628
|
var SessionListEntryWire = z3.object({
|
|
@@ -1603,7 +1640,8 @@ function sessionListEntryToWire(entry) {
|
|
|
1603
1640
|
const hydraMeta = {
|
|
1604
1641
|
attachedClients: entry.attachedClients,
|
|
1605
1642
|
status: entry.status,
|
|
1606
|
-
busy: entry.busy
|
|
1643
|
+
busy: entry.busy,
|
|
1644
|
+
awaitingInput: entry.awaitingInput
|
|
1607
1645
|
};
|
|
1608
1646
|
if (entry.agentId !== void 0) {
|
|
1609
1647
|
hydraMeta.agentId = entry.agentId;
|
|
@@ -3324,6 +3362,14 @@ var Session = class {
|
|
|
3324
3362
|
get turnStartedAt() {
|
|
3325
3363
|
return this.promptStartedAt;
|
|
3326
3364
|
}
|
|
3365
|
+
// True when the agent is blocked on the user: an outstanding
|
|
3366
|
+
// session/request_permission (which also carries agent-posed
|
|
3367
|
+
// questions — there's no separate "ask the user" request in ACP).
|
|
3368
|
+
// Lets pickers show a "waiting on you" glyph distinct from the
|
|
3369
|
+
// "actively working" one without having to attach.
|
|
3370
|
+
get awaitingInput() {
|
|
3371
|
+
return this.inFlightPermissions.size > 0;
|
|
3372
|
+
}
|
|
3327
3373
|
// Read the persisted history from disk. Returns [] if no history
|
|
3328
3374
|
// file exists (fresh session, never prompted). Used by attach() and
|
|
3329
3375
|
// the HTTP /history endpoint.
|
|
@@ -3380,23 +3426,24 @@ var Session = class {
|
|
|
3380
3426
|
return this.loadReplay(historyPolicy, opts);
|
|
3381
3427
|
}
|
|
3382
3428
|
async loadReplay(historyPolicy, opts) {
|
|
3429
|
+
const maybeCoalesce = (entries) => opts.raw ? entries : coalesceReplay(entries);
|
|
3383
3430
|
const raw = await this.getHistorySnapshot();
|
|
3384
3431
|
const state = this.buildStateSnapshotReplay();
|
|
3385
3432
|
if (historyPolicy === "after_message") {
|
|
3386
3433
|
const cutoff = opts.afterMessageId ? findMessageIdIndex(raw, opts.afterMessageId) : -1;
|
|
3387
3434
|
if (cutoff < 0) {
|
|
3388
3435
|
return {
|
|
3389
|
-
entries: [...state, ...
|
|
3436
|
+
entries: [...state, ...maybeCoalesce(raw)],
|
|
3390
3437
|
appliedPolicy: "full"
|
|
3391
3438
|
};
|
|
3392
3439
|
}
|
|
3393
3440
|
return {
|
|
3394
|
-
entries: [...state, ...
|
|
3441
|
+
entries: [...state, ...maybeCoalesce(raw.slice(cutoff + 1))],
|
|
3395
3442
|
appliedPolicy: "after_message"
|
|
3396
3443
|
};
|
|
3397
3444
|
}
|
|
3398
3445
|
return {
|
|
3399
|
-
entries: [...state, ...
|
|
3446
|
+
entries: [...state, ...maybeCoalesce(raw)],
|
|
3400
3447
|
appliedPolicy: "full"
|
|
3401
3448
|
};
|
|
3402
3449
|
}
|
|
@@ -4663,6 +4710,7 @@ var Session = class {
|
|
|
4663
4710
|
const out = [
|
|
4664
4711
|
{ name: "hydra", description: "Hydra session command (kill, restart, title, agent <agent>)" },
|
|
4665
4712
|
{ name: "model", description: "Switch model; omit arg to list available models" },
|
|
4713
|
+
{ name: "mode", description: "Switch mode; omit arg to list available modes" },
|
|
4666
4714
|
{ name: "sessions", description: "List all sessions" },
|
|
4667
4715
|
{ name: "help", description: "Show available commands" }
|
|
4668
4716
|
];
|
|
@@ -4933,6 +4981,60 @@ ${body}
|
|
|
4933
4981
|
});
|
|
4934
4982
|
return { stopReason: "end_turn" };
|
|
4935
4983
|
}
|
|
4984
|
+
// /mode — the text-command twin of session/set_mode, so clients that
|
|
4985
|
+
// can only send prompts (e.g. Slack) can switch modes. With no arg it
|
|
4986
|
+
// lists advertised modes; with an arg it forwards session/set_mode to
|
|
4987
|
+
// the agent and then applies the change locally. The forward +
|
|
4988
|
+
// applyModeChange pair MUST stay identical to the daemon's
|
|
4989
|
+
// session/set_mode handler (see acp-ws.ts) — applyModeChange owns the
|
|
4990
|
+
// persistence + synthetic current_mode_update broadcast for both paths.
|
|
4991
|
+
async handleModeCommand(text) {
|
|
4992
|
+
const arg = text.slice("/mode".length).trim();
|
|
4993
|
+
if (arg === "") {
|
|
4994
|
+
const modes2 = this.agentAdvertisedModes;
|
|
4995
|
+
const current = this.currentMode;
|
|
4996
|
+
let body;
|
|
4997
|
+
if (modes2.length === 0) {
|
|
4998
|
+
body = current ? `Current mode: ${current}` : "_(no modes advertised yet)_";
|
|
4999
|
+
} else {
|
|
5000
|
+
const inList = current ? modes2.some((m) => m.id === current) : true;
|
|
5001
|
+
const lines = modes2.map((m) => {
|
|
5002
|
+
const marker = m.id === current ? "\u25B6 " : " ";
|
|
5003
|
+
const desc = m.name && m.name !== m.id ? ` ${m.name}` : "";
|
|
5004
|
+
return `${marker}${m.id}${desc}`;
|
|
5005
|
+
});
|
|
5006
|
+
if (!inList && current) {
|
|
5007
|
+
lines.unshift(`\u25B6 ${current}`);
|
|
5008
|
+
}
|
|
5009
|
+
body = lines.join("\n");
|
|
5010
|
+
}
|
|
5011
|
+
this.recordAndBroadcast("session/update", {
|
|
5012
|
+
sessionId: this.upstreamSessionId,
|
|
5013
|
+
update: {
|
|
5014
|
+
sessionUpdate: "agent_message_chunk",
|
|
5015
|
+
content: { type: "text", text: `
|
|
5016
|
+
${body}
|
|
5017
|
+
` },
|
|
5018
|
+
_meta: { "hydra-acp": { synthetic: true } }
|
|
5019
|
+
}
|
|
5020
|
+
});
|
|
5021
|
+
return { stopReason: "end_turn" };
|
|
5022
|
+
}
|
|
5023
|
+
const modes = this.agentAdvertisedModes;
|
|
5024
|
+
if (modes.length > 0 && !modes.some((m) => m.id === arg)) {
|
|
5025
|
+
const known = modes.map((m) => m.id).join(", ");
|
|
5026
|
+
throw withCode(
|
|
5027
|
+
new Error(`unknown mode: ${arg} (known: ${known})`),
|
|
5028
|
+
JsonRpcErrorCodes.InvalidParams
|
|
5029
|
+
);
|
|
5030
|
+
}
|
|
5031
|
+
await this.forwardRequest("session/set_mode", {
|
|
5032
|
+
sessionId: this.sessionId,
|
|
5033
|
+
modeId: arg
|
|
5034
|
+
});
|
|
5035
|
+
this.applyModeChange(arg);
|
|
5036
|
+
return { stopReason: "end_turn" };
|
|
5037
|
+
}
|
|
4936
5038
|
// /hydra title. With an arg, sets the title directly (synchronous,
|
|
4937
5039
|
// broadcasts session_info_update). Without an arg, asks the manager
|
|
4938
5040
|
// to schedule a background synopsis via the ephemeral-agent path —
|
|
@@ -5758,12 +5860,14 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
|
|
|
5758
5860
|
}
|
|
5759
5861
|
this.broadcastPromptReceived(entry);
|
|
5760
5862
|
const promptText = extractPromptText(entry.prompt).trim();
|
|
5761
|
-
if (promptText === "/model" || promptText.startsWith("/model ") || promptText === "/sessions" || promptText === "/help") {
|
|
5863
|
+
if (promptText === "/model" || promptText.startsWith("/model ") || promptText === "/mode" || promptText.startsWith("/mode ") || promptText === "/sessions" || promptText === "/help") {
|
|
5762
5864
|
let result;
|
|
5763
5865
|
if (promptText === "/sessions") {
|
|
5764
5866
|
result = await this.handleSessionsCommand();
|
|
5765
5867
|
} else if (promptText === "/help") {
|
|
5766
5868
|
result = await this.handleHelpCommand();
|
|
5869
|
+
} else if (promptText === "/mode" || promptText.startsWith("/mode ")) {
|
|
5870
|
+
result = await this.handleModeCommand(promptText);
|
|
5767
5871
|
} else {
|
|
5768
5872
|
result = await this.handleModelCommand(promptText);
|
|
5769
5873
|
}
|
|
@@ -5825,7 +5929,12 @@ var STATE_UPDATE_KINDS = /* @__PURE__ */ new Set([
|
|
|
5825
5929
|
"current_mode_update",
|
|
5826
5930
|
"available_commands_update",
|
|
5827
5931
|
"available_modes_update",
|
|
5828
|
-
"usage_update"
|
|
5932
|
+
"usage_update",
|
|
5933
|
+
// opencode's non-spec carrier for model/mode/config state. Its canonical
|
|
5934
|
+
// form is harvested into meta.json and re-synthesized on attach, so
|
|
5935
|
+
// recording it would just give never-prompted sessions a non-empty
|
|
5936
|
+
// history and make them look interactive in the picker.
|
|
5937
|
+
"config_option_update"
|
|
5829
5938
|
]);
|
|
5830
5939
|
function isStateUpdate(method, params) {
|
|
5831
5940
|
if (method !== "session/update") {
|
|
@@ -7357,7 +7466,14 @@ var SessionManager = class {
|
|
|
7357
7466
|
histories: this.histories,
|
|
7358
7467
|
synopsisAgent: this.synopsisAgent,
|
|
7359
7468
|
synopsisModel: this.synopsisModel,
|
|
7360
|
-
persistTitle: (id, title) =>
|
|
7469
|
+
persistTitle: async (id, title) => {
|
|
7470
|
+
const live = this.get(id);
|
|
7471
|
+
if (live) {
|
|
7472
|
+
await live.retitle(title);
|
|
7473
|
+
return;
|
|
7474
|
+
}
|
|
7475
|
+
await this.persistTitle(id, title);
|
|
7476
|
+
},
|
|
7361
7477
|
persistSynopsis: (id, synopsis, through) => this.persistSynopsis(id, synopsis, through),
|
|
7362
7478
|
logger: this.logger,
|
|
7363
7479
|
npmRegistry: this.npmRegistry
|
|
@@ -7710,27 +7826,27 @@ var SessionManager = class {
|
|
|
7710
7826
|
return false;
|
|
7711
7827
|
}
|
|
7712
7828
|
}
|
|
7713
|
-
// When the last client detaches from a session that
|
|
7714
|
-
//
|
|
7715
|
-
//
|
|
7716
|
-
//
|
|
7717
|
-
//
|
|
7718
|
-
//
|
|
7719
|
-
//
|
|
7720
|
-
//
|
|
7829
|
+
// When the last client detaches from a session that was never promoted
|
|
7830
|
+
// to interactive, close it so its agent process doesn't linger until the
|
|
7831
|
+
// (default 1h) idle timeout fires. This covers both `hydra cat` runs
|
|
7832
|
+
// (born interactive:undefined with originatingClient hydra-acp-cat, every
|
|
7833
|
+
// prompt ancillary) and any other client that opened a session but never
|
|
7834
|
+
// sent a real, non-ancillary prompt. Promotion to interactive is
|
|
7835
|
+
// synchronous on the first real prompt (Session.prompt sets _interactive
|
|
7836
|
+
// = true before enqueuing), so a session that ever saw a genuine turn
|
|
7837
|
+
// resolves to true here and is left running. The cold record is kept, so
|
|
7838
|
+
// re-attaching resurrects via the reseed path.
|
|
7839
|
+
//
|
|
7840
|
+
// Note: this only fires from the explicit session/detach handler — raw WS
|
|
7841
|
+
// close deliberately does NOT reap (see acp-ws.ts), so an abrupt
|
|
7842
|
+
// disconnect of a never-prompted session falls through to the idle
|
|
7843
|
+
// timeout rather than being torn down.
|
|
7721
7844
|
async reapIfOrphanedNonInteractive(sessionId) {
|
|
7722
7845
|
const session = this.sessions.get(sessionId);
|
|
7723
7846
|
if (!session || session.attachedCount > 0) {
|
|
7724
7847
|
return;
|
|
7725
7848
|
}
|
|
7726
|
-
|
|
7727
|
-
{
|
|
7728
|
-
interactive: session.interactive,
|
|
7729
|
-
...session.originatingClient ? { originatingClient: session.originatingClient } : {}
|
|
7730
|
-
},
|
|
7731
|
-
true
|
|
7732
|
-
);
|
|
7733
|
-
if (interactive !== false) {
|
|
7849
|
+
if (session.interactive === true) {
|
|
7734
7850
|
return;
|
|
7735
7851
|
}
|
|
7736
7852
|
this.logger?.info(
|
|
@@ -8228,7 +8344,8 @@ var SessionManager = class {
|
|
|
8228
8344
|
updatedAt: used,
|
|
8229
8345
|
attachedClients: session.attachedCount,
|
|
8230
8346
|
status: "live",
|
|
8231
|
-
busy: session.turnStartedAt !== void 0
|
|
8347
|
+
busy: session.turnStartedAt !== void 0,
|
|
8348
|
+
awaitingInput: session.awaitingInput
|
|
8232
8349
|
});
|
|
8233
8350
|
}
|
|
8234
8351
|
const records = await this.store.list().catch(() => []);
|
|
@@ -8266,7 +8383,8 @@ var SessionManager = class {
|
|
|
8266
8383
|
updatedAt: used,
|
|
8267
8384
|
attachedClients: 0,
|
|
8268
8385
|
status: "cold",
|
|
8269
|
-
busy: false
|
|
8386
|
+
busy: false,
|
|
8387
|
+
awaitingInput: false
|
|
8270
8388
|
});
|
|
8271
8389
|
}
|
|
8272
8390
|
entries.sort((a, b) => a.updatedAt < b.updatedAt ? 1 : -1);
|
|
@@ -12663,10 +12781,11 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12663
12781
|
params.clientInfo,
|
|
12664
12782
|
params.clientId
|
|
12665
12783
|
);
|
|
12784
|
+
const drip = params.replayMode === "drip";
|
|
12666
12785
|
const { entries: replay, appliedPolicy } = await session.attach(
|
|
12667
12786
|
client,
|
|
12668
12787
|
params.historyPolicy,
|
|
12669
|
-
{ afterMessageId: params.afterMessageId }
|
|
12788
|
+
{ afterMessageId: params.afterMessageId, raw: drip }
|
|
12670
12789
|
);
|
|
12671
12790
|
state.attached.set(session.sessionId, {
|
|
12672
12791
|
sessionId: session.sessionId,
|
|
@@ -12674,10 +12793,35 @@ function registerAcpWsEndpoint(app, deps) {
|
|
|
12674
12793
|
readonly
|
|
12675
12794
|
});
|
|
12676
12795
|
app.log.info(
|
|
12677
|
-
`session/attach OK sessionId=${session.sessionId} clientId=${client.clientId} attachedCount=${state.attached.size} requestedPolicy=${params.historyPolicy} appliedPolicy=${appliedPolicy} replayed=${replay.length} readonly=${readonly}`
|
|
12796
|
+
`session/attach OK sessionId=${session.sessionId} clientId=${client.clientId} attachedCount=${state.attached.size} requestedPolicy=${params.historyPolicy} appliedPolicy=${appliedPolicy} replayed=${replay.length} readonly=${readonly}${drip ? " replayMode=drip" : ""}`
|
|
12678
12797
|
);
|
|
12679
|
-
|
|
12680
|
-
|
|
12798
|
+
if (drip) {
|
|
12799
|
+
const speed = params.dripSpeed && params.dripSpeed > 0 ? params.dripSpeed : 1;
|
|
12800
|
+
const MAX_GAP_MS = 750;
|
|
12801
|
+
void (async () => {
|
|
12802
|
+
let prev = null;
|
|
12803
|
+
for (const note of replay) {
|
|
12804
|
+
const at = typeof note.recordedAt === "number" ? note.recordedAt : null;
|
|
12805
|
+
if (prev !== null && at !== null) {
|
|
12806
|
+
const gap = Math.min(MAX_GAP_MS, Math.max(0, (at - prev) / speed));
|
|
12807
|
+
if (gap > 0) {
|
|
12808
|
+
await new Promise((r) => setTimeout(r, gap));
|
|
12809
|
+
}
|
|
12810
|
+
}
|
|
12811
|
+
if (at !== null) {
|
|
12812
|
+
prev = at;
|
|
12813
|
+
}
|
|
12814
|
+
try {
|
|
12815
|
+
await connection.notify(note.method, note.params);
|
|
12816
|
+
} catch {
|
|
12817
|
+
return;
|
|
12818
|
+
}
|
|
12819
|
+
}
|
|
12820
|
+
})();
|
|
12821
|
+
} else {
|
|
12822
|
+
for (const note of replay) {
|
|
12823
|
+
await connection.notify(note.method, note.params);
|
|
12824
|
+
}
|
|
12681
12825
|
}
|
|
12682
12826
|
session.replayPendingPermissions(client);
|
|
12683
12827
|
const modesPayload = buildModesPayload(session);
|