@hydra-acp/cli 0.1.10 → 0.1.12

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
@@ -264,7 +264,13 @@ var init_config = __esm({
264
264
  // Width cap on the cwd column in the `sessions list` output and the
265
265
  // TUI picker. Set higher if you keep deeply-nested working directories
266
266
  // and want them visible; the elastic title column shrinks to make room.
267
- cwdColumnMaxWidth: z.number().int().positive().default(24)
267
+ cwdColumnMaxWidth: z.number().int().positive().default(24),
268
+ // When true (default), emit OSC 9;4 progress-bar control codes so the
269
+ // host terminal can show an indeterminate busy indicator (taskbar pulse
270
+ // on Windows Terminal, dock badge on KDE/Konsole, etc.) while a turn is
271
+ // running. Set false if your terminal renders this obnoxiously or you
272
+ // just don't want it.
273
+ progressIndicator: z.boolean().default(true)
268
274
  });
269
275
  ExtensionName = z.string().min(1).regex(/^[A-Za-z0-9._-]+$/, "extension name must be filename-safe");
270
276
  ExtensionBody = z.object({
@@ -302,11 +308,12 @@ var init_config = __esm({
302
308
  maxScrollbackLines: 1e4,
303
309
  mouse: true,
304
310
  logMaxBytes: 5 * 1024 * 1024,
305
- cwdColumnMaxWidth: 24
311
+ cwdColumnMaxWidth: 24,
312
+ progressIndicator: true
306
313
  })
307
314
  });
308
315
  HydraConfigReadOnly = HydraConfig.extend({
309
- daemon: DaemonConfig.omit({ authToken: true })
316
+ daemon: DaemonConfig.omit({ authToken: true }).default({})
310
317
  });
311
318
  }
312
319
  });
@@ -483,6 +490,11 @@ var init_types = __esm({
483
490
  // Last-known usage snapshot so list views can show per-session cost
484
491
  // (and tokens, in callers that care) without resurrecting cold sessions.
485
492
  currentUsage: SessionListUsage.optional(),
493
+ // Origin host (and origin upstream id) for imported sessions. Picker
494
+ // uses the host to fill in the UPSTREAM cell pre-first-attach;
495
+ // future "connect back to origin" callers would dial both.
496
+ importedFromMachine: z3.string().optional(),
497
+ importedFromUpstreamSessionId: z3.string().optional(),
486
498
  updatedAt: z3.string(),
487
499
  attachedClients: z3.number().int().nonnegative(),
488
500
  status: z3.enum(["live", "cold"]).default("live"),
@@ -563,6 +575,15 @@ var init_connection = __esm({
563
575
  }
564
576
  }
565
577
  }
578
+ // Discard any notifications buffered for the given method without firing
579
+ // handlers. Used by the resurrect path to drop the agent's session/load
580
+ // replay: that replay is the agent re-emitting our own history back at
581
+ // us, and if we flushed it through wireAgent's session/update handler
582
+ // every entry would be re-appended to history.jsonl, doubling the log
583
+ // each time the session was woken up.
584
+ drainBuffered(method) {
585
+ this.bufferedNotifications.delete(method);
586
+ }
566
587
  onClose(handler) {
567
588
  this.closeHandlers.push(handler);
568
589
  }
@@ -997,6 +1018,7 @@ var init_session = __esm({
997
1018
  // and noisy state churn keep a quiet session alive forever.
998
1019
  lastRecordedAt;
999
1020
  spawnReplacementAgent;
1021
+ logger;
1000
1022
  agentChangeHandlers = [];
1001
1023
  // Last available_commands_update we observed from the agent. Stored
1002
1024
  // so we can re-broadcast a merged (hydra ∪ agent) list whenever
@@ -1028,6 +1050,7 @@ var init_session = __esm({
1028
1050
  }
1029
1051
  this.idleTimeoutMs = init.idleTimeoutMs ?? 0;
1030
1052
  this.spawnReplacementAgent = init.spawnReplacementAgent;
1053
+ this.logger = init.logger;
1031
1054
  if (init.firstPromptSeeded) {
1032
1055
  this.firstPromptSeeded = true;
1033
1056
  }
@@ -1357,6 +1380,9 @@ var init_session = __esm({
1357
1380
  if (this.closed) {
1358
1381
  return;
1359
1382
  }
1383
+ this.logger?.info(
1384
+ `session ${this.sessionId} closing deleteRecord=${opts.deleteRecord ?? false} regenTitle=${opts.regenTitle ?? false}`
1385
+ );
1360
1386
  this.cancelIdleTimer();
1361
1387
  if (opts.regenTitle && this.firstPromptSeeded) {
1362
1388
  const timeoutMs = opts.regenTitleTimeoutMs ?? 5e3;
@@ -1911,6 +1937,10 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
1911
1937
  return;
1912
1938
  }
1913
1939
  const opts = this.firstPromptSeeded ? { deleteRecord: false, regenTitle: true } : { deleteRecord: true };
1940
+ const idleSec = Math.round(idle / 1e3);
1941
+ this.logger?.info(
1942
+ `session ${this.sessionId} idle timeout fired after ${idleSec}s (window=${Math.round(this.idleTimeoutMs / 1e3)}s) \u2014 closing`
1943
+ );
1914
1944
  void this.close(opts).catch(() => void 0);
1915
1945
  }
1916
1946
  cancelIdleTimer() {
@@ -2087,6 +2117,8 @@ function recordFromMemorySession(args) {
2087
2117
  lineageId: args.lineageId,
2088
2118
  upstreamSessionId: args.upstreamSessionId,
2089
2119
  importedFromSessionId: args.importedFromSessionId,
2120
+ importedFromUpstreamSessionId: args.importedFromUpstreamSessionId,
2121
+ importedFromMachine: args.importedFromMachine,
2090
2122
  agentId: args.agentId,
2091
2123
  cwd: args.cwd,
2092
2124
  title: args.title,
@@ -2134,6 +2166,16 @@ var init_session_store = __esm({
2134
2166
  // origin's local id at export time, kept for debuggability and as a
2135
2167
  // breadcrumb in `sessions list` (informational, not used for routing).
2136
2168
  importedFromSessionId: z4.string().optional(),
2169
+ // Origin's agent-side session id at export time. Carried as a
2170
+ // breadcrumb and as the handle a future "connect back to origin"
2171
+ // feature would dial. Absent when the origin record had no upstream
2172
+ // bound (re-export of an imported, not-yet-attached session).
2173
+ importedFromUpstreamSessionId: z4.string().optional(),
2174
+ // Hostname of the machine that exported the bundle we imported
2175
+ // (i.e. the most recent hop, not necessarily the true multi-hop
2176
+ // origin). Surfaced in the picker so imported rows don't look like
2177
+ // they materialized from nowhere.
2178
+ importedFromMachine: z4.string().optional(),
2137
2179
  agentId: z4.string(),
2138
2180
  cwd: z4.string(),
2139
2181
  title: z4.string().optional(),
@@ -2349,6 +2391,7 @@ function encodeBundle(params) {
2349
2391
  session: {
2350
2392
  sessionId: params.record.sessionId,
2351
2393
  lineageId: params.record.lineageId,
2394
+ ...params.record.upstreamSessionId ? { upstreamSessionId: params.record.upstreamSessionId } : {},
2352
2395
  agentId: params.record.agentId,
2353
2396
  cwd: params.record.cwd,
2354
2397
  ...params.record.title !== void 0 ? { title: params.record.title } : {},
@@ -2386,6 +2429,12 @@ var init_bundle = __esm({
2386
2429
  // Required on bundles — the export path backfills if the source
2387
2430
  // record was written before lineageId existed.
2388
2431
  lineageId: z5.string(),
2432
+ // The exporter's agent-side session id at export time. Carried so
2433
+ // importers can persist it as a breadcrumb (and, eventually, as the
2434
+ // handle a "connect back to origin" feature would need). Omitted on
2435
+ // bundles whose source record never bound to an agent (e.g. a
2436
+ // re-export of an imported, not-yet-attached session).
2437
+ upstreamSessionId: z5.string().optional(),
2389
2438
  agentId: z5.string(),
2390
2439
  cwd: z5.string(),
2391
2440
  title: z5.string().optional(),
@@ -2599,7 +2648,7 @@ var init_agent_display = __esm({
2599
2648
  function toRow(s, now = Date.now()) {
2600
2649
  return {
2601
2650
  session: stripHydraSessionPrefix(s.sessionId),
2602
- upstream: s.upstreamSessionId ?? "-",
2651
+ upstream: formatUpstreamCell(s.upstreamSessionId, s.importedFromMachine),
2603
2652
  state: formatState(s.status, s.attachedClients),
2604
2653
  agent: formatAgentCell(s.agentId, s.currentUsage),
2605
2654
  age: formatRelativeAge(s.updatedAt, now),
@@ -2607,6 +2656,15 @@ function toRow(s, now = Date.now()) {
2607
2656
  cwd: shortenHomePath(s.cwd)
2608
2657
  };
2609
2658
  }
2659
+ function formatUpstreamCell(upstreamSessionId, importedFromMachine) {
2660
+ if (upstreamSessionId && upstreamSessionId.length > 0) {
2661
+ return upstreamSessionId;
2662
+ }
2663
+ if (importedFromMachine && importedFromMachine.length > 0) {
2664
+ return `\u2190 ${importedFromMachine}`;
2665
+ }
2666
+ return "-";
2667
+ }
2610
2668
  function formatState(status, clients) {
2611
2669
  if (status === "cold") {
2612
2670
  return "COLD";
@@ -2946,11 +3004,11 @@ async function runSessionsImport(file, opts = {}) {
2946
3004
  function bundleToSummary(parsed) {
2947
3005
  return {
2948
3006
  sessionId: parsed.session.sessionId,
2949
- upstreamSessionId: "-",
2950
3007
  cwd: parsed.session.cwd,
2951
3008
  agentId: parsed.session.agentId,
2952
3009
  currentUsage: parsed.session.currentUsage,
2953
3010
  title: parsed.session.title,
3011
+ importedFromMachine: parsed.exportedFrom.machine,
2954
3012
  attachedClients: 0,
2955
3013
  updatedAt: parsed.session.updatedAt,
2956
3014
  status: "cold"
@@ -2971,10 +3029,13 @@ function printBundleInfo(raw, cwdColumnMaxWidth) {
2971
3029
  const maxWidth = process.stdout.isTTY ? process.stdout.columns : void 0;
2972
3030
  process.stdout.write(formatRow(HEADER, widths, maxWidth, cwdColumnMaxWidth) + "\n");
2973
3031
  process.stdout.write(formatRow(row, widths, maxWidth, cwdColumnMaxWidth) + "\n");
3032
+ const originUpstream = parsed.session.upstreamSessionId ?? "-";
2974
3033
  process.stdout.write(
2975
3034
  `
2976
3035
  lineage: ${parsed.session.lineageId}
2977
3036
  exported: ${parsed.exportedAt} from ${parsed.exportedFrom.machine} (hydra ${parsed.exportedFrom.hydraVersion})
3037
+ origin session: ${parsed.session.sessionId}
3038
+ origin upstream: ${originUpstream}
2978
3039
  history entries: ${parsed.history.length}` + (parsed.promptHistory ? `, prompt history: ${parsed.promptHistory.length}
2979
3040
  ` : "\n")
2980
3041
  );
@@ -3275,7 +3336,9 @@ async function listSessions(config, opts = {}, fetchImpl = fetch) {
3275
3336
  agentId: s.agentId,
3276
3337
  currentModel: s.currentModel,
3277
3338
  currentUsage: s.currentUsage,
3278
- title: s.title
3339
+ title: s.title,
3340
+ importedFromMachine: s.importedFromMachine,
3341
+ importedFromUpstreamSessionId: s.importedFromUpstreamSessionId
3279
3342
  }));
3280
3343
  }
3281
3344
  async function killSession(config, id, fetchImpl = fetch) {
@@ -4427,6 +4490,12 @@ var init_screen = __esm({
4427
4490
  pasteBuffer = "";
4428
4491
  rawStdinHandler;
4429
4492
  mouseEnabled;
4493
+ progressIndicatorEnabled;
4494
+ // Last OSC 9;4 state we wrote (3 = indeterminate, 0 = remove). Used to
4495
+ // suppress redundant writes when setBanner runs but `status` didn't
4496
+ // actually change, and to re-emit on start() if a picker round-trip
4497
+ // cleared the host terminal's indicator.
4498
+ lastProgressState = 0;
4430
4499
  constructor(opts) {
4431
4500
  this.term = opts.term;
4432
4501
  this.dispatcher = opts.dispatcher;
@@ -4434,6 +4503,7 @@ var init_screen = __esm({
4434
4503
  this.contentRepaintThrottleMs = opts.repaintThrottleMs ?? DEFAULT_CONTENT_REPAINT_THROTTLE_MS;
4435
4504
  this.maxScrollbackLines = opts.maxScrollbackLines ?? DEFAULT_MAX_SCROLLBACK_LINES;
4436
4505
  this.mouseEnabled = opts.mouse ?? true;
4506
+ this.progressIndicatorEnabled = opts.progressIndicator ?? true;
4437
4507
  this.resizeHandler = () => this.repaint();
4438
4508
  this.keyHandler = (name, _matches, data) => this.handleKey(name, data);
4439
4509
  this.mouseHandler = (name) => this.handleMouse(name);
@@ -4462,6 +4532,8 @@ var init_screen = __esm({
4462
4532
  }
4463
4533
  this.term.on("resize", this.resizeHandler);
4464
4534
  this.installBracketedPaste();
4535
+ this.lastProgressState = 0;
4536
+ this.writeProgressIndicator(this.banner.status === "busy" ? 3 : 0);
4465
4537
  this.repaint();
4466
4538
  }
4467
4539
  stop() {
@@ -4482,6 +4554,7 @@ var init_screen = __esm({
4482
4554
  this.term.grabInput(false);
4483
4555
  this.term.hideCursor(false);
4484
4556
  process.stdout.write("\x1B[?7h");
4557
+ this.writeProgressIndicator(0);
4485
4558
  this.term.fullscreen(false);
4486
4559
  this.term("\n");
4487
4560
  }
@@ -4770,9 +4843,26 @@ var init_screen = __esm({
4770
4843
  }
4771
4844
  setBanner(banner) {
4772
4845
  this.banner = { ...this.banner, ...banner };
4846
+ this.writeProgressIndicator(this.banner.status === "busy" ? 3 : 0);
4773
4847
  this.drawBanner();
4774
4848
  this.placeCursor();
4775
4849
  }
4850
+ // OSC 9;4 progress-bar control. State 3 = indeterminate (pulsing
4851
+ // taskbar / dock badge while a turn is running); state 0 = remove.
4852
+ // ConEmu-flavor sequence — supported by Windows Terminal, WezTerm,
4853
+ // Ghostty, Konsole, Black Box, Rio, and others; ignored harmlessly
4854
+ // by terminals that don't implement it. Disabled entirely when
4855
+ // tui.progressIndicator is false.
4856
+ writeProgressIndicator(state) {
4857
+ if (!this.progressIndicatorEnabled) {
4858
+ return;
4859
+ }
4860
+ if (state === this.lastProgressState) {
4861
+ return;
4862
+ }
4863
+ this.lastProgressState = state;
4864
+ process.stdout.write(`\x1B]9;4;${state}\x1B\\`);
4865
+ }
4776
4866
  // Transient right-side banner message. Cleared automatically after
4777
4867
  // durationMs (default 4s). Each call resets the timer, so rapid
4778
4868
  // successive notifications coalesce on the latest text. Active
@@ -7502,6 +7592,7 @@ async function runSession(term, config, opts, exitHint) {
7502
7592
  repaintThrottleMs: config.tui.repaintThrottleMs,
7503
7593
  maxScrollbackLines: config.tui.maxScrollbackLines,
7504
7594
  mouse: config.tui.mouse,
7595
+ progressIndicator: config.tui.progressIndicator,
7505
7596
  onKey: (events) => {
7506
7597
  for (const ev of events) {
7507
7598
  if (pendingPermission && tryHandlePermissionKey(ev)) {
@@ -8642,6 +8733,17 @@ import { fileURLToPath as fileURLToPath2 } from "url";
8642
8733
  import { dirname as dirname6, resolve as resolve4 } from "path";
8643
8734
 
8644
8735
  // src/cli/parse-args.ts
8736
+ var KNOWN_BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
8737
+ "all",
8738
+ "foreground",
8739
+ "help",
8740
+ "info",
8741
+ "new",
8742
+ "reattach",
8743
+ "replace",
8744
+ "rotate-token",
8745
+ "version"
8746
+ ]);
8645
8747
  function parseArgs(argv) {
8646
8748
  const positional = [];
8647
8749
  const flags = {};
@@ -8660,6 +8762,11 @@ function parseArgs(argv) {
8660
8762
  continue;
8661
8763
  }
8662
8764
  const key = token.slice(2);
8765
+ if (KNOWN_BOOLEAN_FLAGS.has(key)) {
8766
+ flags[key] = true;
8767
+ i += 1;
8768
+ continue;
8769
+ }
8663
8770
  const next = argv[i + 1];
8664
8771
  if (next !== void 0 && !next.startsWith("--")) {
8665
8772
  flags[key] = next;
@@ -9383,12 +9490,14 @@ var AgentInstance = class _AgentInstance {
9383
9490
  killed = false;
9384
9491
  stderrTail = "";
9385
9492
  stderrTailBytes;
9493
+ logger;
9386
9494
  exitHandlers = [];
9387
9495
  constructor(opts, child) {
9388
9496
  this.agentId = opts.agentId;
9389
9497
  this.cwd = opts.cwd;
9390
9498
  this.child = child;
9391
9499
  this.stderrTailBytes = opts.stderrTailBytes ?? DEFAULT_STDERR_TAIL_BYTES;
9500
+ this.logger = opts.logger;
9392
9501
  if (!child.stdout || !child.stdin) {
9393
9502
  throw new Error("agent subprocess missing stdio");
9394
9503
  }
@@ -9397,7 +9506,15 @@ var AgentInstance = class _AgentInstance {
9397
9506
  child.stderr?.setEncoding("utf8");
9398
9507
  child.stderr?.on("data", (chunk) => {
9399
9508
  this.stderrTail = (this.stderrTail + chunk).slice(-this.stderrTailBytes);
9400
- process.stderr.write(`[${opts.agentId}] ${chunk}`);
9509
+ if (this.logger) {
9510
+ for (const line of chunk.split(/\r?\n/)) {
9511
+ if (line.length > 0) {
9512
+ this.logger.info(`[${opts.agentId}] ${line}`);
9513
+ }
9514
+ }
9515
+ } else {
9516
+ process.stderr.write(`[${opts.agentId}] ${chunk}`);
9517
+ }
9401
9518
  });
9402
9519
  child.on("error", (err) => {
9403
9520
  const msg = this.formatFailure(err.message);
@@ -9405,9 +9522,16 @@ var AgentInstance = class _AgentInstance {
9405
9522
  });
9406
9523
  child.on("exit", (code, signal) => {
9407
9524
  this.exited = true;
9408
- if (!this.killed) {
9525
+ if (this.killed) {
9526
+ this.logger?.info(
9527
+ `agent ${opts.agentId} pid=${child.pid} exited after kill code=${code} signal=${signal}`
9528
+ );
9529
+ } else {
9409
9530
  const reason = `agent ${opts.agentId} exited before responding (code=${code} signal=${signal})`;
9410
9531
  this.connection.fail(new Error(this.formatFailure(reason)));
9532
+ this.logger?.warn(
9533
+ `agent ${opts.agentId} pid=${child.pid} exited unexpectedly code=${code} signal=${signal}`
9534
+ );
9411
9535
  }
9412
9536
  for (const handler of this.exitHandlers) {
9413
9537
  handler(code, signal);
@@ -9428,7 +9552,15 @@ stderr: ${tail}` : reason;
9428
9552
  const child = spawn3(opts.plan.command, opts.plan.args, {
9429
9553
  cwd: opts.cwd,
9430
9554
  env,
9431
- stdio: ["pipe", "pipe", "pipe"]
9555
+ stdio: ["pipe", "pipe", "pipe"],
9556
+ // setsid the agent into its own session/process group. The daemon
9557
+ // already runs in its own setsid'd session, but macOS terminals
9558
+ // (iTerm2, Terminal.app) sometimes still reach inherited child
9559
+ // processes when the user closes a window — putting the agent
9560
+ // one more session-boundary away keeps it alive across terminal
9561
+ // restarts. The daemon still owns the pipes, so this.kill()
9562
+ // continues to terminate it cleanly on idle/close.
9563
+ detached: true
9432
9564
  });
9433
9565
  return new _AgentInstance(opts, child);
9434
9566
  }
@@ -9443,6 +9575,9 @@ stderr: ${tail}` : reason;
9443
9575
  return;
9444
9576
  }
9445
9577
  this.killed = true;
9578
+ this.logger?.info(
9579
+ `agent ${this.agentId} pid=${this.child.pid} kill requested signal=${signal}`
9580
+ );
9446
9581
  await this.connection.close().catch(() => void 0);
9447
9582
  this.child.kill(signal);
9448
9583
  }
@@ -9628,6 +9763,7 @@ var SessionManager = class {
9628
9763
  this.histories = new HistoryStore({ maxEntries: this.sessionHistoryMaxEntries });
9629
9764
  this.idleTimeoutMs = options.idleTimeoutMs ?? 0;
9630
9765
  this.defaultModels = options.defaultModels ?? {};
9766
+ this.logger = options.logger;
9631
9767
  }
9632
9768
  registry;
9633
9769
  sessions = /* @__PURE__ */ new Map();
@@ -9642,6 +9778,7 @@ var SessionManager = class {
9642
9778
  // concurrent snapshot updates (e.g. an agent emitting model + mode
9643
9779
  // back-to-back) don't lose writes via interleaved reads.
9644
9780
  metaWriteQueues = /* @__PURE__ */ new Map();
9781
+ logger;
9645
9782
  async create(params) {
9646
9783
  const fresh = await this.bootstrapAgent({
9647
9784
  agentId: params.agentId,
@@ -9659,6 +9796,7 @@ var SessionManager = class {
9659
9796
  title: params.title,
9660
9797
  agentArgs: params.agentArgs,
9661
9798
  idleTimeoutMs: this.idleTimeoutMs,
9799
+ logger: this.logger,
9662
9800
  spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
9663
9801
  historyStore: this.histories,
9664
9802
  historyMaxEntries: this.sessionHistoryMaxEntries,
@@ -9741,6 +9879,7 @@ var SessionManager = class {
9741
9879
  await agent.kill().catch(() => void 0);
9742
9880
  return this.doResurrectFromImport(params);
9743
9881
  }
9882
+ agent.connection.drainBuffered("session/update");
9744
9883
  const session = new Session({
9745
9884
  sessionId: params.hydraSessionId,
9746
9885
  cwd: params.cwd,
@@ -9751,6 +9890,7 @@ var SessionManager = class {
9751
9890
  title: params.title,
9752
9891
  agentArgs: params.agentArgs,
9753
9892
  idleTimeoutMs: this.idleTimeoutMs,
9893
+ logger: this.logger,
9754
9894
  spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
9755
9895
  historyStore: this.histories,
9756
9896
  historyMaxEntries: this.sessionHistoryMaxEntries,
@@ -9797,6 +9937,7 @@ var SessionManager = class {
9797
9937
  title: params.title,
9798
9938
  agentArgs: params.agentArgs,
9799
9939
  idleTimeoutMs: this.idleTimeoutMs,
9940
+ logger: this.logger,
9800
9941
  spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
9801
9942
  historyStore: this.histories,
9802
9943
  historyMaxEntries: this.sessionHistoryMaxEntries,
@@ -10061,6 +10202,8 @@ var SessionManager = class {
10061
10202
  agentId: r.agentId,
10062
10203
  currentModel: r.currentModel,
10063
10204
  currentUsage: r.currentUsage,
10205
+ importedFromMachine: r.importedFromMachine,
10206
+ importedFromUpstreamSessionId: r.importedFromUpstreamSessionId,
10064
10207
  updatedAt: used,
10065
10208
  attachedClients: 0,
10066
10209
  status: "cold"
@@ -10168,6 +10311,8 @@ var SessionManager = class {
10168
10311
  lineageId: args.bundle.session.lineageId,
10169
10312
  upstreamSessionId: "",
10170
10313
  importedFromSessionId: args.bundle.session.sessionId,
10314
+ importedFromUpstreamSessionId: args.bundle.session.upstreamSessionId,
10315
+ importedFromMachine: args.bundle.exportedFrom.machine,
10171
10316
  agentId: args.bundle.session.agentId,
10172
10317
  cwd: args.cwd ?? args.bundle.session.cwd,
10173
10318
  title: args.bundle.session.title,
@@ -10290,6 +10435,8 @@ function mergeForPersistence(session, existing) {
10290
10435
  lineageId: existing?.lineageId ?? generateLineageId(),
10291
10436
  upstreamSessionId: session.upstreamSessionId,
10292
10437
  importedFromSessionId: existing?.importedFromSessionId,
10438
+ importedFromUpstreamSessionId: existing?.importedFromUpstreamSessionId,
10439
+ importedFromMachine: existing?.importedFromMachine,
10293
10440
  agentId: session.agentId,
10294
10441
  cwd: session.cwd,
10295
10442
  title: session.title,
@@ -11569,11 +11716,20 @@ async function startDaemon(config) {
11569
11716
  await auth(request, reply);
11570
11717
  });
11571
11718
  const registry = new Registry(config);
11572
- const spawner = (opts) => AgentInstance.spawn({ ...opts, stderrTailBytes: config.daemon.agentStderrTailBytes });
11719
+ const agentLogger = {
11720
+ info: (msg) => app.log.info(msg),
11721
+ warn: (msg) => app.log.warn(msg)
11722
+ };
11723
+ const spawner = (opts) => AgentInstance.spawn({
11724
+ ...opts,
11725
+ stderrTailBytes: config.daemon.agentStderrTailBytes,
11726
+ logger: agentLogger
11727
+ });
11573
11728
  const manager = new SessionManager(registry, spawner, void 0, {
11574
11729
  idleTimeoutMs: config.daemon.sessionIdleTimeoutSeconds * 1e3,
11575
11730
  defaultModels: config.defaultModels,
11576
- sessionHistoryMaxEntries: config.daemon.sessionHistoryMaxEntries
11731
+ sessionHistoryMaxEntries: config.daemon.sessionHistoryMaxEntries,
11732
+ logger: agentLogger
11577
11733
  });
11578
11734
  const extensions = new ExtensionManager(extensionList(config));
11579
11735
  registerHealthRoutes(app, HYDRA_VERSION);
@@ -11810,6 +11966,7 @@ async function runDaemonStart(flags = {}) {
11810
11966
  };
11811
11967
  process.on("SIGINT", () => void shutdown());
11812
11968
  process.on("SIGTERM", () => void shutdown());
11969
+ process.on("SIGHUP", () => void 0);
11813
11970
  return;
11814
11971
  }
11815
11972
  spawnDaemonDetached();
@@ -12880,6 +13037,17 @@ async function main() {
12880
13037
  const positionalAgentId = afterLaunch[0];
12881
13038
  const agentArgs = afterLaunch.slice(1);
12882
13039
  const { flags: flags2 } = parseArgs(beforeLaunch);
13040
+ if (flags2.resume === true) {
13041
+ bareResumeError();
13042
+ return;
13043
+ }
13044
+ if (flags2.reattach === true) {
13045
+ process.stderr.write(
13046
+ "hydra-acp launch: --reattach is not valid here. Pass --resume <id> to attach to a specific session.\n"
13047
+ );
13048
+ process.exit(2);
13049
+ return;
13050
+ }
12883
13051
  const agentId = positionalAgentId ?? resolveOption(flags2, "agent");
12884
13052
  if (!agentId) {
12885
13053
  process.stderr.write(
@@ -12888,8 +13056,7 @@ async function main() {
12888
13056
  process.exit(2);
12889
13057
  return;
12890
13058
  }
12891
- const launchResume = flags2.resume;
12892
- const sessionId2 = typeof launchResume === "string" ? launchResume : resolveOption(flags2, "session-id");
13059
+ const sessionId2 = typeof flags2.resume === "string" ? flags2.resume : resolveOption(flags2, "session-id");
12893
13060
  const name2 = resolveOption(flags2, "name");
12894
13061
  const model2 = resolveOption(flags2, "model");
12895
13062
  await runShim({ sessionId: sessionId2, agentId, agentArgs, name: name2, model: model2 });
@@ -12906,8 +13073,11 @@ async function main() {
12906
13073
  return;
12907
13074
  }
12908
13075
  const subcommand = positional[0];
12909
- const resumeFlag = flags.resume;
12910
- const sessionId = typeof resumeFlag === "string" ? resumeFlag : resolveOption(flags, "session-id");
13076
+ if (flags.resume === true) {
13077
+ bareResumeError();
13078
+ return;
13079
+ }
13080
+ const sessionId = typeof flags.resume === "string" ? flags.resume : resolveOption(flags, "session-id");
12911
13081
  const name = resolveOption(flags, "name");
12912
13082
  const agentIdFromFlag = resolveOption(flags, "agent");
12913
13083
  const model = resolveOption(flags, "model");
@@ -13064,7 +13234,7 @@ async function main() {
13064
13234
  }
13065
13235
  async function dispatchTui(flags, base) {
13066
13236
  const cwd = resolveOption(flags, "cwd");
13067
- const resume = flags.resume === true;
13237
+ const resume = flags.reattach === true;
13068
13238
  const forceNew = flags.new === true;
13069
13239
  const { runTui } = await Promise.resolve().then(() => (init_tui(), tui_exports));
13070
13240
  const tuiOpts = { resume, forceNew };
@@ -13085,6 +13255,12 @@ async function dispatchTui(flags, base) {
13085
13255
  }
13086
13256
  await runTui(tuiOpts);
13087
13257
  }
13258
+ function bareResumeError() {
13259
+ process.stderr.write(
13260
+ "hydra-acp: --resume requires a session id. Use --resume <id> to attach to a specific session, or --reattach to pick the most recent one in cwd.\n"
13261
+ );
13262
+ process.exit(2);
13263
+ }
13088
13264
  function readVersion() {
13089
13265
  try {
13090
13266
  const here = dirname6(fileURLToPath2(import.meta.url));
@@ -13110,6 +13286,7 @@ function printHelp() {
13110
13286
  " from the registry. Args after <agent>",
13111
13287
  " are forwarded to the agent's command.",
13112
13288
  " hydra-acp --resume <id> Attach to an existing session (TUI when in a terminal, shim otherwise)",
13289
+ " hydra-acp --reattach Attach to the most-recent session for the current cwd (TUI/shim auto-pick)",
13113
13290
  " hydra-acp init [--rotate-token] Initialize ~/.hydra-acp/config.json",
13114
13291
  " hydra-acp daemon start [--foreground] Start daemon (detached by default; --foreground to attach)",
13115
13292
  " hydra-acp daemon stop|restart|status",
@@ -13128,9 +13305,9 @@ function printHelp() {
13128
13305
  " hydra-acp extensions logs <name> [-f] [-n N]Tail or follow an extension's log",
13129
13306
  " hydra-acp agents [list] List agents in the cached registry",
13130
13307
  " hydra-acp agents refresh Force a registry re-fetch",
13131
- " hydra-acp tui flags: [--resume [<id>]] [--new] [--agent <id>] [--model <id>] [--cwd <path>] [--name <label>]",
13132
- " --resume <id> attaches to a specific session; bare --resume picks the most-recent",
13133
- " in cwd. Smart default (no flags): picks if any live sessions exist, else new.",
13308
+ " hydra-acp tui flags: [--resume <id>] [--reattach] [--new] [--agent <id>] [--model <id>] [--cwd <path>] [--name <label>]",
13309
+ " --resume <id> attaches to a specific session; --reattach picks the most-recent in cwd.",
13310
+ " Smart default (no flags): shows a picker when sessions exist, else new.",
13134
13311
  " hydra-acp --version Print version",
13135
13312
  " hydra-acp --help Show this help",
13136
13313
  "",
package/dist/index.d.ts CHANGED
@@ -103,18 +103,21 @@ declare const HydraConfig: z.ZodObject<{
103
103
  mouse: z.ZodDefault<z.ZodBoolean>;
104
104
  logMaxBytes: z.ZodDefault<z.ZodNumber>;
105
105
  cwdColumnMaxWidth: z.ZodDefault<z.ZodNumber>;
106
+ progressIndicator: z.ZodDefault<z.ZodBoolean>;
106
107
  }, "strip", z.ZodTypeAny, {
107
108
  repaintThrottleMs: number;
108
109
  maxScrollbackLines: number;
109
110
  mouse: boolean;
110
111
  logMaxBytes: number;
111
112
  cwdColumnMaxWidth: number;
113
+ progressIndicator: boolean;
112
114
  }, {
113
115
  repaintThrottleMs?: number | undefined;
114
116
  maxScrollbackLines?: number | undefined;
115
117
  mouse?: boolean | undefined;
116
118
  logMaxBytes?: number | undefined;
117
119
  cwdColumnMaxWidth?: number | undefined;
120
+ progressIndicator?: boolean | undefined;
118
121
  }>>;
119
122
  }, "strip", z.ZodTypeAny, {
120
123
  daemon: {
@@ -142,6 +145,7 @@ declare const HydraConfig: z.ZodObject<{
142
145
  mouse: boolean;
143
146
  logMaxBytes: number;
144
147
  cwdColumnMaxWidth: number;
148
+ progressIndicator: boolean;
145
149
  };
146
150
  registry: {
147
151
  url: string;
@@ -177,6 +181,7 @@ declare const HydraConfig: z.ZodObject<{
177
181
  mouse?: boolean | undefined;
178
182
  logMaxBytes?: number | undefined;
179
183
  cwdColumnMaxWidth?: number | undefined;
184
+ progressIndicator?: boolean | undefined;
180
185
  } | undefined;
181
186
  registry?: {
182
187
  url?: string | undefined;
@@ -1330,6 +1335,8 @@ declare const SessionListEntry: z.ZodObject<{
1330
1335
  costAmount?: number | undefined;
1331
1336
  costCurrency?: string | undefined;
1332
1337
  }>>;
1338
+ importedFromMachine: z.ZodOptional<z.ZodString>;
1339
+ importedFromUpstreamSessionId: z.ZodOptional<z.ZodString>;
1333
1340
  updatedAt: z.ZodString;
1334
1341
  attachedClients: z.ZodNumber;
1335
1342
  status: z.ZodDefault<z.ZodEnum<["live", "cold"]>>;
@@ -1351,6 +1358,8 @@ declare const SessionListEntry: z.ZodObject<{
1351
1358
  costAmount?: number | undefined;
1352
1359
  costCurrency?: string | undefined;
1353
1360
  } | undefined;
1361
+ importedFromMachine?: string | undefined;
1362
+ importedFromUpstreamSessionId?: string | undefined;
1354
1363
  }, {
1355
1364
  cwd: string;
1356
1365
  sessionId: string;
@@ -1368,6 +1377,8 @@ declare const SessionListEntry: z.ZodObject<{
1368
1377
  costAmount?: number | undefined;
1369
1378
  costCurrency?: string | undefined;
1370
1379
  } | undefined;
1380
+ importedFromMachine?: string | undefined;
1381
+ importedFromUpstreamSessionId?: string | undefined;
1371
1382
  }>;
1372
1383
  type SessionListEntry = z.infer<typeof SessionListEntry>;
1373
1384
  declare const SessionListResult: z.ZodObject<{
@@ -1394,6 +1405,8 @@ declare const SessionListResult: z.ZodObject<{
1394
1405
  costAmount?: number | undefined;
1395
1406
  costCurrency?: string | undefined;
1396
1407
  }>>;
1408
+ importedFromMachine: z.ZodOptional<z.ZodString>;
1409
+ importedFromUpstreamSessionId: z.ZodOptional<z.ZodString>;
1397
1410
  updatedAt: z.ZodString;
1398
1411
  attachedClients: z.ZodNumber;
1399
1412
  status: z.ZodDefault<z.ZodEnum<["live", "cold"]>>;
@@ -1415,6 +1428,8 @@ declare const SessionListResult: z.ZodObject<{
1415
1428
  costAmount?: number | undefined;
1416
1429
  costCurrency?: string | undefined;
1417
1430
  } | undefined;
1431
+ importedFromMachine?: string | undefined;
1432
+ importedFromUpstreamSessionId?: string | undefined;
1418
1433
  }, {
1419
1434
  cwd: string;
1420
1435
  sessionId: string;
@@ -1432,6 +1447,8 @@ declare const SessionListResult: z.ZodObject<{
1432
1447
  costAmount?: number | undefined;
1433
1448
  costCurrency?: string | undefined;
1434
1449
  } | undefined;
1450
+ importedFromMachine?: string | undefined;
1451
+ importedFromUpstreamSessionId?: string | undefined;
1435
1452
  }>, "many">;
1436
1453
  nextCursor: z.ZodOptional<z.ZodString>;
1437
1454
  }, "strip", z.ZodTypeAny, {
@@ -1452,6 +1469,8 @@ declare const SessionListResult: z.ZodObject<{
1452
1469
  costAmount?: number | undefined;
1453
1470
  costCurrency?: string | undefined;
1454
1471
  } | undefined;
1472
+ importedFromMachine?: string | undefined;
1473
+ importedFromUpstreamSessionId?: string | undefined;
1455
1474
  }[];
1456
1475
  nextCursor?: string | undefined;
1457
1476
  }, {
@@ -1472,6 +1491,8 @@ declare const SessionListResult: z.ZodObject<{
1472
1491
  costAmount?: number | undefined;
1473
1492
  costCurrency?: string | undefined;
1474
1493
  } | undefined;
1494
+ importedFromMachine?: string | undefined;
1495
+ importedFromUpstreamSessionId?: string | undefined;
1475
1496
  }[];
1476
1497
  nextCursor?: string | undefined;
1477
1498
  }>;
@@ -1532,6 +1553,7 @@ declare class JsonRpcConnection {
1532
1553
  onRequest(method: string, handler: RequestHandler): void;
1533
1554
  setDefaultHandler(handler: RequestHandler): void;
1534
1555
  onNotification(method: string, handler: NotificationHandler): void;
1556
+ drainBuffered(method: string): void;
1535
1557
  onClose(handler: (err?: Error) => void): void;
1536
1558
  request<T = unknown>(method: string, params?: unknown): Promise<T>;
1537
1559
  requestWithId<T = unknown>(method: string, params?: unknown): {
@@ -1555,6 +1577,11 @@ interface AgentInstanceOptions {
1555
1577
  plan: SpawnPlan;
1556
1578
  extraEnv?: Record<string, string>;
1557
1579
  stderrTailBytes?: number;
1580
+ logger?: AgentLogger;
1581
+ }
1582
+ interface AgentLogger {
1583
+ info: (msg: string) => void;
1584
+ warn: (msg: string) => void;
1558
1585
  }
1559
1586
  declare class AgentInstance {
1560
1587
  readonly agentId: string;
@@ -1565,6 +1592,7 @@ declare class AgentInstance {
1565
1592
  private killed;
1566
1593
  private stderrTail;
1567
1594
  private stderrTailBytes;
1595
+ private logger?;
1568
1596
  private exitHandlers;
1569
1597
  private constructor();
1570
1598
  private formatFailure;
@@ -1635,6 +1663,10 @@ interface SessionInit {
1635
1663
  agentMeta?: Record<string, unknown>;
1636
1664
  agentArgs?: string[];
1637
1665
  idleTimeoutMs?: number;
1666
+ logger?: {
1667
+ info: (msg: string) => void;
1668
+ warn: (msg: string) => void;
1669
+ };
1638
1670
  spawnReplacementAgent?: SpawnReplacementAgent;
1639
1671
  historyStore?: HistoryStore;
1640
1672
  historyMaxEntries?: number;
@@ -1683,6 +1715,7 @@ declare class Session {
1683
1715
  private idleTimer;
1684
1716
  private lastRecordedAt;
1685
1717
  private spawnReplacementAgent;
1718
+ private logger;
1686
1719
  private agentChangeHandlers;
1687
1720
  private agentAdvertisedCommands;
1688
1721
  private agentCommandsHandlers;
@@ -1766,6 +1799,8 @@ declare const SessionRecord: z.ZodObject<{
1766
1799
  lineageId: z.ZodOptional<z.ZodString>;
1767
1800
  upstreamSessionId: z.ZodString;
1768
1801
  importedFromSessionId: z.ZodOptional<z.ZodString>;
1802
+ importedFromUpstreamSessionId: z.ZodOptional<z.ZodString>;
1803
+ importedFromMachine: z.ZodOptional<z.ZodString>;
1769
1804
  agentId: z.ZodString;
1770
1805
  cwd: z.ZodString;
1771
1806
  title: z.ZodOptional<z.ZodString>;
@@ -1818,6 +1853,8 @@ declare const SessionRecord: z.ZodObject<{
1818
1853
  costAmount?: number | undefined;
1819
1854
  costCurrency?: string | undefined;
1820
1855
  } | undefined;
1856
+ importedFromMachine?: string | undefined;
1857
+ importedFromUpstreamSessionId?: string | undefined;
1821
1858
  lineageId?: string | undefined;
1822
1859
  importedFromSessionId?: string | undefined;
1823
1860
  agentCommands?: {
@@ -1842,6 +1879,8 @@ declare const SessionRecord: z.ZodObject<{
1842
1879
  costAmount?: number | undefined;
1843
1880
  costCurrency?: string | undefined;
1844
1881
  } | undefined;
1882
+ importedFromMachine?: string | undefined;
1883
+ importedFromUpstreamSessionId?: string | undefined;
1845
1884
  lineageId?: string | undefined;
1846
1885
  importedFromSessionId?: string | undefined;
1847
1886
  agentCommands?: {
@@ -1874,6 +1913,7 @@ declare const Bundle: z.ZodObject<{
1874
1913
  session: z.ZodObject<{
1875
1914
  sessionId: z.ZodString;
1876
1915
  lineageId: z.ZodString;
1916
+ upstreamSessionId: z.ZodOptional<z.ZodString>;
1877
1917
  agentId: z.ZodString;
1878
1918
  cwd: z.ZodString;
1879
1919
  title: z.ZodOptional<z.ZodString>;
@@ -1914,6 +1954,7 @@ declare const Bundle: z.ZodObject<{
1914
1954
  updatedAt: string;
1915
1955
  lineageId: string;
1916
1956
  createdAt: string;
1957
+ upstreamSessionId?: string | undefined;
1917
1958
  title?: string | undefined;
1918
1959
  currentModel?: string | undefined;
1919
1960
  currentMode?: string | undefined;
@@ -1934,6 +1975,7 @@ declare const Bundle: z.ZodObject<{
1934
1975
  updatedAt: string;
1935
1976
  lineageId: string;
1936
1977
  createdAt: string;
1978
+ upstreamSessionId?: string | undefined;
1937
1979
  title?: string | undefined;
1938
1980
  currentModel?: string | undefined;
1939
1981
  currentMode?: string | undefined;
@@ -1976,6 +2018,7 @@ declare const Bundle: z.ZodObject<{
1976
2018
  updatedAt: string;
1977
2019
  lineageId: string;
1978
2020
  createdAt: string;
2021
+ upstreamSessionId?: string | undefined;
1979
2022
  title?: string | undefined;
1980
2023
  currentModel?: string | undefined;
1981
2024
  currentMode?: string | undefined;
@@ -2010,6 +2053,7 @@ declare const Bundle: z.ZodObject<{
2010
2053
  updatedAt: string;
2011
2054
  lineageId: string;
2012
2055
  createdAt: string;
2056
+ upstreamSessionId?: string | undefined;
2013
2057
  title?: string | undefined;
2014
2058
  currentModel?: string | undefined;
2015
2059
  currentMode?: string | undefined;
@@ -2059,6 +2103,7 @@ interface SessionManagerOptions {
2059
2103
  idleTimeoutMs?: number;
2060
2104
  defaultModels?: Record<string, string>;
2061
2105
  sessionHistoryMaxEntries?: number;
2106
+ logger?: AgentLogger;
2062
2107
  }
2063
2108
  declare class SessionManager {
2064
2109
  private registry;
@@ -2071,6 +2116,7 @@ declare class SessionManager {
2071
2116
  private defaultModels;
2072
2117
  private sessionHistoryMaxEntries;
2073
2118
  private metaWriteQueues;
2119
+ private logger?;
2074
2120
  constructor(registry: Registry, spawner?: AgentSpawner, store?: SessionStore, options?: SessionManagerOptions);
2075
2121
  create(params: CreateSessionParams): Promise<Session>;
2076
2122
  resurrect(params: ResurrectParams): Promise<Session>;
package/dist/index.js CHANGED
@@ -120,7 +120,13 @@ var TuiConfig = z.object({
120
120
  // Width cap on the cwd column in the `sessions list` output and the
121
121
  // TUI picker. Set higher if you keep deeply-nested working directories
122
122
  // and want them visible; the elastic title column shrinks to make room.
123
- cwdColumnMaxWidth: z.number().int().positive().default(24)
123
+ cwdColumnMaxWidth: z.number().int().positive().default(24),
124
+ // When true (default), emit OSC 9;4 progress-bar control codes so the
125
+ // host terminal can show an indeterminate busy indicator (taskbar pulse
126
+ // on Windows Terminal, dock badge on KDE/Konsole, etc.) while a turn is
127
+ // running. Set false if your terminal renders this obnoxiously or you
128
+ // just don't want it.
129
+ progressIndicator: z.boolean().default(true)
124
130
  });
125
131
  var ExtensionName = z.string().min(1).regex(/^[A-Za-z0-9._-]+$/, "extension name must be filename-safe");
126
132
  var ExtensionBody = z.object({
@@ -158,11 +164,12 @@ var HydraConfig = z.object({
158
164
  maxScrollbackLines: 1e4,
159
165
  mouse: true,
160
166
  logMaxBytes: 5 * 1024 * 1024,
161
- cwdColumnMaxWidth: 24
167
+ cwdColumnMaxWidth: 24,
168
+ progressIndicator: true
162
169
  })
163
170
  });
164
171
  var HydraConfigReadOnly = HydraConfig.extend({
165
- daemon: DaemonConfig.omit({ authToken: true })
172
+ daemon: DaemonConfig.omit({ authToken: true }).default({})
166
173
  });
167
174
  function extensionList(config) {
168
175
  return Object.entries(config.extensions).map(([name, body]) => ({
@@ -1013,6 +1020,11 @@ var SessionListEntry = z3.object({
1013
1020
  // Last-known usage snapshot so list views can show per-session cost
1014
1021
  // (and tokens, in callers that care) without resurrecting cold sessions.
1015
1022
  currentUsage: SessionListUsage.optional(),
1023
+ // Origin host (and origin upstream id) for imported sessions. Picker
1024
+ // uses the host to fill in the UPSTREAM cell pre-first-attach;
1025
+ // future "connect back to origin" callers would dial both.
1026
+ importedFromMachine: z3.string().optional(),
1027
+ importedFromUpstreamSessionId: z3.string().optional(),
1016
1028
  updatedAt: z3.string(),
1017
1029
  attachedClients: z3.number().int().nonnegative(),
1018
1030
  status: z3.enum(["live", "cold"]).default("live"),
@@ -1162,6 +1174,15 @@ var JsonRpcConnection = class _JsonRpcConnection {
1162
1174
  }
1163
1175
  }
1164
1176
  }
1177
+ // Discard any notifications buffered for the given method without firing
1178
+ // handlers. Used by the resurrect path to drop the agent's session/load
1179
+ // replay: that replay is the agent re-emitting our own history back at
1180
+ // us, and if we flushed it through wireAgent's session/update handler
1181
+ // every entry would be re-appended to history.jsonl, doubling the log
1182
+ // each time the session was woken up.
1183
+ drainBuffered(method) {
1184
+ this.bufferedNotifications.delete(method);
1185
+ }
1165
1186
  onClose(handler) {
1166
1187
  this.closeHandlers.push(handler);
1167
1188
  }
@@ -1314,12 +1335,14 @@ var AgentInstance = class _AgentInstance {
1314
1335
  killed = false;
1315
1336
  stderrTail = "";
1316
1337
  stderrTailBytes;
1338
+ logger;
1317
1339
  exitHandlers = [];
1318
1340
  constructor(opts, child) {
1319
1341
  this.agentId = opts.agentId;
1320
1342
  this.cwd = opts.cwd;
1321
1343
  this.child = child;
1322
1344
  this.stderrTailBytes = opts.stderrTailBytes ?? DEFAULT_STDERR_TAIL_BYTES;
1345
+ this.logger = opts.logger;
1323
1346
  if (!child.stdout || !child.stdin) {
1324
1347
  throw new Error("agent subprocess missing stdio");
1325
1348
  }
@@ -1328,7 +1351,15 @@ var AgentInstance = class _AgentInstance {
1328
1351
  child.stderr?.setEncoding("utf8");
1329
1352
  child.stderr?.on("data", (chunk) => {
1330
1353
  this.stderrTail = (this.stderrTail + chunk).slice(-this.stderrTailBytes);
1331
- process.stderr.write(`[${opts.agentId}] ${chunk}`);
1354
+ if (this.logger) {
1355
+ for (const line of chunk.split(/\r?\n/)) {
1356
+ if (line.length > 0) {
1357
+ this.logger.info(`[${opts.agentId}] ${line}`);
1358
+ }
1359
+ }
1360
+ } else {
1361
+ process.stderr.write(`[${opts.agentId}] ${chunk}`);
1362
+ }
1332
1363
  });
1333
1364
  child.on("error", (err) => {
1334
1365
  const msg = this.formatFailure(err.message);
@@ -1336,9 +1367,16 @@ var AgentInstance = class _AgentInstance {
1336
1367
  });
1337
1368
  child.on("exit", (code, signal) => {
1338
1369
  this.exited = true;
1339
- if (!this.killed) {
1370
+ if (this.killed) {
1371
+ this.logger?.info(
1372
+ `agent ${opts.agentId} pid=${child.pid} exited after kill code=${code} signal=${signal}`
1373
+ );
1374
+ } else {
1340
1375
  const reason = `agent ${opts.agentId} exited before responding (code=${code} signal=${signal})`;
1341
1376
  this.connection.fail(new Error(this.formatFailure(reason)));
1377
+ this.logger?.warn(
1378
+ `agent ${opts.agentId} pid=${child.pid} exited unexpectedly code=${code} signal=${signal}`
1379
+ );
1342
1380
  }
1343
1381
  for (const handler of this.exitHandlers) {
1344
1382
  handler(code, signal);
@@ -1359,7 +1397,15 @@ stderr: ${tail}` : reason;
1359
1397
  const child = spawn3(opts.plan.command, opts.plan.args, {
1360
1398
  cwd: opts.cwd,
1361
1399
  env,
1362
- stdio: ["pipe", "pipe", "pipe"]
1400
+ stdio: ["pipe", "pipe", "pipe"],
1401
+ // setsid the agent into its own session/process group. The daemon
1402
+ // already runs in its own setsid'd session, but macOS terminals
1403
+ // (iTerm2, Terminal.app) sometimes still reach inherited child
1404
+ // processes when the user closes a window — putting the agent
1405
+ // one more session-boundary away keeps it alive across terminal
1406
+ // restarts. The daemon still owns the pipes, so this.kill()
1407
+ // continues to terminate it cleanly on idle/close.
1408
+ detached: true
1363
1409
  });
1364
1410
  return new _AgentInstance(opts, child);
1365
1411
  }
@@ -1374,6 +1420,9 @@ stderr: ${tail}` : reason;
1374
1420
  return;
1375
1421
  }
1376
1422
  this.killed = true;
1423
+ this.logger?.info(
1424
+ `agent ${this.agentId} pid=${this.child.pid} kill requested signal=${signal}`
1425
+ );
1377
1426
  await this.connection.close().catch(() => void 0);
1378
1427
  this.child.kill(signal);
1379
1428
  }
@@ -1486,6 +1535,7 @@ var Session = class {
1486
1535
  // and noisy state churn keep a quiet session alive forever.
1487
1536
  lastRecordedAt;
1488
1537
  spawnReplacementAgent;
1538
+ logger;
1489
1539
  agentChangeHandlers = [];
1490
1540
  // Last available_commands_update we observed from the agent. Stored
1491
1541
  // so we can re-broadcast a merged (hydra ∪ agent) list whenever
@@ -1517,6 +1567,7 @@ var Session = class {
1517
1567
  }
1518
1568
  this.idleTimeoutMs = init.idleTimeoutMs ?? 0;
1519
1569
  this.spawnReplacementAgent = init.spawnReplacementAgent;
1570
+ this.logger = init.logger;
1520
1571
  if (init.firstPromptSeeded) {
1521
1572
  this.firstPromptSeeded = true;
1522
1573
  }
@@ -1846,6 +1897,9 @@ var Session = class {
1846
1897
  if (this.closed) {
1847
1898
  return;
1848
1899
  }
1900
+ this.logger?.info(
1901
+ `session ${this.sessionId} closing deleteRecord=${opts.deleteRecord ?? false} regenTitle=${opts.regenTitle ?? false}`
1902
+ );
1849
1903
  this.cancelIdleTimer();
1850
1904
  if (opts.regenTitle && this.firstPromptSeeded) {
1851
1905
  const timeoutMs = opts.regenTitleTimeoutMs ?? 5e3;
@@ -2400,6 +2454,10 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
2400
2454
  return;
2401
2455
  }
2402
2456
  const opts = this.firstPromptSeeded ? { deleteRecord: false, regenTitle: true } : { deleteRecord: true };
2457
+ const idleSec = Math.round(idle / 1e3);
2458
+ this.logger?.info(
2459
+ `session ${this.sessionId} idle timeout fired after ${idleSec}s (window=${Math.round(this.idleTimeoutMs / 1e3)}s) \u2014 closing`
2460
+ );
2403
2461
  void this.close(opts).catch(() => void 0);
2404
2462
  }
2405
2463
  cancelIdleTimer() {
@@ -2768,6 +2826,16 @@ var SessionRecord = z4.object({
2768
2826
  // origin's local id at export time, kept for debuggability and as a
2769
2827
  // breadcrumb in `sessions list` (informational, not used for routing).
2770
2828
  importedFromSessionId: z4.string().optional(),
2829
+ // Origin's agent-side session id at export time. Carried as a
2830
+ // breadcrumb and as the handle a future "connect back to origin"
2831
+ // feature would dial. Absent when the origin record had no upstream
2832
+ // bound (re-export of an imported, not-yet-attached session).
2833
+ importedFromUpstreamSessionId: z4.string().optional(),
2834
+ // Hostname of the machine that exported the bundle we imported
2835
+ // (i.e. the most recent hop, not necessarily the true multi-hop
2836
+ // origin). Surfaced in the picker so imported rows don't look like
2837
+ // they materialized from nowhere.
2838
+ importedFromMachine: z4.string().optional(),
2771
2839
  agentId: z4.string(),
2772
2840
  cwd: z4.string(),
2773
2841
  title: z4.string().optional(),
@@ -2888,6 +2956,8 @@ function recordFromMemorySession(args) {
2888
2956
  lineageId: args.lineageId,
2889
2957
  upstreamSessionId: args.upstreamSessionId,
2890
2958
  importedFromSessionId: args.importedFromSessionId,
2959
+ importedFromUpstreamSessionId: args.importedFromUpstreamSessionId,
2960
+ importedFromMachine: args.importedFromMachine,
2891
2961
  agentId: args.agentId,
2892
2962
  cwd: args.cwd,
2893
2963
  title: args.title,
@@ -3105,6 +3175,7 @@ var SessionManager = class {
3105
3175
  this.histories = new HistoryStore({ maxEntries: this.sessionHistoryMaxEntries });
3106
3176
  this.idleTimeoutMs = options.idleTimeoutMs ?? 0;
3107
3177
  this.defaultModels = options.defaultModels ?? {};
3178
+ this.logger = options.logger;
3108
3179
  }
3109
3180
  registry;
3110
3181
  sessions = /* @__PURE__ */ new Map();
@@ -3119,6 +3190,7 @@ var SessionManager = class {
3119
3190
  // concurrent snapshot updates (e.g. an agent emitting model + mode
3120
3191
  // back-to-back) don't lose writes via interleaved reads.
3121
3192
  metaWriteQueues = /* @__PURE__ */ new Map();
3193
+ logger;
3122
3194
  async create(params) {
3123
3195
  const fresh = await this.bootstrapAgent({
3124
3196
  agentId: params.agentId,
@@ -3136,6 +3208,7 @@ var SessionManager = class {
3136
3208
  title: params.title,
3137
3209
  agentArgs: params.agentArgs,
3138
3210
  idleTimeoutMs: this.idleTimeoutMs,
3211
+ logger: this.logger,
3139
3212
  spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
3140
3213
  historyStore: this.histories,
3141
3214
  historyMaxEntries: this.sessionHistoryMaxEntries,
@@ -3218,6 +3291,7 @@ var SessionManager = class {
3218
3291
  await agent.kill().catch(() => void 0);
3219
3292
  return this.doResurrectFromImport(params);
3220
3293
  }
3294
+ agent.connection.drainBuffered("session/update");
3221
3295
  const session = new Session({
3222
3296
  sessionId: params.hydraSessionId,
3223
3297
  cwd: params.cwd,
@@ -3228,6 +3302,7 @@ var SessionManager = class {
3228
3302
  title: params.title,
3229
3303
  agentArgs: params.agentArgs,
3230
3304
  idleTimeoutMs: this.idleTimeoutMs,
3305
+ logger: this.logger,
3231
3306
  spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
3232
3307
  historyStore: this.histories,
3233
3308
  historyMaxEntries: this.sessionHistoryMaxEntries,
@@ -3274,6 +3349,7 @@ var SessionManager = class {
3274
3349
  title: params.title,
3275
3350
  agentArgs: params.agentArgs,
3276
3351
  idleTimeoutMs: this.idleTimeoutMs,
3352
+ logger: this.logger,
3277
3353
  spawnReplacementAgent: (p) => this.bootstrapAgent({ ...p, mcpServers: [] }),
3278
3354
  historyStore: this.histories,
3279
3355
  historyMaxEntries: this.sessionHistoryMaxEntries,
@@ -3538,6 +3614,8 @@ var SessionManager = class {
3538
3614
  agentId: r.agentId,
3539
3615
  currentModel: r.currentModel,
3540
3616
  currentUsage: r.currentUsage,
3617
+ importedFromMachine: r.importedFromMachine,
3618
+ importedFromUpstreamSessionId: r.importedFromUpstreamSessionId,
3541
3619
  updatedAt: used,
3542
3620
  attachedClients: 0,
3543
3621
  status: "cold"
@@ -3645,6 +3723,8 @@ var SessionManager = class {
3645
3723
  lineageId: args.bundle.session.lineageId,
3646
3724
  upstreamSessionId: "",
3647
3725
  importedFromSessionId: args.bundle.session.sessionId,
3726
+ importedFromUpstreamSessionId: args.bundle.session.upstreamSessionId,
3727
+ importedFromMachine: args.bundle.exportedFrom.machine,
3648
3728
  agentId: args.bundle.session.agentId,
3649
3729
  cwd: args.cwd ?? args.bundle.session.cwd,
3650
3730
  title: args.bundle.session.title,
@@ -3767,6 +3847,8 @@ function mergeForPersistence(session, existing) {
3767
3847
  lineageId: existing?.lineageId ?? generateLineageId(),
3768
3848
  upstreamSessionId: session.upstreamSessionId,
3769
3849
  importedFromSessionId: existing?.importedFromSessionId,
3850
+ importedFromUpstreamSessionId: existing?.importedFromUpstreamSessionId,
3851
+ importedFromMachine: existing?.importedFromMachine,
3770
3852
  agentId: session.agentId,
3771
3853
  cwd: session.cwd,
3772
3854
  title: session.title,
@@ -4352,6 +4434,12 @@ var BundleSession = z5.object({
4352
4434
  // Required on bundles — the export path backfills if the source
4353
4435
  // record was written before lineageId existed.
4354
4436
  lineageId: z5.string(),
4437
+ // The exporter's agent-side session id at export time. Carried so
4438
+ // importers can persist it as a breadcrumb (and, eventually, as the
4439
+ // handle a "connect back to origin" feature would need). Omitted on
4440
+ // bundles whose source record never bound to an agent (e.g. a
4441
+ // re-export of an imported, not-yet-attached session).
4442
+ upstreamSessionId: z5.string().optional(),
4355
4443
  agentId: z5.string(),
4356
4444
  cwd: z5.string(),
4357
4445
  title: z5.string().optional(),
@@ -4384,6 +4472,7 @@ function encodeBundle(params) {
4384
4472
  session: {
4385
4473
  sessionId: params.record.sessionId,
4386
4474
  lineageId: params.record.lineageId,
4475
+ ...params.record.upstreamSessionId ? { upstreamSessionId: params.record.upstreamSessionId } : {},
4387
4476
  agentId: params.record.agentId,
4388
4477
  cwd: params.record.cwd,
4389
4478
  ...params.record.title !== void 0 ? { title: params.record.title } : {},
@@ -5175,11 +5264,20 @@ async function startDaemon(config) {
5175
5264
  await auth(request, reply);
5176
5265
  });
5177
5266
  const registry = new Registry(config);
5178
- const spawner = (opts) => AgentInstance.spawn({ ...opts, stderrTailBytes: config.daemon.agentStderrTailBytes });
5267
+ const agentLogger = {
5268
+ info: (msg) => app.log.info(msg),
5269
+ warn: (msg) => app.log.warn(msg)
5270
+ };
5271
+ const spawner = (opts) => AgentInstance.spawn({
5272
+ ...opts,
5273
+ stderrTailBytes: config.daemon.agentStderrTailBytes,
5274
+ logger: agentLogger
5275
+ });
5179
5276
  const manager = new SessionManager(registry, spawner, void 0, {
5180
5277
  idleTimeoutMs: config.daemon.sessionIdleTimeoutSeconds * 1e3,
5181
5278
  defaultModels: config.defaultModels,
5182
- sessionHistoryMaxEntries: config.daemon.sessionHistoryMaxEntries
5279
+ sessionHistoryMaxEntries: config.daemon.sessionHistoryMaxEntries,
5280
+ logger: agentLogger
5183
5281
  });
5184
5282
  const extensions = new ExtensionManager(extensionList(config));
5185
5283
  registerHealthRoutes(app, HYDRA_VERSION);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hydra-acp/cli",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "Multi-client ACP session daemon: spawn agents, attach over WSS, multiplex sessions across editors.",
5
5
  "license": "MIT",
6
6
  "type": "module",