@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/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. Lets list views
1565
- // hide cat-style ancillary sessions by default while letting an
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 all = coalesceReplay(await this.getHistorySnapshot());
3383
+ const raw = await this.getHistorySnapshot();
3365
3384
  const state = this.buildStateSnapshotReplay();
3366
3385
  if (historyPolicy === "after_message") {
3367
- const cutoff = opts.afterMessageId ? findMessageIdIndex(all, opts.afterMessageId) : -1;
3386
+ const cutoff = opts.afterMessageId ? findMessageIdIndex(raw, opts.afterMessageId) : -1;
3368
3387
  if (cutoff < 0) {
3369
- return { entries: [...state, ...all], appliedPolicy: "full" };
3388
+ return {
3389
+ entries: [...state, ...coalesceReplay(raw)],
3390
+ appliedPolicy: "full"
3391
+ };
3370
3392
  }
3371
3393
  return {
3372
- entries: [...state, ...all.slice(cutoff + 1)],
3394
+ entries: [...state, ...coalesceReplay(raw.slice(cutoff + 1))],
3373
3395
  appliedPolicy: "after_message"
3374
3396
  };
3375
3397
  }
3376
- return { entries: [...state, ...all], appliedPolicy: "full" };
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.currentModel = trimmed;
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 || trimmed === this.currentModel) {
4591
+ if (!trimmed) {
4549
4592
  return;
4550
4593
  }
4551
- this.logger?.info(
4552
- `applyModelChange: sessionId=${this.sessionId} ${JSON.stringify(this.currentModel)} \u2192 ${JSON.stringify(trimmed)}`
4553
- );
4554
- this.currentModel = trimmed;
4555
- for (const handler of this.modelHandlers) {
4556
- try {
4557
- handler(trimmed);
4558
- } catch {
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 so the persistence hook and any other listeners
4576
- // see the change, identical to the agent-notification path.
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 || trimmed === this.currentMode) {
4627
+ if (!trimmed) {
4580
4628
  return;
4581
4629
  }
4582
- this.logger?.info(
4583
- `applyModeChange: sessionId=${this.sessionId} ${JSON.stringify(this.currentMode)} \u2192 ${JSON.stringify(trimmed)}`
4584
- );
4585
- this.currentMode = trimmed;
4586
- for (const handler of this.modeHandlers) {
4587
- try {
4588
- handler(trimmed);
4589
- } catch {
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
- ...hydraCommandsAsAdvertised(),
4604
- { name: "model <model-id>", description: "Switch model; omit arg to list available models" },
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. Picker and
6220
- // `sessions list` use this to hide cat-style ancillary sessions by
6221
- // default; carried in meta.json so cold sessions filter the same way.
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.resolveImportCwd(params.cwd);
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 resolveImportCwd(cwd) {
7706
+ async dirExists(cwd) {
7620
7707
  try {
7621
- const stat2 = await fs13.stat(cwd);
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
- return os2.homedir();
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 used = await historyMtimeIso(session.sessionId) ?? new Date(session.updatedAt).toISOString();
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 used = await historyMtimeIso(r.sessionId) ?? r.updatedAt;
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 historyMtimeIso (used when the history file
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 historyMtimeIso(sessionId) {
9105
+ async function historyStatus(sessionId) {
8939
9106
  try {
8940
9107
  const st = await fs13.stat(paths.historyFile(sessionId));
8941
- return new Date(st.mtimeMs).toISOString();
9108
+ return {
9109
+ mtime: new Date(st.mtimeMs).toISOString(),
9110
+ hasContent: st.size > 0
9111
+ };
8942
9112
  } catch {
8943
- return void 0;
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 sessions = await manager.list({ cwd: query?.cwd });
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.get("/v1/sessions/search", async (request, reply) => {
11175
- const query = request.query;
11176
- const q = query?.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 = query?.sessionIds ? query.sessionIds.split(",").filter((s) => s.length > 0) : void 0;
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, defaults) {
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
- ...resurrectParams,
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,