@hydra-acp/cli 0.1.57 → 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/cli.js CHANGED
@@ -108,7 +108,11 @@ var init_paths = __esm({
108
108
  // line, append-only so concurrent TUIs don't lose each other's
109
109
  // writes.
110
110
  globalTuiHistoryFile: () => path.join(hydraHome(), "prompt-history"),
111
- tuiLogFile: () => path.join(hydraHome(), "tui.log")
111
+ tuiLogFile: () => path.join(hydraHome(), "tui.log"),
112
+ // Diagnostic dump of every JSON-RPC message that crosses a `hydra-acp
113
+ // shim` process. Append-only NDJSON. One file shared by every shim;
114
+ // each line carries the writing process's pid for disambiguation.
115
+ shimWireLogFile: () => path.join(hydraHome(), "shim-wire.log")
112
116
  };
113
117
  }
114
118
  });
@@ -1025,6 +1029,12 @@ function extractHydraMeta(meta) {
1025
1029
  if (typeof obj.mcpStdin === "boolean") {
1026
1030
  out.mcpStdin = obj.mcpStdin;
1027
1031
  }
1032
+ if (typeof obj.interactive === "boolean") {
1033
+ out.interactive = obj.interactive;
1034
+ }
1035
+ if (typeof obj.ancillary === "boolean") {
1036
+ out.ancillary = obj.ancillary;
1037
+ }
1028
1038
  if (typeof obj.promptAmending === "boolean") {
1029
1039
  out.promptAmending = obj.promptAmending;
1030
1040
  }
@@ -1269,10 +1279,14 @@ var init_types = __esm({
1269
1279
  // local session, an import is a cross-machine takeover.
1270
1280
  forkedFromSessionId: z3.string().optional(),
1271
1281
  forkedFromMessageId: z3.string().optional(),
1272
- // clientInfo from the process that issued session/new. Lets list views
1273
- // hide cat-style ancillary sessions by default while letting an
1274
- // override flag surface them.
1282
+ // clientInfo from the process that issued session/new. Carried for
1283
+ // log/display; the effective filtering signal is `interactive` below.
1275
1284
  originatingClient: z3.object({ name: z3.string(), version: z3.string().optional() }).optional(),
1285
+ // Tristate filter signal computed by effectiveInteractive(): explicit
1286
+ // when the record stored a value, else inferred (legacy cat hint or
1287
+ // history-presence). Clients can use this to render a hint glyph
1288
+ // (e.g. dim non-interactive rows when the user toggles them in).
1289
+ interactive: z3.boolean().optional(),
1276
1290
  updatedAt: z3.string(),
1277
1291
  attachedClients: z3.number().int().nonnegative(),
1278
1292
  status: z3.enum(["live", "cold"]).default("live"),
@@ -1295,7 +1309,10 @@ var init_types = __esm({
1295
1309
  });
1296
1310
  SessionPromptParams = z3.object({
1297
1311
  sessionId: z3.string(),
1298
- prompt: z3.array(z3.unknown())
1312
+ prompt: z3.array(z3.unknown()),
1313
+ // Hydra extensions ride under _meta["hydra-acp"] (e.g. `ancillary` to
1314
+ // mark a non-promoting turn). Kept so Session.prompt can read them.
1315
+ _meta: z3.record(z3.unknown()).optional()
1299
1316
  });
1300
1317
  SessionCancelParams = z3.object({
1301
1318
  sessionId: z3.string()
@@ -2622,6 +2639,12 @@ var init_session = __esm({
2622
2639
  forkedFromSessionId;
2623
2640
  forkedFromMessageId;
2624
2641
  originatingClient;
2642
+ // Tristate. Mutates from undefined → true on first prompt (or directly
2643
+ // false if init.interactive === false). Persisted via interactiveHandlers.
2644
+ _interactive;
2645
+ get interactive() {
2646
+ return this._interactive;
2647
+ }
2625
2648
  title;
2626
2649
  // Snapshot state delivered to attaching clients via the attach
2627
2650
  // response _meta rather than via history replay (which would be
@@ -2750,6 +2773,7 @@ var init_session = __esm({
2750
2773
  agentModelsHandlers = [];
2751
2774
  modelHandlers = [];
2752
2775
  modeHandlers = [];
2776
+ interactiveHandlers = [];
2753
2777
  usageHandlers = [];
2754
2778
  cumulativeCost = 0;
2755
2779
  // Total cost across all agent lives. costAmount in the returned snapshot
@@ -2833,6 +2857,7 @@ var init_session = __esm({
2833
2857
  if (init.firstPromptSeeded) {
2834
2858
  this._firstPromptSeeded = true;
2835
2859
  }
2860
+ this._interactive = init.interactive;
2836
2861
  this.historyStore = init.historyStore;
2837
2862
  this.historyMaxEntries = init.historyMaxEntries ?? DEFAULT_HISTORY_MAX_ENTRIES;
2838
2863
  this.compactEvery = Math.max(1, Math.floor(this.historyMaxEntries * 0.2));
@@ -3095,19 +3120,25 @@ var init_session = __esm({
3095
3120
  return this.loadReplay(historyPolicy, opts);
3096
3121
  }
3097
3122
  async loadReplay(historyPolicy, opts) {
3098
- const all = coalesceReplay(await this.getHistorySnapshot());
3123
+ const raw = await this.getHistorySnapshot();
3099
3124
  const state = this.buildStateSnapshotReplay();
3100
3125
  if (historyPolicy === "after_message") {
3101
- const cutoff = opts.afterMessageId ? findMessageIdIndex(all, opts.afterMessageId) : -1;
3126
+ const cutoff = opts.afterMessageId ? findMessageIdIndex(raw, opts.afterMessageId) : -1;
3102
3127
  if (cutoff < 0) {
3103
- return { entries: [...state, ...all], appliedPolicy: "full" };
3128
+ return {
3129
+ entries: [...state, ...coalesceReplay(raw)],
3130
+ appliedPolicy: "full"
3131
+ };
3104
3132
  }
3105
3133
  return {
3106
- entries: [...state, ...all.slice(cutoff + 1)],
3134
+ entries: [...state, ...coalesceReplay(raw.slice(cutoff + 1))],
3107
3135
  appliedPolicy: "after_message"
3108
3136
  };
3109
3137
  }
3110
- return { entries: [...state, ...all], appliedPolicy: "full" };
3138
+ return {
3139
+ entries: [...state, ...coalesceReplay(raw)],
3140
+ appliedPolicy: "full"
3141
+ };
3111
3142
  }
3112
3143
  // Synthesizes one session/update notification per cached STATE_UPDATE_KIND
3113
3144
  // so an attaching client receives the current snapshot through the
@@ -3288,6 +3319,18 @@ var init_session = __esm({
3288
3319
  const messageId = generateMessageId();
3289
3320
  this.maybeSeedTitleFromPrompt(params);
3290
3321
  this._firstPromptSeeded = true;
3322
+ const ancillary = extractHydraMeta(
3323
+ (params ?? {})._meta
3324
+ ).ancillary === true;
3325
+ if (!ancillary && this._interactive === void 0) {
3326
+ this._interactive = true;
3327
+ for (const handler of this.interactiveHandlers) {
3328
+ try {
3329
+ handler(true);
3330
+ } catch {
3331
+ }
3332
+ }
3333
+ }
3291
3334
  return this.enqueueUserPrompt(client, params, messageId);
3292
3335
  }
3293
3336
  // DEVIATION FROM RFD #533: this broadcast is deliberately deferred
@@ -4070,13 +4113,7 @@ var init_session = __esm({
4070
4113
  this.logger?.info(
4071
4114
  `live config_option_update(model): sessionId=${this.sessionId} ${JSON.stringify(this.currentModel)} \u2192 ${JSON.stringify(trimmed)}`
4072
4115
  );
4073
- this.currentModel = trimmed;
4074
- for (const handler of this.modelHandlers) {
4075
- try {
4076
- handler(trimmed);
4077
- } catch {
4078
- }
4079
- }
4116
+ this.applyModelChange(trimmed);
4080
4117
  }
4081
4118
  }
4082
4119
  break;
@@ -4272,6 +4309,9 @@ var init_session = __esm({
4272
4309
  onModeChange(handler) {
4273
4310
  this.modeHandlers.push(handler);
4274
4311
  }
4312
+ onInteractiveChange(handler) {
4313
+ this.interactiveHandlers.push(handler);
4314
+ }
4275
4315
  // Apply a model change initiated by a client request (session/set_model)
4276
4316
  // when the agent doesn't emit a current_model_update notification, or
4277
4317
  // emits a non-spec shape (e.g. config_option_update). Fires modelHandlers
@@ -6269,6 +6309,9 @@ async function listSessions(target, opts = {}, fetchImpl = fetch) {
6269
6309
  if (opts.all) {
6270
6310
  url.searchParams.set("all", "true");
6271
6311
  }
6312
+ if (opts.includeNonInteractive) {
6313
+ url.searchParams.set("includeNonInteractive", "true");
6314
+ }
6272
6315
  const response = await fetchImpl(url.toString(), {
6273
6316
  headers: { Authorization: `Bearer ${target.token}` }
6274
6317
  });
@@ -6295,7 +6338,8 @@ async function listSessions(target, opts = {}, fetchImpl = fetch) {
6295
6338
  forkedFromSessionId: s.forkedFromSessionId,
6296
6339
  forkedFromMessageId: s.forkedFromMessageId,
6297
6340
  busy: s.busy,
6298
- originatingClient: s.originatingClient
6341
+ originatingClient: s.originatingClient,
6342
+ interactive: s.interactive
6299
6343
  }));
6300
6344
  }
6301
6345
  async function forkSession(target, id, opts = {}, fetchImpl = fetch) {
@@ -6359,13 +6403,17 @@ async function regenSessionTitle(target, id, fetchImpl = fetch) {
6359
6403
  }
6360
6404
  }
6361
6405
  async function searchSessions(target, query, opts = {}, fetchImpl = fetch) {
6362
- const url = new URL(`${target.baseUrl}/v1/sessions/search`);
6363
- url.searchParams.set("q", query);
6406
+ const body = { q: query };
6364
6407
  if (opts.sessionIds && opts.sessionIds.length > 0) {
6365
- url.searchParams.set("sessionIds", opts.sessionIds.join(","));
6408
+ body.sessionIds = opts.sessionIds;
6366
6409
  }
6367
- const response = await fetchImpl(url.toString(), {
6368
- headers: { Authorization: `Bearer ${target.token}` }
6410
+ const response = await fetchImpl(`${target.baseUrl}/v1/sessions/search`, {
6411
+ method: "POST",
6412
+ headers: {
6413
+ Authorization: `Bearer ${target.token}`,
6414
+ "Content-Type": "application/json"
6415
+ },
6416
+ body: JSON.stringify(body)
6369
6417
  });
6370
6418
  if (!response.ok) {
6371
6419
  throw new Error(`daemon returned HTTP ${response.status}`);
@@ -7864,6 +7912,85 @@ var init_update_check = __esm({
7864
7912
  }
7865
7913
  });
7866
7914
 
7915
+ // src/core/cwd.ts
7916
+ import * as fs22 from "fs/promises";
7917
+ import * as path17 from "path";
7918
+ async function validateLocalCwd(input) {
7919
+ const trimmed = input.trim();
7920
+ if (trimmed.length === 0) {
7921
+ return { ok: false, reason: "path is empty" };
7922
+ }
7923
+ const resolved = path17.resolve(expandHome(trimmed));
7924
+ let stat5;
7925
+ try {
7926
+ stat5 = await fs22.stat(resolved);
7927
+ } catch {
7928
+ return { ok: false, reason: `${resolved} does not exist` };
7929
+ }
7930
+ if (!stat5.isDirectory()) {
7931
+ return { ok: false, reason: `${resolved} is not a directory` };
7932
+ }
7933
+ return { ok: true, path: resolved };
7934
+ }
7935
+ async function pickInitialLocalCwd(sessionCwd) {
7936
+ const candidates = [];
7937
+ const seen = /* @__PURE__ */ new Set();
7938
+ const push = (p) => {
7939
+ if (!seen.has(p)) {
7940
+ seen.add(p);
7941
+ candidates.push(p);
7942
+ }
7943
+ };
7944
+ push(sessionCwd);
7945
+ if (sessionCwd.startsWith("/Users/")) {
7946
+ push("/home/" + sessionCwd.slice("/Users/".length));
7947
+ } else if (sessionCwd.startsWith("/home/")) {
7948
+ push("/Users/" + sessionCwd.slice("/home/".length));
7949
+ }
7950
+ for (const candidate of candidates) {
7951
+ try {
7952
+ const stat5 = await fs22.stat(candidate);
7953
+ if (stat5.isDirectory()) {
7954
+ return candidate;
7955
+ }
7956
+ } catch {
7957
+ }
7958
+ }
7959
+ return null;
7960
+ }
7961
+ async function completeLocalPath(input) {
7962
+ const lastSlash = input.lastIndexOf("/");
7963
+ let prefix;
7964
+ let basePrefix;
7965
+ let dirForRead;
7966
+ if (lastSlash === -1) {
7967
+ prefix = "";
7968
+ basePrefix = input;
7969
+ dirForRead = ".";
7970
+ } else {
7971
+ prefix = input.slice(0, lastSlash + 1);
7972
+ basePrefix = input.slice(lastSlash + 1);
7973
+ dirForRead = lastSlash === 0 ? "/" : prefix;
7974
+ }
7975
+ const resolvedDir = path17.resolve(expandHome(dirForRead));
7976
+ let entries;
7977
+ try {
7978
+ const list = await fs22.readdir(resolvedDir, { withFileTypes: true });
7979
+ entries = list.map((e) => ({ name: e.name, isDir: e.isDirectory() }));
7980
+ } catch {
7981
+ return { prefix, basePrefix, matches: [] };
7982
+ }
7983
+ const showHidden = basePrefix.startsWith(".");
7984
+ const matches = entries.filter((e) => e.name.startsWith(basePrefix)).filter((e) => showHidden || !e.name.startsWith(".")).map((e) => e.isDir ? `${e.name}/` : e.name).sort();
7985
+ return { prefix, basePrefix, matches };
7986
+ }
7987
+ var init_cwd = __esm({
7988
+ "src/core/cwd.ts"() {
7989
+ "use strict";
7990
+ init_config();
7991
+ }
7992
+ });
7993
+
7867
7994
  // src/tui/input.ts
7868
7995
  function formatPasteToken(id, lineCount) {
7869
7996
  return `[pasted #${id} +${lineCount} lines]`;
@@ -8125,12 +8252,18 @@ var init_input = __esm({
8125
8252
  return [];
8126
8253
  case "ctrl-c":
8127
8254
  return this.handleCtrlC();
8128
- case "ctrl-d":
8255
+ case "ctrl-d": {
8129
8256
  if (this.bufferIsEmpty()) {
8130
8257
  return [{ type: "exit" }];
8131
8258
  }
8259
+ const lastLine = this.buffer[this.buffer.length - 1] ?? "";
8260
+ const atEndOfBuffer = this.row === this.buffer.length - 1 && this.col >= lastLine.length;
8261
+ if (atEndOfBuffer) {
8262
+ return [{ type: "exit" }];
8263
+ }
8132
8264
  this.deleteForward();
8133
8265
  return [];
8266
+ }
8134
8267
  case "ctrl-l":
8135
8268
  return [{ type: "redraw" }];
8136
8269
  case "ctrl-p":
@@ -8808,9 +8941,9 @@ var init_input = __esm({
8808
8941
  });
8809
8942
 
8810
8943
  // src/tui/attachments.ts
8811
- import path17 from "path";
8944
+ import path18 from "path";
8812
8945
  function mimeFromExtension(p) {
8813
- return EXTENSION_TO_MIME[path17.extname(p).toLowerCase()] ?? null;
8946
+ return EXTENSION_TO_MIME[path18.extname(p).toLowerCase()] ?? null;
8814
8947
  }
8815
8948
  function isSupportedImagePath(p) {
8816
8949
  return mimeFromExtension(p) !== null;
@@ -10698,9 +10831,9 @@ uncaught: ${err.stack ?? err.message}
10698
10831
  this.permissionPrompt = spec ? { ...spec } : null;
10699
10832
  this.repaint();
10700
10833
  }
10701
- // Two-line confirmation modal that takes over the prompt area. Used to
10702
- // ask "interrupt before exit?" when the user quits during an in-flight
10703
- // turn that no one else is watching. Pass null to dismiss.
10834
+ // Two-line confirmation modal that takes over the prompt area. Pass
10835
+ // null to dismiss. Currently unused kept as a generic primitive for
10836
+ // any future modal that needs a question + hint footer.
10704
10837
  setConfirmPrompt(spec) {
10705
10838
  this.confirmPrompt = spec ? { ...spec } : null;
10706
10839
  this.repaint();
@@ -12301,7 +12434,11 @@ var init_import_action_prompt = __esm({
12301
12434
  // src/tui/picker.ts
12302
12435
  function createPickerPrefs() {
12303
12436
  return {
12304
- filters: { cwdOnly: false, hostFilter: "__local", showCat: false }
12437
+ filters: {
12438
+ cwdOnly: false,
12439
+ hostFilter: "__local",
12440
+ includeNonInteractive: false
12441
+ }
12305
12442
  };
12306
12443
  }
12307
12444
  async function pickSession(term, opts) {
@@ -12329,10 +12466,8 @@ async function pickSession(term, opts) {
12329
12466
  if (prefs.filters.cwdOnly) {
12330
12467
  base = base.filter((s) => s.cwd === opts.cwd);
12331
12468
  }
12332
- if (!prefs.filters.showCat) {
12333
- base = base.filter(
12334
- (s) => s.originatingClient?.name !== HYDRA_CAT_CLIENT_NAME
12335
- );
12469
+ if (!prefs.filters.includeNonInteractive) {
12470
+ base = base.filter((s) => s.interactive === true);
12336
12471
  }
12337
12472
  base = filterByHost(base, prefs.filters.hostFilter);
12338
12473
  return base;
@@ -12534,8 +12669,8 @@ async function pickSession(term, opts) {
12534
12669
  prefs.filters.hostFilter === "__local" ? "host: local" : `host: ${prefs.filters.hostFilter}`
12535
12670
  );
12536
12671
  }
12537
- if (prefs.filters.showCat) {
12538
- parts.push("+cat");
12672
+ if (prefs.filters.includeNonInteractive) {
12673
+ parts.push("+non-interactive");
12539
12674
  }
12540
12675
  if (above > 0) {
12541
12676
  parts.push(`\u2191 ${above} above`);
@@ -13176,7 +13311,9 @@ ${cells}`;
13176
13311
  try {
13177
13312
  const beforeKey = refreshOpts.silent ? renderFingerprint() : "";
13178
13313
  const beforeTotal = total;
13179
- const next = await listSessions(opts.target);
13314
+ const next = await listSessions(opts.target, {
13315
+ includeNonInteractive: true
13316
+ });
13180
13317
  const followId = preferredId ?? (selectedIdx > 0 ? visible[selectedIdx - 1]?.sessionId : void 0);
13181
13318
  allSessions = sortSessions(next, opts.cwd);
13182
13319
  applyFilter();
@@ -13654,7 +13791,7 @@ ${cells}`;
13654
13791
  const effects = composer.feed(event);
13655
13792
  const after = composer.state();
13656
13793
  const unchanged = before.buffer.length === after.buffer.length && before.buffer.every((line, i) => line === after.buffer[i]) && before.row === after.row && before.col === after.col;
13657
- if (effects.some((e) => e.type === "exit") || unchanged && name === "CTRL_D") {
13794
+ if (effects.some((e) => e.type === "exit")) {
13658
13795
  cleanup();
13659
13796
  resolve8({ kind: "abort" });
13660
13797
  return;
@@ -13754,7 +13891,7 @@ ${cells}`;
13754
13891
  }
13755
13892
  if (name === "i" || name === "I") {
13756
13893
  const keepId = selectedIdx > 0 ? visible[selectedIdx - 1]?.sessionId : void 0;
13757
- prefs.filters.showCat = !prefs.filters.showCat;
13894
+ prefs.filters.includeNonInteractive = !prefs.filters.includeNonInteractive;
13758
13895
  applyFilter();
13759
13896
  restoreCursorAfterFilter(keepId);
13760
13897
  renderFromScratch();
@@ -14020,7 +14157,6 @@ var init_picker = __esm({
14020
14157
  init_session_row();
14021
14158
  init_paths();
14022
14159
  init_session();
14023
- init_hydra_version();
14024
14160
  init_discovery();
14025
14161
  init_history();
14026
14162
  init_input();
@@ -14060,85 +14196,6 @@ var init_picker = __esm({
14060
14196
  }
14061
14197
  });
14062
14198
 
14063
- // src/core/cwd.ts
14064
- import * as fs21 from "fs/promises";
14065
- import * as path18 from "path";
14066
- async function validateLocalCwd(input) {
14067
- const trimmed = input.trim();
14068
- if (trimmed.length === 0) {
14069
- return { ok: false, reason: "path is empty" };
14070
- }
14071
- const resolved = path18.resolve(expandHome(trimmed));
14072
- let stat5;
14073
- try {
14074
- stat5 = await fs21.stat(resolved);
14075
- } catch {
14076
- return { ok: false, reason: `${resolved} does not exist` };
14077
- }
14078
- if (!stat5.isDirectory()) {
14079
- return { ok: false, reason: `${resolved} is not a directory` };
14080
- }
14081
- return { ok: true, path: resolved };
14082
- }
14083
- async function pickInitialLocalCwd(sessionCwd) {
14084
- const candidates = [];
14085
- const seen = /* @__PURE__ */ new Set();
14086
- const push = (p) => {
14087
- if (!seen.has(p)) {
14088
- seen.add(p);
14089
- candidates.push(p);
14090
- }
14091
- };
14092
- push(sessionCwd);
14093
- if (sessionCwd.startsWith("/Users/")) {
14094
- push("/home/" + sessionCwd.slice("/Users/".length));
14095
- } else if (sessionCwd.startsWith("/home/")) {
14096
- push("/Users/" + sessionCwd.slice("/home/".length));
14097
- }
14098
- for (const candidate of candidates) {
14099
- try {
14100
- const stat5 = await fs21.stat(candidate);
14101
- if (stat5.isDirectory()) {
14102
- return candidate;
14103
- }
14104
- } catch {
14105
- }
14106
- }
14107
- return null;
14108
- }
14109
- async function completeLocalPath(input) {
14110
- const lastSlash = input.lastIndexOf("/");
14111
- let prefix;
14112
- let basePrefix;
14113
- let dirForRead;
14114
- if (lastSlash === -1) {
14115
- prefix = "";
14116
- basePrefix = input;
14117
- dirForRead = ".";
14118
- } else {
14119
- prefix = input.slice(0, lastSlash + 1);
14120
- basePrefix = input.slice(lastSlash + 1);
14121
- dirForRead = lastSlash === 0 ? "/" : prefix;
14122
- }
14123
- const resolvedDir = path18.resolve(expandHome(dirForRead));
14124
- let entries;
14125
- try {
14126
- const list = await fs21.readdir(resolvedDir, { withFileTypes: true });
14127
- entries = list.map((e) => ({ name: e.name, isDir: e.isDirectory() }));
14128
- } catch {
14129
- return { prefix, basePrefix, matches: [] };
14130
- }
14131
- const showHidden = basePrefix.startsWith(".");
14132
- const matches = entries.filter((e) => e.name.startsWith(basePrefix)).filter((e) => showHidden || !e.name.startsWith(".")).map((e) => e.isDir ? `${e.name}/` : e.name).sort();
14133
- return { prefix, basePrefix, matches };
14134
- }
14135
- var init_cwd = __esm({
14136
- "src/core/cwd.ts"() {
14137
- "use strict";
14138
- init_config();
14139
- }
14140
- });
14141
-
14142
14199
  // src/tui/completion.ts
14143
14200
  function longestCommonPrefix(names) {
14144
14201
  if (names.length === 0) {
@@ -14189,24 +14246,25 @@ async function promptForImportCwd(term, session, opts = {}) {
14189
14246
  const defaultCwd = opts.defaultCwd ?? await pickInitialLocalCwd(session.cwd) ?? os6.homedir();
14190
14247
  resetTerminalModes();
14191
14248
  const shortId2 = stripHydraSessionPrefix(session.sessionId);
14192
- const fromMachine = session.importedFromMachine ?? "another machine";
14193
14249
  const originalCwd = shortenHomePath(session.cwd);
14250
+ const title = opts.title ?? "Fork locally \u2014 choose cwd";
14251
+ const intro = opts.intro ?? "Pick a local cwd for this session:";
14252
+ const headerRows = [
14253
+ { label: "session: ", value: shortId2 },
14254
+ ...session.importedFromMachine ? [{ label: "from: ", value: session.importedFromMachine }] : [],
14255
+ { label: "cwd: ", value: originalCwd }
14256
+ ];
14194
14257
  let buffer = defaultCwd;
14195
14258
  let errorLine = null;
14196
14259
  let busy = false;
14197
14260
  let layout = null;
14198
14261
  const render = () => {
14199
- const contentHeight = 9;
14262
+ const contentHeight = headerRows.length + 6;
14200
14263
  layout = drawBox(term, {
14201
14264
  contentHeight,
14202
- title: "Fork locally \u2014 choose cwd"
14265
+ title
14203
14266
  });
14204
14267
  const innerW = layout.contentW;
14205
- const headerRows = [
14206
- { label: "session: ", value: shortId2 },
14207
- { label: "from: ", value: fromMachine },
14208
- { label: "cwd: ", value: originalCwd }
14209
- ];
14210
14268
  let row = 0;
14211
14269
  for (const hr of headerRows) {
14212
14270
  term.moveTo(layout.contentX, layout.contentY + row);
@@ -14216,7 +14274,7 @@ async function promptForImportCwd(term, session, opts = {}) {
14216
14274
  }
14217
14275
  row++;
14218
14276
  term.moveTo(layout.contentX, layout.contentY + row);
14219
- term.noFormat(" Pick a local cwd for this session:");
14277
+ term.noFormat(` ${intro}`);
14220
14278
  row += 2;
14221
14279
  paintInputRow(row);
14222
14280
  row += 2;
@@ -14230,7 +14288,7 @@ async function promptForImportCwd(term, session, opts = {}) {
14230
14288
  );
14231
14289
  }
14232
14290
  };
14233
- const inputRow = () => 6;
14291
+ const inputRow = () => headerRows.length + 3;
14234
14292
  const paintInputRow = (rowOffset) => {
14235
14293
  if (!layout) {
14236
14294
  return;
@@ -14411,7 +14469,7 @@ var init_import_cwd_prompt = __esm({
14411
14469
 
14412
14470
  // src/tui/clipboard.ts
14413
14471
  import { spawn as nodeSpawn } from "child_process";
14414
- import fs22 from "fs/promises";
14472
+ import fs23 from "fs/promises";
14415
14473
  import os7 from "os";
14416
14474
  import path19 from "path";
14417
14475
  async function readClipboard(envIn = {}) {
@@ -14452,7 +14510,7 @@ async function readMacOS(env) {
14452
14510
  return img;
14453
14511
  }
14454
14512
  } catch {
14455
- await fs22.unlink(tmpPath).catch(() => void 0);
14513
+ await fs23.unlink(tmpPath).catch(() => void 0);
14456
14514
  }
14457
14515
  try {
14458
14516
  const buf = await runCapture(env.spawn, "pbpaste", []);
@@ -14567,9 +14625,9 @@ async function which(env, cmd) {
14567
14625
  }
14568
14626
  async function readFileAsAttachment(p, unlinkAfter) {
14569
14627
  try {
14570
- const buf = await fs22.readFile(p);
14628
+ const buf = await fs23.readFile(p);
14571
14629
  if (unlinkAfter) {
14572
- await fs22.unlink(p).catch(() => void 0);
14630
+ await fs23.unlink(p).catch(() => void 0);
14573
14631
  }
14574
14632
  if (buf.length === 0) {
14575
14633
  return { ok: false, reason: "no image on clipboard" };
@@ -14694,7 +14752,7 @@ function parseReattachResponse(result) {
14694
14752
  return out;
14695
14753
  }
14696
14754
  function shouldDriftSnap(args) {
14697
- return !args.replayDraining && args.pendingTurns > 0 && args.queueSize === 0 && !args.ownTurnInFlight && !args.hasInFlightHead;
14755
+ return !args.replayDraining && !args.amended && args.pendingTurns > 0 && args.queueSize === 0 && !args.ownTurnInFlight && !args.hasInFlightHead;
14698
14756
  }
14699
14757
  function computeAttachReconcile(args) {
14700
14758
  if (args.daemonTurnStartedAt !== void 0) {
@@ -14715,10 +14773,10 @@ var init_reconnect_state = __esm({
14715
14773
  });
14716
14774
 
14717
14775
  // src/tui/app.ts
14718
- import { appendFileSync, statSync, renameSync } from "fs";
14776
+ import { appendFileSync, statSync as statSync2, renameSync as renameSync2 } from "fs";
14719
14777
  import { nanoid as nanoid3 } from "nanoid";
14720
14778
  import termkit from "terminal-kit";
14721
- import fs23 from "fs/promises";
14779
+ import fs24 from "fs/promises";
14722
14780
  import path20 from "path";
14723
14781
  function isReadonlyForbiddenEffect(effect) {
14724
14782
  switch (effect.type) {
@@ -14856,6 +14914,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
14856
14914
  }
14857
14915
  };
14858
14916
  let pendingTurns = 0;
14917
+ let cancelling = false;
14859
14918
  let currentHeadMessageId;
14860
14919
  let sessionBusySince = null;
14861
14920
  let sessionElapsedTimer = null;
@@ -14866,6 +14925,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
14866
14925
  pendingTurns = Math.max(0, pendingTurns + delta);
14867
14926
  const screenReady = typeof screenRef !== "undefined" && screenRef !== null;
14868
14927
  if (before === 0 && pendingTurns > 0) {
14928
+ cancelling = false;
14869
14929
  sessionBusySince = Date.now();
14870
14930
  lastUpdateAt = Date.now();
14871
14931
  dispatcherRef?.setTurnRunning(true);
@@ -14886,6 +14946,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
14886
14946
  }, 1e3);
14887
14947
  }
14888
14948
  } else if (before > 0 && pendingTurns === 0) {
14949
+ cancelling = false;
14889
14950
  sessionBusySince = null;
14890
14951
  lastUpdateAt = null;
14891
14952
  dispatcherRef?.setTurnRunning(false);
@@ -14900,6 +14961,11 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
14900
14961
  stalled: false
14901
14962
  });
14902
14963
  }
14964
+ } else if (pendingTurns > 0 && cancelling) {
14965
+ cancelling = false;
14966
+ if (screenReady) {
14967
+ screenRef.setBanner({ status: "busy", stalled: false });
14968
+ }
14903
14969
  }
14904
14970
  void delta;
14905
14971
  };
@@ -15258,18 +15324,19 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
15258
15324
  historyPolicy: "full",
15259
15325
  clientInfo: { name: "hydra-acp-tui", version: HYDRA_VERSION },
15260
15326
  ...opts.readonly === true ? { readonly: true } : {},
15261
- // Forward the user-chosen cwd for first-launch imported sessions
15262
- // via a full resume hint. upstreamSessionId is empty so the
15263
- // daemon routes through doResurrectFromImport (session-manager.ts)
15264
- // with the user-supplied cwd instead of silently falling back to
15265
- // $HOME in resolveImportCwd.
15266
- ...ctx.importAttachHint !== void 0 ? {
15327
+ // Forward the user-chosen cwd via a full resume hint. An empty
15328
+ // upstreamSessionId routes through doResurrectFromImport
15329
+ // (first-launch imports); a real one takes the normal session/load
15330
+ // path (repairing a local session whose recorded cwd is gone).
15331
+ // Either way the daemon resurrects with this cwd instead of the
15332
+ // stale recorded one.
15333
+ ...ctx.resumeHint !== void 0 ? {
15267
15334
  _meta: {
15268
15335
  [HYDRA_META_KEY]: {
15269
15336
  resume: {
15270
- upstreamSessionId: "",
15271
- agentId: ctx.importAttachHint.agentId,
15272
- cwd: ctx.importAttachHint.cwd
15337
+ upstreamSessionId: ctx.resumeHint.upstreamSessionId,
15338
+ agentId: ctx.resumeHint.agentId,
15339
+ cwd: ctx.resumeHint.cwd
15273
15340
  }
15274
15341
  }
15275
15342
  }
@@ -15356,9 +15423,6 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
15356
15423
  if (pendingPermission && tryHandlePermissionKey(ev)) {
15357
15424
  continue;
15358
15425
  }
15359
- if (exitConfirmation && tryHandleExitConfirmKey(ev)) {
15360
- continue;
15361
- }
15362
15426
  if (tryHandleHelpKey(ev)) {
15363
15427
  continue;
15364
15428
  }
@@ -15572,86 +15636,35 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
15572
15636
  const cancelRemoteTurn = () => {
15573
15637
  conn.notify("session/cancel", { sessionId: resolvedSessionId }).catch(() => void 0);
15574
15638
  };
15575
- const sigintHandler = () => {
15576
- if (turnInFlight) {
15577
- turnInFlight.cancel();
15639
+ const markCancelling = () => {
15640
+ if (screenRef === null) {
15578
15641
  return;
15579
15642
  }
15580
- if (pendingTurns > 0) {
15581
- cancelRemoteTurn();
15643
+ if (pendingTurns !== 1) {
15582
15644
  return;
15583
15645
  }
15584
- void requestExit();
15646
+ cancelling = true;
15647
+ screenRef.setBanner({
15648
+ status: "cancelling",
15649
+ elapsedMs: void 0,
15650
+ stalled: false
15651
+ });
15585
15652
  };
15586
- let exitConfirmation = null;
15587
- const requestExit = async () => {
15588
- if (exitConfirmation) {
15589
- stop(0);
15590
- return;
15591
- }
15592
- if (pendingTurns === 0) {
15593
- stop(0);
15594
- return;
15595
- }
15596
- let onlyClient = false;
15597
- try {
15598
- const sessions = await listSessions(target);
15599
- const me = sessions.find((s) => s.sessionId === resolvedSessionId);
15600
- onlyClient = !me || me.attachedClients <= 1;
15601
- } catch {
15602
- stop(0);
15653
+ const sigintHandler = () => {
15654
+ if (turnInFlight) {
15655
+ turnInFlight.cancel();
15656
+ markCancelling();
15603
15657
  return;
15604
15658
  }
15605
- if (!onlyClient) {
15606
- stop(0);
15659
+ if (pendingTurns > 0) {
15660
+ cancelRemoteTurn();
15661
+ markCancelling();
15607
15662
  return;
15608
15663
  }
15609
- exitConfirmation = { offered: true };
15610
- screen.setConfirmPrompt({
15611
- question: "Agent is still working. Interrupt it before exit?",
15612
- hint: "y interrupt then exit \xB7 n / Enter detach silently \xB7 Esc cancel"
15613
- });
15614
- };
15615
- const dismissExitConfirmation = () => {
15616
- exitConfirmation = null;
15617
- screen.setConfirmPrompt(null);
15664
+ requestExit();
15618
15665
  };
15619
- const tryHandleExitConfirmKey = (ev) => {
15620
- if (!exitConfirmation) {
15621
- return false;
15622
- }
15623
- if (ev.type === "char") {
15624
- const ch = ev.ch.toLowerCase();
15625
- if (ch === "y") {
15626
- dismissExitConfirmation();
15627
- conn.notify("session/cancel", { sessionId: resolvedSessionId }).catch(() => void 0);
15628
- stop(0);
15629
- return true;
15630
- }
15631
- if (ch === "n") {
15632
- dismissExitConfirmation();
15633
- stop(0);
15634
- return true;
15635
- }
15636
- return true;
15637
- }
15638
- if (ev.type === "key") {
15639
- if (ev.name === "enter") {
15640
- dismissExitConfirmation();
15641
- stop(0);
15642
- return true;
15643
- }
15644
- if (ev.name === "escape") {
15645
- dismissExitConfirmation();
15646
- return true;
15647
- }
15648
- if (ev.name === "ctrl-c" || ev.name === "ctrl-d") {
15649
- dismissExitConfirmation();
15650
- stop(0);
15651
- return true;
15652
- }
15653
- }
15654
- return true;
15666
+ const requestExit = () => {
15667
+ stop(0);
15655
15668
  };
15656
15669
  const buildHelpEntries = () => {
15657
15670
  const enqueueDesc = "enqueue prompt (sends now, or queues during a turn)";
@@ -15723,7 +15736,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
15723
15736
  let resolvedChoice = null;
15724
15737
  let attachOverrides = null;
15725
15738
  while (resolvedChoice === null) {
15726
- const sessions = await listSessions(target);
15739
+ const sessions = await listSessions(target, { includeNonInteractive: true });
15727
15740
  const choice2 = await pickSession(term, {
15728
15741
  cwd: resolvedCwd,
15729
15742
  sessions,
@@ -15761,14 +15774,43 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
15761
15774
  readonly: false,
15762
15775
  cwd: decided2.ctx.cwd
15763
15776
  };
15764
- if (decided2.ctx.importAttachHint !== void 0) {
15765
- attachOverrides.importAttachHint = decided2.ctx.importAttachHint;
15777
+ if (decided2.ctx.resumeHint !== void 0) {
15778
+ attachOverrides.resumeHint = decided2.ctx.resumeHint;
15766
15779
  }
15767
15780
  break;
15768
15781
  }
15769
15782
  const chosen = sessions.find((s) => s.sessionId === choice2.sessionId);
15770
15783
  const isImportedFirstLaunch = chosen !== void 0 && !!chosen.importedFromMachine && !chosen.upstreamSessionId && choice2.readonly !== true;
15771
15784
  if (!isImportedFirstLaunch) {
15785
+ if (target.isLocal && chosen && !chosen.importedFromMachine && choice2.readonly !== true) {
15786
+ const v = await validateLocalCwd(chosen.cwd);
15787
+ if (!v.ok) {
15788
+ const r = await promptForImportCwd(term, chosen, {
15789
+ defaultCwd: expandHome(config.defaultCwd),
15790
+ title: "Working directory missing \u2014 choose cwd",
15791
+ intro: "This session's working directory no longer exists. Pick a new one:"
15792
+ });
15793
+ if (r.kind === "cancel") {
15794
+ screen.start({ skipFullscreen: true });
15795
+ screen.resumeRepaint();
15796
+ return;
15797
+ }
15798
+ if (r.kind === "back") {
15799
+ continue;
15800
+ }
15801
+ resolvedChoice = { choice: choice2, sessions };
15802
+ attachOverrides = {
15803
+ readonly: false,
15804
+ cwd: r.path,
15805
+ resumeHint: {
15806
+ agentId: choice2.agentId ?? chosen.agentId ?? "",
15807
+ cwd: r.path,
15808
+ upstreamSessionId: ""
15809
+ }
15810
+ };
15811
+ break;
15812
+ }
15813
+ }
15772
15814
  resolvedChoice = { choice: choice2, sessions };
15773
15815
  break;
15774
15816
  }
@@ -15787,8 +15829,8 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
15787
15829
  readonly: opsShim.readonly === true,
15788
15830
  cwd: decided.ctx.cwd
15789
15831
  };
15790
- if (decided.ctx.importAttachHint !== void 0) {
15791
- attachOverrides.importAttachHint = decided.ctx.importAttachHint;
15832
+ if (decided.ctx.resumeHint !== void 0) {
15833
+ attachOverrides.resumeHint = decided.ctx.resumeHint;
15792
15834
  }
15793
15835
  }
15794
15836
  const { choice } = resolvedChoice;
@@ -15823,10 +15865,10 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
15823
15865
  if (choice.agentId !== void 0) {
15824
15866
  nextOpts.agentId = choice.agentId;
15825
15867
  }
15826
- if (attachOverrides?.importAttachHint !== void 0) {
15827
- nextOpts.importAttachHint = attachOverrides.importAttachHint;
15868
+ if (attachOverrides?.resumeHint !== void 0) {
15869
+ nextOpts.resumeHint = attachOverrides.resumeHint;
15828
15870
  } else {
15829
- delete nextOpts.importAttachHint;
15871
+ delete nextOpts.resumeHint;
15830
15872
  }
15831
15873
  resume(nextOpts);
15832
15874
  };
@@ -15929,10 +15971,11 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
15929
15971
  } else if (pendingTurns > 0) {
15930
15972
  cancelRemoteTurn();
15931
15973
  }
15974
+ markCancelling();
15932
15975
  return;
15933
15976
  }
15934
15977
  case "exit":
15935
- void requestExit();
15978
+ requestExit();
15936
15979
  return;
15937
15980
  case "plan-toggle":
15938
15981
  void handleModeToggle(effect.on);
@@ -16020,7 +16063,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16020
16063
  continue;
16021
16064
  }
16022
16065
  try {
16023
- const buf = await fs23.readFile(token);
16066
+ const buf = await fs24.readFile(token);
16024
16067
  if (buf.length > MAX_ATTACHMENT_BYTES) {
16025
16068
  screen.notify(
16026
16069
  `image too large (${formatSize(buf.length)}, max ${formatSize(MAX_ATTACHMENT_BYTES)})`
@@ -16230,7 +16273,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16230
16273
  switch (cmd) {
16231
16274
  case "/quit":
16232
16275
  case "/exit":
16233
- void requestExit();
16276
+ requestExit();
16234
16277
  return true;
16235
16278
  case "/clear":
16236
16279
  toolStates.clear();
@@ -16241,6 +16284,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16241
16284
  toolsBlockStopReason = null;
16242
16285
  toolsExpanded = false;
16243
16286
  lastEditMarkPath = null;
16287
+ turnHasShownProse = false;
16244
16288
  screen.clearScrollback();
16245
16289
  return true;
16246
16290
  case "/demo-plan": {
@@ -16586,6 +16630,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16586
16630
  }
16587
16631
  };
16588
16632
  let lastEditMarkPath = null;
16633
+ let turnHasShownProse = false;
16589
16634
  const maybeRenderEditDiff = (toolCallId) => {
16590
16635
  const mode = config.tui.showFileUpdates;
16591
16636
  if (mode === "none") {
@@ -16602,6 +16647,9 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16602
16647
  }
16603
16648
  return;
16604
16649
  }
16650
+ if (!turnHasShownProse) {
16651
+ return;
16652
+ }
16605
16653
  const diff = state.editDiff;
16606
16654
  if (diff.path && diff.path === lastEditMarkPath) {
16607
16655
  return;
@@ -16687,6 +16735,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16687
16735
  toolsExpanded = false;
16688
16736
  toolsBlockEndedAt = null;
16689
16737
  lastEditMarkPath = null;
16738
+ turnHasShownProse = false;
16690
16739
  startToolsBlock();
16691
16740
  screen.redraw();
16692
16741
  return;
@@ -16694,6 +16743,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16694
16743
  if (event.kind === "agent-text") {
16695
16744
  closeThought();
16696
16745
  if (event.text.length > 0) {
16746
+ turnHasShownProse = true;
16697
16747
  lastEditMarkPath = null;
16698
16748
  }
16699
16749
  appendAgentText(event.text);
@@ -16702,6 +16752,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16702
16752
  if (event.kind === "agent-thought") {
16703
16753
  closeAgentText();
16704
16754
  if (viewPrefs.showThoughts && event.text.length > 0) {
16755
+ turnHasShownProse = true;
16705
16756
  lastEditMarkPath = null;
16706
16757
  }
16707
16758
  appendThought(event.text);
@@ -16825,13 +16876,15 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16825
16876
  toolsExpanded = false;
16826
16877
  upstreamInterruptedSeen = false;
16827
16878
  lastEditMarkPath = null;
16879
+ turnHasShownProse = false;
16828
16880
  screen.ensureSeparator();
16829
16881
  if (shouldDriftSnap({
16830
16882
  pendingTurns,
16831
16883
  queueSize: queueCache.size,
16832
16884
  ownTurnInFlight: turnInFlight !== null,
16833
16885
  hasInFlightHead: currentHeadMessageId !== void 0,
16834
- replayDraining
16886
+ replayDraining,
16887
+ amended: event.amended === true
16835
16888
  })) {
16836
16889
  adjustPendingTurns(-pendingTurns);
16837
16890
  }
@@ -16910,6 +16963,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
16910
16963
  toolsBlockStopReason = null;
16911
16964
  toolsExpanded = false;
16912
16965
  lastEditMarkPath = null;
16966
+ turnHasShownProse = false;
16913
16967
  };
16914
16968
  onDisconnectHook = () => {
16915
16969
  screen.setBanner({ status: "disconnected", elapsedMs: void 0 });
@@ -17059,8 +17113,8 @@ async function resolveSession(term, config, target, opts, pickerPrefs) {
17059
17113
  agentId: opts.agentId ?? "",
17060
17114
  cwd
17061
17115
  };
17062
- if (opts.importAttachHint !== void 0) {
17063
- ctx.importAttachHint = opts.importAttachHint;
17116
+ if (opts.resumeHint !== void 0) {
17117
+ ctx.resumeHint = opts.resumeHint;
17064
17118
  }
17065
17119
  return ctx;
17066
17120
  }
@@ -17082,7 +17136,7 @@ async function resolveSession(term, config, target, opts, pickerPrefs) {
17082
17136
  };
17083
17137
  }
17084
17138
  while (true) {
17085
- const sessions = await listSessions(target);
17139
+ const sessions = await listSessions(target, { includeNonInteractive: true });
17086
17140
  const choice = await pickSession(term, {
17087
17141
  cwd,
17088
17142
  sessions,
@@ -17122,6 +17176,33 @@ async function resolveSession(term, config, target, opts, pickerPrefs) {
17122
17176
  }
17123
17177
  return decided.ctx;
17124
17178
  }
17179
+ if (target.isLocal && chosen && !chosen.importedFromMachine && !opts.readonly) {
17180
+ const v = await validateLocalCwd(chosen.cwd);
17181
+ if (!v.ok) {
17182
+ const r = await promptForImportCwd(term, chosen, {
17183
+ defaultCwd: expandHome(config.defaultCwd),
17184
+ title: "Working directory missing \u2014 choose cwd",
17185
+ intro: "This session's working directory no longer exists. Pick a new one:"
17186
+ });
17187
+ if (r.kind === "cancel") {
17188
+ return null;
17189
+ }
17190
+ if (r.kind === "back") {
17191
+ continue;
17192
+ }
17193
+ const agentId = choice.agentId ?? chosen.agentId ?? "";
17194
+ return {
17195
+ sessionId: choice.sessionId,
17196
+ agentId,
17197
+ cwd: r.path,
17198
+ resumeHint: {
17199
+ agentId,
17200
+ cwd: r.path,
17201
+ upstreamSessionId: ""
17202
+ }
17203
+ };
17204
+ }
17205
+ }
17125
17206
  return {
17126
17207
  sessionId: choice.sessionId,
17127
17208
  agentId: choice.agentId ?? "",
@@ -17164,7 +17245,9 @@ async function runImportedFirstLaunchFlow(term, chosen, choice, opts) {
17164
17245
  sessionId: choice.sessionId,
17165
17246
  agentId,
17166
17247
  cwd: cwdResult.path,
17167
- importAttachHint: { agentId, cwd: cwdResult.path }
17248
+ // Empty upstreamSessionId import-reseed path for a never-launched
17249
+ // imported session.
17250
+ resumeHint: { agentId, cwd: cwdResult.path, upstreamSessionId: "" }
17168
17251
  }
17169
17252
  };
17170
17253
  }
@@ -17208,9 +17291,15 @@ fork failed: ${err.message}
17208
17291
  // For foreign-never-launched forks, the daemon stamped the chosen
17209
17292
  // cwd onto meta.json via the POST body, but the very first attach
17210
17293
  // still goes through the import-reseed path (upstreamSessionId=""),
17211
- // and importAttachHint is what makes attachManagerHooks persist
17294
+ // and the resume hint is what makes attachManagerHooks persist
17212
17295
  // the local cwd over the bundle's recorded one.
17213
- ...isForeignNeverLaunched ? { importAttachHint: { agentId: choice.sourceAgentId ?? "", cwd } } : {}
17296
+ ...isForeignNeverLaunched ? {
17297
+ resumeHint: {
17298
+ agentId: choice.sourceAgentId ?? "",
17299
+ cwd,
17300
+ upstreamSessionId: ""
17301
+ }
17302
+ } : {}
17214
17303
  }
17215
17304
  };
17216
17305
  }
@@ -17336,11 +17425,11 @@ function createInstallStatusLine(term, baseLabel) {
17336
17425
  }
17337
17426
  function rotateIfBig(target) {
17338
17427
  try {
17339
- const stat5 = statSync(target);
17428
+ const stat5 = statSync2(target);
17340
17429
  if (stat5.size < logMaxBytes) {
17341
17430
  return;
17342
17431
  }
17343
- renameSync(target, `${target}.0`);
17432
+ renameSync2(target, `${target}.0`);
17344
17433
  } catch {
17345
17434
  }
17346
17435
  }
@@ -17352,6 +17441,7 @@ var init_app = __esm({
17352
17441
  init_types();
17353
17442
  init_resilient_ws();
17354
17443
  init_config();
17444
+ init_cwd();
17355
17445
  init_remote_target();
17356
17446
  init_daemon_bootstrap();
17357
17447
  init_bin_name();
@@ -18849,10 +18939,17 @@ var SessionRecord = z5.object({
18849
18939
  // ended at. Kept so future UI can show "branched from turn N of session X".
18850
18940
  forkedFromSessionId: z5.string().optional(),
18851
18941
  forkedFromMessageId: z5.string().optional(),
18852
- // clientInfo from the process that issued session/new. Picker and
18853
- // `sessions list` use this to hide cat-style ancillary sessions by
18854
- // default; carried in meta.json so cold sessions filter the same way.
18942
+ // clientInfo from the process that issued session/new. Display only
18943
+ // since the `interactive` flag below; kept on the record for log
18944
+ // attribution and as the legacy hint inside effectiveInteractive
18945
+ // (pre-flag cat sessions can be recognised from this field).
18855
18946
  originatingClient: PersistedOriginatingClient.optional(),
18947
+ // Tristate: true once the session has had a real turn, false when
18948
+ // explicitly created as ancillary (e.g. `hydra cat`), undefined for
18949
+ // pre-flag records / freshly-created sessions that haven't decided
18950
+ // yet. effectiveInteractive() in session-manager.ts is the single
18951
+ // resolver — every filter site goes through it.
18952
+ interactive: z5.boolean().optional(),
18856
18953
  createdAt: z5.string(),
18857
18954
  updatedAt: z5.string()
18858
18955
  });
@@ -18971,6 +19068,7 @@ function recordFromMemorySession(args) {
18971
19068
  forkedFromSessionId: args.forkedFromSessionId,
18972
19069
  forkedFromMessageId: args.forkedFromMessageId,
18973
19070
  originatingClient: args.originatingClient,
19071
+ interactive: args.interactive,
18974
19072
  createdAt: args.createdAt ?? now,
18975
19073
  updatedAt: args.updatedAt ?? now
18976
19074
  };
@@ -19774,6 +19872,7 @@ var HistoryStore = class {
19774
19872
 
19775
19873
  // src/core/session-manager.ts
19776
19874
  init_paths();
19875
+ init_config();
19777
19876
  init_history();
19778
19877
 
19779
19878
  // src/core/bundle.ts
@@ -19809,6 +19908,14 @@ var BundleSession = z6.object({
19809
19908
  currentUsage: PersistedUsage.optional(),
19810
19909
  agentCommands: z6.array(PersistedAgentCommand).optional(),
19811
19910
  agentModes: z6.array(PersistedAgentMode).optional(),
19911
+ // Raw interactive tristate (NOT the resolved effectiveInteractive) so
19912
+ // the value stays promotable on the destination: a cat/empty source
19913
+ // arrives as undefined and a real turn there can still flip it to
19914
+ // true. Carried alongside originatingClient so the importer's
19915
+ // effectiveInteractive can re-apply the cat-name hint at read time
19916
+ // without freezing a sticky `false` into the record.
19917
+ interactive: z6.boolean().optional(),
19918
+ originatingClient: PersistedOriginatingClient.optional(),
19812
19919
  createdAt: z6.string(),
19813
19920
  updatedAt: z6.string()
19814
19921
  });
@@ -19852,6 +19959,8 @@ function encodeBundle(params) {
19852
19959
  ...params.record.currentUsage !== void 0 ? { currentUsage: params.record.currentUsage } : {},
19853
19960
  ...params.record.agentCommands !== void 0 ? { agentCommands: params.record.agentCommands } : {},
19854
19961
  ...params.record.agentModes !== void 0 ? { agentModes: params.record.agentModes } : {},
19962
+ ...params.record.interactive !== void 0 ? { interactive: params.record.interactive } : {},
19963
+ ...params.record.originatingClient !== void 0 ? { originatingClient: params.record.originatingClient } : {},
19855
19964
  createdAt: params.record.createdAt,
19856
19965
  updatedAt: params.record.updatedAt
19857
19966
  },
@@ -19890,6 +19999,7 @@ var SessionManager = class {
19890
19999
  this.logger = options.logger;
19891
20000
  this.npmRegistry = options.npmRegistry;
19892
20001
  this.extensionCommands = options.extensionCommands;
20002
+ this.defaultCwd = options.defaultCwd ?? "~";
19893
20003
  this.synopsisCoordinator = new SynopsisCoordinator({
19894
20004
  registry: this.registry,
19895
20005
  store: this.store,
@@ -19923,6 +20033,7 @@ var SessionManager = class {
19923
20033
  logger;
19924
20034
  npmRegistry;
19925
20035
  extensionCommands;
20036
+ defaultCwd;
19926
20037
  // Background queue for ephemeral-agent synopsis generation. Runs
19927
20038
  // out-of-band so session close is instant; persists synopsis/title
19928
20039
  // via the same enqueueMetaWrite path the in-session handlers used.
@@ -19982,6 +20093,7 @@ var SessionManager = class {
19982
20093
  transformChain: params.transformChain,
19983
20094
  parentSessionId: params.parentSessionId,
19984
20095
  originatingClient: params.originatingClient,
20096
+ interactive: params.interactive,
19985
20097
  extensionCommands: this.extensionCommands,
19986
20098
  scheduleSynopsis: () => this.synopsisCoordinator.schedule(session.sessionId)
19987
20099
  });
@@ -20028,6 +20140,9 @@ var SessionManager = class {
20028
20140
  if (params.upstreamSessionId === "") {
20029
20141
  return this.doResurrectFromImport(params);
20030
20142
  }
20143
+ if (!await this.dirExists(params.cwd)) {
20144
+ return this.doResurrectFromImport(params);
20145
+ }
20031
20146
  const plan = await planSpawn(agentDef, params.agentArgs ?? [], {
20032
20147
  npmRegistry: this.npmRegistry,
20033
20148
  onInstallProgress: params.onInstallProgress
@@ -20155,6 +20270,7 @@ var SessionManager = class {
20155
20270
  firstPromptSeeded: !!params.title,
20156
20271
  createdAt: params.createdAt ? new Date(params.createdAt).getTime() : void 0,
20157
20272
  originatingClient: params.originatingClient,
20273
+ interactive: params.interactive,
20158
20274
  forkedFromSessionId: params.forkedFromSessionId,
20159
20275
  forkedFromMessageId: params.forkedFromMessageId,
20160
20276
  extensionCommands: this.extensionCommands,
@@ -20171,7 +20287,7 @@ var SessionManager = class {
20171
20287
  // so subsequent resurrects of this session use the normal session/load
20172
20288
  // path.
20173
20289
  async doResurrectFromImport(params) {
20174
- const cwd = await this.resolveImportCwd(params.cwd);
20290
+ const cwd = await this.resolveResurrectCwd(params.cwd);
20175
20291
  const fresh = await this.bootstrapAgent({
20176
20292
  agentId: params.agentId,
20177
20293
  cwd,
@@ -20226,6 +20342,7 @@ var SessionManager = class {
20226
20342
  firstPromptSeeded: !!params.title,
20227
20343
  createdAt: params.createdAt ? new Date(params.createdAt).getTime() : void 0,
20228
20344
  originatingClient: params.originatingClient,
20345
+ interactive: params.interactive,
20229
20346
  forkedFromSessionId: params.forkedFromSessionId,
20230
20347
  forkedFromMessageId: params.forkedFromMessageId,
20231
20348
  extensionCommands: this.extensionCommands,
@@ -20235,15 +20352,50 @@ var SessionManager = class {
20235
20352
  void session.seedFromImport().catch(() => void 0);
20236
20353
  return session;
20237
20354
  }
20238
- async resolveImportCwd(cwd) {
20355
+ async dirExists(cwd) {
20239
20356
  try {
20240
- const stat5 = await fs14.stat(cwd);
20241
- if (stat5.isDirectory()) {
20242
- return cwd;
20243
- }
20357
+ return (await fs14.stat(cwd)).isDirectory();
20244
20358
  } catch {
20359
+ return false;
20245
20360
  }
20246
- return os3.homedir();
20361
+ }
20362
+ // When the last client detaches from a session that resolves to
20363
+ // non-interactive — e.g. a `hydra cat` run, born interactive:undefined
20364
+ // with originatingClient hydra-acp-cat, whose every prompt is ancillary
20365
+ // — close it so its agent process doesn't linger until the (default 1h)
20366
+ // idle timeout fires. The cold record is kept, so the rare refine-in-TUI
20367
+ // still works via the resurrect/reseed path. Sessions promoted to
20368
+ // interactive (driven by a real, non-ancillary prompt) resolve to true
20369
+ // and are left running.
20370
+ async reapIfOrphanedNonInteractive(sessionId) {
20371
+ const session = this.sessions.get(sessionId);
20372
+ if (!session || session.attachedCount > 0) {
20373
+ return;
20374
+ }
20375
+ const interactive = effectiveInteractive(
20376
+ {
20377
+ interactive: session.interactive,
20378
+ ...session.originatingClient ? { originatingClient: session.originatingClient } : {}
20379
+ },
20380
+ true
20381
+ );
20382
+ if (interactive !== false) {
20383
+ return;
20384
+ }
20385
+ this.logger?.info(
20386
+ `reaping orphaned non-interactive session ${sessionId} (agent killed, cold record kept)`
20387
+ );
20388
+ await session.close({ deleteRecord: false }).catch(() => void 0);
20389
+ }
20390
+ // Resolve a recorded cwd for resurrect: use it if it still exists,
20391
+ // otherwise fall back to the configured defaultCwd. Covers both bundles
20392
+ // imported from another machine and local sessions (e.g. `cat`) whose
20393
+ // recorded dir was cleaned up, so the reseed spawn never ENOENTs.
20394
+ async resolveResurrectCwd(cwd) {
20395
+ if (await this.dirExists(cwd)) {
20396
+ return cwd;
20397
+ }
20398
+ return expandHome(this.defaultCwd);
20247
20399
  }
20248
20400
  // Pull every session the agent itself remembers (across all cwds) and
20249
20401
  // persist a cold hydra record for each one we don't already track.
@@ -20332,6 +20484,10 @@ var SessionManager = class {
20332
20484
  agentId,
20333
20485
  cwd: entry.cwd,
20334
20486
  pendingHistorySync: true,
20487
+ // `hydra agent sync` is a user-explicit "show me agent-side
20488
+ // sessions" action; the rows are meant to be visible immediately
20489
+ // even before the first resurrect populates history.jsonl.
20490
+ interactive: true,
20335
20491
  createdAt: ts,
20336
20492
  updatedAt: ts
20337
20493
  };
@@ -20498,6 +20654,11 @@ var SessionManager = class {
20498
20654
  () => void 0
20499
20655
  );
20500
20656
  });
20657
+ session.onInteractiveChange((interactive) => {
20658
+ void this.persistSnapshot(session.sessionId, { interactive }).catch(
20659
+ () => void 0
20660
+ );
20661
+ });
20501
20662
  session.onUsageChange((usage) => {
20502
20663
  void this.persistSnapshot(session.sessionId, {
20503
20664
  currentUsage: usageSnapshotToPersisted(usage)
@@ -20591,6 +20752,7 @@ var SessionManager = class {
20591
20752
  createdAt: record.createdAt,
20592
20753
  pendingHistorySync: record.pendingHistorySync,
20593
20754
  originatingClient: record.originatingClient,
20755
+ interactive: record.interactive,
20594
20756
  forkedFromSessionId: record.forkedFromSessionId,
20595
20757
  forkedFromMessageId: record.forkedFromMessageId
20596
20758
  };
@@ -20678,12 +20840,27 @@ var SessionManager = class {
20678
20840
  async list(filter = {}) {
20679
20841
  const entries = [];
20680
20842
  const liveIds = /* @__PURE__ */ new Set();
20843
+ const includeRow = (interactive) => {
20844
+ if (filter.includeNonInteractive) return true;
20845
+ return interactive === true;
20846
+ };
20681
20847
  for (const session of this.sessions.values()) {
20682
20848
  if (filter.cwd && session.cwd !== filter.cwd) {
20683
20849
  continue;
20684
20850
  }
20685
20851
  liveIds.add(session.sessionId);
20686
- const used = await historyMtimeIso(session.sessionId) ?? new Date(session.updatedAt).toISOString();
20852
+ const hist = await historyStatus(session.sessionId);
20853
+ const interactive = effectiveInteractive(
20854
+ {
20855
+ interactive: session.interactive,
20856
+ ...session.originatingClient ? { originatingClient: session.originatingClient } : {}
20857
+ },
20858
+ hist.hasContent
20859
+ );
20860
+ if (!includeRow(interactive)) {
20861
+ continue;
20862
+ }
20863
+ const used = hist.mtime ?? new Date(session.updatedAt).toISOString();
20687
20864
  entries.push({
20688
20865
  sessionId: session.sessionId,
20689
20866
  upstreamSessionId: session.upstreamSessionId,
@@ -20696,6 +20873,7 @@ var SessionManager = class {
20696
20873
  forkedFromSessionId: session.forkedFromSessionId,
20697
20874
  forkedFromMessageId: session.forkedFromMessageId,
20698
20875
  originatingClient: session.originatingClient,
20876
+ interactive,
20699
20877
  updatedAt: used,
20700
20878
  attachedClients: session.attachedCount,
20701
20879
  status: "live",
@@ -20710,7 +20888,12 @@ var SessionManager = class {
20710
20888
  if (filter.cwd && r.cwd !== filter.cwd) {
20711
20889
  continue;
20712
20890
  }
20713
- const used = await historyMtimeIso(r.sessionId) ?? r.updatedAt;
20891
+ const hist = await historyStatus(r.sessionId);
20892
+ const interactive = effectiveInteractive(r, hist.hasContent);
20893
+ if (!includeRow(interactive)) {
20894
+ continue;
20895
+ }
20896
+ const used = hist.mtime ?? r.updatedAt;
20714
20897
  entries.push({
20715
20898
  sessionId: r.sessionId,
20716
20899
  upstreamSessionId: r.upstreamSessionId,
@@ -20728,6 +20911,7 @@ var SessionManager = class {
20728
20911
  forkedFromSessionId: r.forkedFromSessionId,
20729
20912
  forkedFromMessageId: r.forkedFromMessageId,
20730
20913
  originatingClient: r.originatingClient,
20914
+ interactive,
20731
20915
  updatedAt: used,
20732
20916
  attachedClients: 0,
20733
20917
  status: "cold",
@@ -20962,8 +21146,18 @@ var SessionManager = class {
20962
21146
  currentUsage: args.bundle.session.currentUsage,
20963
21147
  agentCommands: args.bundle.session.agentCommands,
20964
21148
  agentModes: args.bundle.session.agentModes,
21149
+ // Carry the source's raw interactive tristate and originating
21150
+ // client rather than forcing true. A real conversation arrives
21151
+ // as true (visible immediately); an empty source arrives as
21152
+ // undefined (hidden until a turn lands here); a cat source
21153
+ // arrives as undefined + cat originatingClient, so
21154
+ // effectiveInteractive hides it via the hint while leaving it
21155
+ // promotable. Legacy bundles (pre-flag) carry neither and fall
21156
+ // back to effectiveInteractive's history-presence inference.
21157
+ interactive: args.bundle.session.interactive,
21158
+ originatingClient: args.bundle.session.originatingClient,
20965
21159
  createdAt: args.preservedCreatedAt ?? now,
20966
- // Fallback path for historyMtimeIso (used when the history file
21160
+ // Fallback path for historyStatus (used when the history file
20967
21161
  // is missing). Keep this consistent with the utimes stamp above.
20968
21162
  updatedAt: args.bundle.session.updatedAt
20969
21163
  });
@@ -21072,6 +21266,8 @@ var SessionManager = class {
21072
21266
  ...update.agentCommands !== void 0 ? { agentCommands: update.agentCommands } : {},
21073
21267
  ...update.agentModes !== void 0 ? { agentModes: update.agentModes } : {},
21074
21268
  ...update.agentModels !== void 0 ? { agentModels: update.agentModels } : {},
21269
+ ...update.interactive !== void 0 ? { interactive: update.interactive } : {},
21270
+ ...update.cwd !== void 0 ? { cwd: update.cwd } : {},
21075
21271
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
21076
21272
  });
21077
21273
  });
@@ -21248,6 +21444,7 @@ function mergeForPersistence(session, existing) {
21248
21444
  forkedFromSessionId: session.forkedFromSessionId ?? existing?.forkedFromSessionId,
21249
21445
  forkedFromMessageId: session.forkedFromMessageId ?? existing?.forkedFromMessageId,
21250
21446
  originatingClient: session.originatingClient ?? existing?.originatingClient,
21447
+ interactive: session.interactive ?? existing?.interactive,
21251
21448
  createdAt: existing?.createdAt ?? new Date(session.createdAt).toISOString()
21252
21449
  });
21253
21450
  }
@@ -21554,13 +21751,25 @@ async function loadPromptHistorySafely(sessionId) {
21554
21751
  return [];
21555
21752
  }
21556
21753
  }
21557
- async function historyMtimeIso(sessionId) {
21754
+ async function historyStatus(sessionId) {
21558
21755
  try {
21559
21756
  const st = await fs14.stat(paths.historyFile(sessionId));
21560
- return new Date(st.mtimeMs).toISOString();
21757
+ return {
21758
+ mtime: new Date(st.mtimeMs).toISOString(),
21759
+ hasContent: st.size > 0
21760
+ };
21561
21761
  } catch {
21562
- return void 0;
21762
+ return { hasContent: false };
21763
+ }
21764
+ }
21765
+ function effectiveInteractive(record, hasContent) {
21766
+ if (record.interactive !== void 0) {
21767
+ return record.interactive;
21563
21768
  }
21769
+ if (record.originatingClient?.name === HYDRA_CAT_CLIENT_NAME) {
21770
+ return false;
21771
+ }
21772
+ return hasContent ? true : void 0;
21564
21773
  }
21565
21774
 
21566
21775
  // src/core/child-supervisor.ts
@@ -23338,17 +23547,21 @@ function resolveHydraHost(defaults) {
23338
23547
  function registerSessionRoutes(app, manager, defaults) {
23339
23548
  app.get("/v1/sessions", async (request) => {
23340
23549
  const query = request.query;
23341
- const sessions = await manager.list({ cwd: query?.cwd });
23550
+ const includeNonInteractive = query?.includeNonInteractive === "1" || query?.includeNonInteractive === "true";
23551
+ const sessions = await manager.list({
23552
+ cwd: query?.cwd,
23553
+ includeNonInteractive
23554
+ });
23342
23555
  return { sessions };
23343
23556
  });
23344
- app.get("/v1/sessions/search", async (request, reply) => {
23345
- const query = request.query;
23346
- const q = query?.q ?? "";
23557
+ app.post("/v1/sessions/search", async (request, reply) => {
23558
+ const body = request.body ?? {};
23559
+ const q = typeof body.q === "string" ? body.q : "";
23347
23560
  if (q.trim().length === 0) {
23348
23561
  reply.code(400).send({ error: "q is required" });
23349
23562
  return reply;
23350
23563
  }
23351
- const ids = query?.sessionIds ? query.sessionIds.split(",").filter((s) => s.length > 0) : void 0;
23564
+ const ids = Array.isArray(body.sessionIds) ? body.sessionIds.filter((s) => typeof s === "string" && s.length > 0) : void 0;
23352
23565
  const out = await searchHistories(manager, q, { sessionIds: ids });
23353
23566
  return out;
23354
23567
  });
@@ -23943,13 +24156,8 @@ function parseRegisterBody2(body) {
23943
24156
  }
23944
24157
 
23945
24158
  // src/daemon/routes/config.ts
23946
- function registerConfigRoutes(app, defaults) {
23947
- app.get("/v1/config", async () => {
23948
- return {
23949
- defaultAgent: defaults.defaultAgent,
23950
- defaultCwd: defaults.defaultCwd
23951
- };
23952
- });
24159
+ function registerConfigRoutes(app, snapshot) {
24160
+ app.get("/v1/config", async () => snapshot);
23953
24161
  }
23954
24162
 
23955
24163
  // src/daemon/routes/auth.ts
@@ -24469,7 +24677,8 @@ function registerAcpWsEndpoint(app, deps) {
24469
24677
  model: hydraMeta.model,
24470
24678
  onInstallProgress: makeInstallProgressForwarder(connection),
24471
24679
  transformChain,
24472
- originatingClient: state.clientInfo
24680
+ originatingClient: state.clientInfo,
24681
+ ...hydraMeta.interactive !== void 0 ? { interactive: hydraMeta.interactive } : {}
24473
24682
  });
24474
24683
  } catch (err) {
24475
24684
  if (stdinReservation !== void 0) {
@@ -24596,8 +24805,9 @@ function registerAcpWsEndpoint(app, deps) {
24596
24805
  err.code = JsonRpcErrorCodes.SessionNotFound;
24597
24806
  throw err;
24598
24807
  }
24808
+ const resurrectWithOriginator = resurrectParams.originatingClient ? resurrectParams : { ...resurrectParams, originatingClient: state.clientInfo };
24599
24809
  session = await deps.manager.resurrect({
24600
- ...resurrectParams,
24810
+ ...resurrectWithOriginator,
24601
24811
  onInstallProgress: makeInstallProgressForwarder(connection)
24602
24812
  });
24603
24813
  wireDefaultTransformers(session, deps);
@@ -24654,6 +24864,9 @@ function registerAcpWsEndpoint(app, deps) {
24654
24864
  const session = deps.manager.get(params.sessionId);
24655
24865
  session?.detach(att.clientId);
24656
24866
  state.attached.delete(params.sessionId);
24867
+ if (session) {
24868
+ void deps.manager.reapIfOrphanedNonInteractive(params.sessionId);
24869
+ }
24657
24870
  return { sessionId: params.sessionId, status: "detached" };
24658
24871
  });
24659
24872
  connection.onRequest("session/list", async (raw) => {
@@ -25901,7 +26114,8 @@ async function startDaemon(config, serviceToken) {
25901
26114
  sessionHistoryMaxEntries: config.daemon.sessionHistoryMaxEntries,
25902
26115
  logger: agentLogger,
25903
26116
  npmRegistry: config.npmRegistry,
25904
- extensionCommands
26117
+ extensionCommands,
26118
+ defaultCwd: config.defaultCwd
25905
26119
  });
25906
26120
  const extensions = new ExtensionManager(extensionList(config), void 0, {
25907
26121
  tokenRegistry: processRegistry
@@ -25922,7 +26136,12 @@ async function startDaemon(config, serviceToken) {
25922
26136
  registerTransformerRoutes(app, transformers);
25923
26137
  registerConfigRoutes(app, {
25924
26138
  defaultAgent: config.defaultAgent,
25925
- defaultCwd: config.defaultCwd
26139
+ defaultCwd: config.defaultCwd,
26140
+ defaultModels: { ...config.defaultModels },
26141
+ ...config.synopsisAgent !== void 0 ? { synopsisAgent: config.synopsisAgent } : {},
26142
+ ...config.synopsisModel !== void 0 ? { synopsisModel: config.synopsisModel } : {},
26143
+ synopsisOnClose: config.synopsisOnClose,
26144
+ defaultTransformers: [...config.defaultTransformers]
25926
26145
  });
25927
26146
  registerAuthRoutes(app, {
25928
26147
  store: sessionTokenStore,
@@ -26410,7 +26629,6 @@ init_remote_target();
26410
26629
  init_remote_url();
26411
26630
  init_session();
26412
26631
  init_discovery();
26413
- init_hydra_version();
26414
26632
  import * as fs19 from "fs/promises";
26415
26633
  import * as path16 from "path";
26416
26634
  init_session_row();
@@ -26419,6 +26637,9 @@ async function runSessionsList(opts = {}) {
26419
26637
  const serviceToken = await loadServiceToken();
26420
26638
  const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
26421
26639
  const url = new URL(`${baseUrl}/v1/sessions`);
26640
+ if (opts.includeNonInteractive || opts.all) {
26641
+ url.searchParams.set("includeNonInteractive", "true");
26642
+ }
26422
26643
  const response = await fetch(url.toString(), {
26423
26644
  headers: { Authorization: `Bearer ${serviceToken}` }
26424
26645
  });
@@ -26428,13 +26649,11 @@ async function runSessionsList(opts = {}) {
26428
26649
  process.exit(1);
26429
26650
  }
26430
26651
  const body = await response.json();
26431
- const sessionsAfterCatFilter = opts.includeCat ? body.sessions : body.sessions.filter(
26432
- (s) => s.originatingClient?.name !== HYDRA_CAT_CLIENT_NAME
26433
- );
26652
+ const sessionsAfterInteractiveFilter = body.sessions;
26434
26653
  const host = opts.host ?? "local";
26435
- const hostFiltered = host === "all" ? sessionsAfterCatFilter : host === "local" ? sessionsAfterCatFilter.filter(
26654
+ const hostFiltered = host === "all" ? sessionsAfterInteractiveFilter : host === "local" ? sessionsAfterInteractiveFilter.filter(
26436
26655
  (s) => !s.importedFromMachine || !!s.upstreamSessionId
26437
- ) : sessionsAfterCatFilter.filter(
26656
+ ) : sessionsAfterInteractiveFilter.filter(
26438
26657
  (s) => s.importedFromMachine === host && !s.upstreamSessionId
26439
26658
  );
26440
26659
  if (opts.json) {
@@ -28631,15 +28850,17 @@ async function runAgentsLogs(agentId, rest) {
28631
28850
  const logPath = paths.agentLogFile(agentId);
28632
28851
  await runLogTail(logPath, rest, "No log file (agent never ran?)");
28633
28852
  }
28634
- async function runAgentsSetDefault(agentId) {
28635
- if (!agentId) {
28636
- process.stderr.write("Usage: hydra-acp agent set <agent-id>\n");
28637
- process.exit(2);
28638
- return;
28639
- }
28853
+ async function runAgentsSet(agentId, modelId) {
28640
28854
  const config = await loadConfig();
28641
28855
  const serviceToken = await loadServiceToken();
28642
28856
  const baseUrl = httpBase(config.daemon.host, config.daemon.port, !!config.daemon.tls);
28857
+ if (!agentId) {
28858
+ const daemonView2 = await fetchDaemonAgentDefaults(baseUrl, serviceToken);
28859
+ const view = daemonView2 ?? readAgentDefaults(await readRawConfig3());
28860
+ process.stdout.write(`${formatDefaultLine(view)}
28861
+ `);
28862
+ return;
28863
+ }
28643
28864
  let known;
28644
28865
  try {
28645
28866
  const r = await fetch(`${baseUrl}/v1/agents`, {
@@ -28660,13 +28881,62 @@ async function runAgentsSetDefault(agentId) {
28660
28881
  return;
28661
28882
  }
28662
28883
  const raw = await readRawConfig3();
28663
- raw.defaultAgent = agentId;
28664
- await writeRawConfig3(raw);
28884
+ if (modelId === void 0) {
28885
+ raw.defaultAgent = agentId;
28886
+ await writeRawConfig3(raw);
28887
+ } else {
28888
+ const models = raw.defaultModels && typeof raw.defaultModels === "object" ? raw.defaultModels : {};
28889
+ models[agentId] = modelId;
28890
+ raw.defaultModels = models;
28891
+ await writeRawConfig3(raw);
28892
+ }
28893
+ const disk = readAgentDefaults(await readRawConfig3());
28894
+ if (modelId !== void 0 && agentId !== disk.agent) {
28895
+ process.stdout.write(
28896
+ `Default model for ${agentId} is now ${modelId}.
28897
+ `
28898
+ );
28899
+ }
28900
+ process.stdout.write(`${formatDefaultLine(disk)}
28901
+ `);
28902
+ const daemonView = await fetchDaemonAgentDefaults(baseUrl, serviceToken);
28903
+ if (daemonView === void 0) {
28904
+ return;
28905
+ }
28906
+ if (daemonView.agent === disk.agent && daemonView.model === disk.model) {
28907
+ return;
28908
+ }
28665
28909
  process.stdout.write(
28666
- `Set defaultAgent to '${agentId}' in ${paths.config()}
28910
+ `Daemon still has ${formatAgentModel(daemonView)} \u2014 restart with \`hydra-acp daemon restart\` to apply.
28667
28911
  `
28668
28912
  );
28669
28913
  }
28914
+ function formatDefaultLine(view) {
28915
+ return `Default agent is ${formatAgentModel(view)}`;
28916
+ }
28917
+ function formatAgentModel(view) {
28918
+ return view.model !== void 0 ? `${view.agent} with ${view.model}` : view.agent;
28919
+ }
28920
+ function readAgentDefaults(raw) {
28921
+ const agent = typeof raw.defaultAgent === "string" ? raw.defaultAgent : "(unset)";
28922
+ const models = raw.defaultModels && typeof raw.defaultModels === "object" ? raw.defaultModels : {};
28923
+ const rawModel = models[agent];
28924
+ return typeof rawModel === "string" ? { agent, model: rawModel } : { agent };
28925
+ }
28926
+ async function fetchDaemonAgentDefaults(baseUrl, serviceToken) {
28927
+ try {
28928
+ const r = await fetch(`${baseUrl}/v1/config`, {
28929
+ headers: { Authorization: `Bearer ${serviceToken}` }
28930
+ });
28931
+ if (!r.ok) {
28932
+ return void 0;
28933
+ }
28934
+ const body = await r.json();
28935
+ return readAgentDefaults(body);
28936
+ } catch {
28937
+ return void 0;
28938
+ }
28939
+ }
28670
28940
  async function readRawConfig3() {
28671
28941
  const raw = await fsp12.readFile(paths.config(), "utf8");
28672
28942
  return JSON.parse(raw);
@@ -28837,6 +29107,7 @@ function maxLen5(headerCell, values) {
28837
29107
  }
28838
29108
 
28839
29109
  // src/shim/proxy.ts
29110
+ import * as fs21 from "fs";
28840
29111
  init_config();
28841
29112
  init_remote_target();
28842
29113
  init_daemon_bootstrap();
@@ -29013,6 +29284,8 @@ function isResponse2(msg) {
29013
29284
 
29014
29285
  // src/shim/proxy.ts
29015
29286
  init_permission_pick();
29287
+ init_hydra_version();
29288
+ init_paths();
29016
29289
 
29017
29290
  // src/core/process-title.ts
29018
29291
  init_bin_name();
@@ -29097,6 +29370,7 @@ function wireShim({
29097
29370
  tracker
29098
29371
  }) {
29099
29372
  upstream.onMessage((msg) => {
29373
+ wireLog("daemon\u2192client", msg);
29100
29374
  tracker.observeFromServer(msg);
29101
29375
  if (opts.dangerouslySkipPermissions === true && isPermissionRequest(msg)) {
29102
29376
  void upstream.send({
@@ -29111,7 +29385,12 @@ function wireShim({
29111
29385
  });
29112
29386
  const namingState = { name: opts.name, used: false };
29113
29387
  downstream.onMessage((msg) => {
29388
+ wireLog("client\u2192daemon", msg);
29114
29389
  tracker.observeFromClient(msg);
29390
+ if (isInitializeRequest(msg)) {
29391
+ void upstream.send(normaliseInitializeClientInfo(msg));
29392
+ return;
29393
+ }
29115
29394
  if (isSessionNewRequest(msg)) {
29116
29395
  if (opts.sessionId) {
29117
29396
  void upstream.send(buildAttachFromNew(msg, opts.sessionId));
@@ -29255,6 +29534,29 @@ async function replayAttach(stream, ctx, afterMessageId) {
29255
29534
  function isSessionNewRequest(msg) {
29256
29535
  return "method" in msg && "id" in msg && msg.id !== void 0 && msg.method === "session/new";
29257
29536
  }
29537
+ function isInitializeRequest(msg) {
29538
+ return "method" in msg && "id" in msg && msg.id !== void 0 && msg.method === "initialize";
29539
+ }
29540
+ function normaliseInitializeClientInfo(msg) {
29541
+ const params = msg.params ?? {};
29542
+ const existing = params.clientInfo;
29543
+ const existingObj = existing && typeof existing === "object" && !Array.isArray(existing) ? existing : void 0;
29544
+ const existingName = existingObj && typeof existingObj.name === "string" ? existingObj.name.trim() : "";
29545
+ if (existingName.length > 0) {
29546
+ return msg;
29547
+ }
29548
+ return {
29549
+ ...msg,
29550
+ params: {
29551
+ ...params,
29552
+ clientInfo: {
29553
+ ...existingObj ?? {},
29554
+ name: "hydra-acp-shim",
29555
+ version: HYDRA_VERSION
29556
+ }
29557
+ }
29558
+ };
29559
+ }
29258
29560
  function isPermissionRequest(msg) {
29259
29561
  return "method" in msg && "id" in msg && msg.id !== void 0 && msg.method === "session/request_permission";
29260
29562
  }
@@ -29276,6 +29578,40 @@ function rewriteSessionNewWithAgent(msg, agentId) {
29276
29578
  params: { ...params, agentId }
29277
29579
  };
29278
29580
  }
29581
+ var WIRE_LOG_MAX_BYTES = 25 * 1024 * 1024;
29582
+ var wireLogChecked = false;
29583
+ var wireLogPath = null;
29584
+ function wireLog(direction, msg) {
29585
+ if (!process.env.HYDRA_SHIM_WIRE_LOG) {
29586
+ return;
29587
+ }
29588
+ if (!wireLogChecked) {
29589
+ wireLogChecked = true;
29590
+ try {
29591
+ wireLogPath = paths.shimWireLogFile();
29592
+ fs21.mkdirSync(paths.home(), { recursive: true });
29593
+ const st = fs21.statSync(wireLogPath, { throwIfNoEntry: false });
29594
+ if (st && st.size > WIRE_LOG_MAX_BYTES) {
29595
+ fs21.renameSync(wireLogPath, `${wireLogPath}.1`);
29596
+ }
29597
+ } catch {
29598
+ wireLogPath = null;
29599
+ }
29600
+ }
29601
+ if (!wireLogPath) {
29602
+ return;
29603
+ }
29604
+ try {
29605
+ const line = JSON.stringify({
29606
+ t: (/* @__PURE__ */ new Date()).toISOString(),
29607
+ pid: process.pid,
29608
+ dir: direction,
29609
+ msg
29610
+ }) + "\n";
29611
+ fs21.appendFile(wireLogPath, line, () => void 0);
29612
+ } catch {
29613
+ }
29614
+ }
29279
29615
  function injectHydraMeta(msg, additions) {
29280
29616
  const params = msg.params ?? {};
29281
29617
  const existingMeta = params._meta ?? {};
@@ -29428,6 +29764,13 @@ function applyStyle(text, style) {
29428
29764
  // src/cli/commands/cat.ts
29429
29765
  var DEFAULT_STREAM_THRESHOLD = 1 * 1024 * 1024;
29430
29766
  var HYDRA_STDIN_TOOL_PREFIX = "mcp__hydra-acp-stdin__";
29767
+ function catPromptParams(sessionId, prompt) {
29768
+ return {
29769
+ sessionId,
29770
+ prompt,
29771
+ _meta: { [HYDRA_META_KEY]: { ancillary: true } }
29772
+ };
29773
+ }
29431
29774
  function deriveTitleFromPrompt(prompt) {
29432
29775
  if (!prompt) {
29433
29776
  return void 0;
@@ -29608,10 +29951,7 @@ async function runCatLoop(args) {
29608
29951
  return;
29609
29952
  }
29610
29953
  try {
29611
- await conn.request("session/prompt", {
29612
- sessionId,
29613
- prompt: promptBlocks
29614
- });
29954
+ await conn.request("session/prompt", catPromptParams(sessionId, promptBlocks));
29615
29955
  firstChunkSent = true;
29616
29956
  } catch (err) {
29617
29957
  stderr(`hydra-acp cat: prompt failed: ${err.message}
@@ -29834,10 +30174,10 @@ function runStreamingPath(args) {
29834
30174
  }
29835
30175
  await writeChain.catch(() => void 0);
29836
30176
  const promptText = buildStreamPromptText(opts.prompt, open2.capacityBytes);
29837
- const promptDone = conn.request("session/prompt", {
29838
- sessionId,
29839
- prompt: [{ type: "text", text: promptText }]
29840
- }).catch((err) => {
30177
+ const promptDone = conn.request(
30178
+ "session/prompt",
30179
+ catPromptParams(sessionId, [{ type: "text", text: promptText }])
30180
+ ).catch((err) => {
29841
30181
  args.onPromptFailed(
29842
30182
  new Error(`prompt failed: ${err.message}`)
29843
30183
  );
@@ -30167,7 +30507,7 @@ async function main() {
30167
30507
  all: flags.all === true,
30168
30508
  json: flags.json === true,
30169
30509
  host: typeof flags.host === "string" ? flags.host : void 0,
30170
- includeCat: flags["include-cat"] === true
30510
+ includeNonInteractive: flags["include-non-interactive"] === true
30171
30511
  });
30172
30512
  return;
30173
30513
  }
@@ -30332,7 +30672,7 @@ async function main() {
30332
30672
  return;
30333
30673
  }
30334
30674
  if (sub === "set") {
30335
- await runAgentsSetDefault(positional[2]);
30675
+ await runAgentsSet(positional[2], positional[3]);
30336
30676
  return;
30337
30677
  }
30338
30678
  if (sub === "log" || sub === "logs") {
@@ -30540,10 +30880,10 @@ function printHelp() {
30540
30880
  " hydra-acp daemon start [--foreground] Start daemon (detached by default; --foreground to attach)",
30541
30881
  " hydra-acp daemon stop|restart",
30542
30882
  " hydra-acp daemon logs [-f] [-n N] Tail or follow the daemon log",
30543
- " hydra-acp session [list] [--all] [--json] [--host=<host>] [--include-cat]",
30544
- " List sessions (live + 20 most-recent cold; --all for everything; --json emits JSON for scripts).",
30883
+ " hydra-acp session [list] [--all] [--json] [--host=<host>] [--include-non-interactive]",
30884
+ " List sessions (live + 20 most-recent cold; --all lifts the cold cap AND surfaces non-interactive sessions; --json emits JSON for scripts).",
30545
30885
  " --host filters by origin machine: 'local' (default) shows only sessions created here, 'all' shows everything, or pass a hostname (e.g. machine-b) to show only imports from that peer.",
30546
- " --include-cat surfaces sessions spawned by `hydra cat` (hidden by default).",
30886
+ " --include-non-interactive surfaces ancillary (e.g. `hydra cat`) or never-prompted sessions while keeping the cold cap (a narrower --all).",
30547
30887
  " hydra-acp session info <id> [--verbose] [--json] [--diff] [--fold] [--no-color] [--no-pager]",
30548
30888
  " Aggregate one session: turn count, tool histogram, files touched, cost/duration, synopsis. --diff appends the session diff under the summary and pages the whole thing on a TTY (inherits --fold).",
30549
30889
  " hydra-acp session diff <id> [--json] [--no-color] [--no-pager] [--fold]",
@@ -30571,7 +30911,7 @@ function printHelp() {
30571
30911
  " hydra-acp agent [list] List agents in the cached registry",
30572
30912
  " hydra-acp agent refresh Force a registry re-fetch",
30573
30913
  " hydra-acp agent install <id> Pre-install <id> from the registry (else lazy on first session)",
30574
- " hydra-acp agent set <id> Set <id> as the default agent (config.defaultAgent)",
30914
+ " hydra-acp agent set [<id>] [model] With no args, report the daemon's current default agent and its default model. With <id>, set <id> as the default agent (config.defaultAgent). With <id> and [model], set the per-agent default model (config.defaultModels[<id>]).",
30575
30915
  " hydra-acp agent sync <id> Spawn <id> just long enough to ACP session/list it, then persist any sessions it remembers (across every cwd) as cold rows in `session list`",
30576
30916
  " hydra-acp agent log <id> [-f] [-n N] Tail or follow an agent's spawn/stderr log",
30577
30917
  " hydra-acp auth password [--force] Set the daemon's master password",