@hydra-acp/cli 0.1.59 → 0.1.60

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/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(24),
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("claude-acp"),
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: 24,
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, ...coalesceReplay(raw)],
3436
+ entries: [...state, ...maybeCoalesce(raw)],
3390
3437
  appliedPolicy: "full"
3391
3438
  };
3392
3439
  }
3393
3440
  return {
3394
- entries: [...state, ...coalesceReplay(raw.slice(cutoff + 1))],
3441
+ entries: [...state, ...maybeCoalesce(raw.slice(cutoff + 1))],
3395
3442
  appliedPolicy: "after_message"
3396
3443
  };
3397
3444
  }
3398
3445
  return {
3399
- entries: [...state, ...coalesceReplay(raw)],
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) => this.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 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.
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
- const interactive = effectiveInteractive(
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
- for (const note of replay) {
12680
- await connection.notify(note.method, note.params);
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hydra-acp/cli",
3
- "version": "0.1.59",
3
+ "version": "0.1.60",
4
4
  "description": "Multi-client ACP session daemon: spawn agents, attach over WSS, multiplex sessions across editors.",
5
5
  "license": "MIT",
6
6
  "type": "module",