@songsid/agend 2.1.4-beta.1 → 2.1.4-beta.11

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.
Files changed (107) hide show
  1. package/dist/backend/antigravity.d.ts +1 -0
  2. package/dist/backend/antigravity.js +1 -0
  3. package/dist/backend/antigravity.js.map +1 -1
  4. package/dist/backend/claude-code.d.ts +1 -0
  5. package/dist/backend/claude-code.js +24 -2
  6. package/dist/backend/claude-code.js.map +1 -1
  7. package/dist/backend/codex.d.ts +1 -0
  8. package/dist/backend/codex.js +43 -4
  9. package/dist/backend/codex.js.map +1 -1
  10. package/dist/backend/gemini-cli.d.ts +1 -0
  11. package/dist/backend/gemini-cli.js +1 -0
  12. package/dist/backend/gemini-cli.js.map +1 -1
  13. package/dist/backend/grok.d.ts +1 -0
  14. package/dist/backend/grok.js +1 -0
  15. package/dist/backend/grok.js.map +1 -1
  16. package/dist/backend/kiro.d.ts +1 -0
  17. package/dist/backend/kiro.js +1 -0
  18. package/dist/backend/kiro.js.map +1 -1
  19. package/dist/backend/mock.d.ts +1 -0
  20. package/dist/backend/mock.js +1 -0
  21. package/dist/backend/mock.js.map +1 -1
  22. package/dist/backend/opencode.d.ts +40 -0
  23. package/dist/backend/opencode.js +81 -1
  24. package/dist/backend/opencode.js.map +1 -1
  25. package/dist/backend/types.d.ts +10 -0
  26. package/dist/backend/types.js +7 -0
  27. package/dist/backend/types.js.map +1 -1
  28. package/dist/channel/adapters/discord.js +24 -7
  29. package/dist/channel/adapters/discord.js.map +1 -1
  30. package/dist/channel/mcp-server.js +6 -20
  31. package/dist/channel/mcp-server.js.map +1 -1
  32. package/dist/channel/mcp-tools.js +1 -1
  33. package/dist/channel/mcp-tools.js.map +1 -1
  34. package/dist/channel/types.d.ts +1 -1
  35. package/dist/classic-channel-manager.d.ts +34 -3
  36. package/dist/classic-channel-manager.js +190 -31
  37. package/dist/classic-channel-manager.js.map +1 -1
  38. package/dist/cli.js +65 -13
  39. package/dist/cli.js.map +1 -1
  40. package/dist/completion-install.d.ts +70 -0
  41. package/dist/completion-install.js +152 -0
  42. package/dist/completion-install.js.map +1 -0
  43. package/dist/completion.d.ts +7 -1
  44. package/dist/completion.js +14 -4
  45. package/dist/completion.js.map +1 -1
  46. package/dist/config-validator.js +35 -7
  47. package/dist/config-validator.js.map +1 -1
  48. package/dist/config.js +1 -0
  49. package/dist/config.js.map +1 -1
  50. package/dist/daemon.d.ts +53 -0
  51. package/dist/daemon.js +229 -40
  52. package/dist/daemon.js.map +1 -1
  53. package/dist/fleet-context.d.ts +8 -0
  54. package/dist/fleet-manager.d.ts +143 -3
  55. package/dist/fleet-manager.js +1109 -290
  56. package/dist/fleet-manager.js.map +1 -1
  57. package/dist/general-knowledge/skills/backend-providers/SKILL.md +114 -0
  58. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +3 -1
  59. package/dist/general-knowledge/skills/delegation-playbook/SKILL.md +52 -0
  60. package/dist/general-knowledge/skills/development-workflow/SKILL.md +28 -0
  61. package/dist/general-knowledge/skills/fleet-config/SKILL.md +1 -0
  62. package/dist/general-knowledge/skills/fleet-health/SKILL.md +1 -0
  63. package/dist/general-knowledge/skills/fleet-restart/SKILL.md +1 -0
  64. package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +1 -0
  65. package/dist/general-knowledge/skills/model-discovery/SKILL.md +1 -0
  66. package/dist/general-knowledge/skills/multi-channel/SKILL.md +1 -0
  67. package/dist/general-knowledge/skills/scheduling/SKILL.md +1 -0
  68. package/dist/general-knowledge/skills/session-management/SKILL.md +1 -0
  69. package/dist/general-knowledge/skills/tui-effort/SKILL.md +1 -0
  70. package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +30 -0
  71. package/dist/instance-lifecycle.d.ts +10 -0
  72. package/dist/instance-lifecycle.js +50 -33
  73. package/dist/instance-lifecycle.js.map +1 -1
  74. package/dist/instructions.d.ts +12 -0
  75. package/dist/instructions.js +33 -0
  76. package/dist/instructions.js.map +1 -1
  77. package/dist/locale.js +130 -0
  78. package/dist/locale.js.map +1 -1
  79. package/dist/outbound-schemas.d.ts +2 -1
  80. package/dist/outbound-schemas.js +1 -0
  81. package/dist/outbound-schemas.js.map +1 -1
  82. package/dist/quickstart.js +25 -0
  83. package/dist/quickstart.js.map +1 -1
  84. package/dist/restart-progress.d.ts +7 -1
  85. package/dist/restart-progress.js +27 -6
  86. package/dist/restart-progress.js.map +1 -1
  87. package/dist/tool-progress.d.ts +40 -0
  88. package/dist/tool-progress.js +289 -0
  89. package/dist/tool-progress.js.map +1 -0
  90. package/dist/topic-commands.d.ts +26 -0
  91. package/dist/topic-commands.js +117 -1
  92. package/dist/topic-commands.js.map +1 -1
  93. package/dist/transcript-monitor.d.ts +15 -2
  94. package/dist/transcript-monitor.js +63 -17
  95. package/dist/transcript-monitor.js.map +1 -1
  96. package/dist/transcript-sources.d.ts +106 -0
  97. package/dist/transcript-sources.js +428 -0
  98. package/dist/transcript-sources.js.map +1 -0
  99. package/dist/types.d.ts +8 -0
  100. package/dist/update-marker.d.ts +30 -0
  101. package/dist/update-marker.js +71 -20
  102. package/dist/update-marker.js.map +1 -1
  103. package/dist/update-progress.d.ts +6 -0
  104. package/dist/update-progress.js +29 -0
  105. package/dist/update-progress.js.map +1 -0
  106. package/dist/workflow-templates/default.md +2 -1
  107. package/package.json +2 -2
package/dist/daemon.js CHANGED
@@ -8,6 +8,8 @@ import { mcpServerState } from "./mcp-liveness.js";
8
8
  import { clearPausedMarker, writePausedMarker } from "./pause-marker.js";
9
9
  import { TmuxManager, resolveTmuxLogicalSize } from "./tmux-manager.js";
10
10
  import { TranscriptMonitor } from "./transcript-monitor.js";
11
+ import { createTranscriptSource } from "./transcript-sources.js";
12
+ import { ProgressAccumulator, summarizeProgress } from "./tool-progress.js";
11
13
  import { ContextGuardian } from "./context-guardian.js";
12
14
  import { IpcServer } from "./channel/ipc-bridge.js";
13
15
  import { daemonBudgetMs } from "./channel/ipc-timeouts.js";
@@ -665,6 +667,7 @@ export class Daemon extends EventEmitter {
665
667
  instanceStateReadyPattern = null;
666
668
  instanceStateBusyPattern = null;
667
669
  instanceStateMonitorActive = false;
670
+ sessionCheckpointWarningEmitted = false;
668
671
  statePollInFlight = false;
669
672
  // ── Dead-MCP proxy reply: per-turn evidence ─────────────────────────────
670
673
  // Set when an inbound message lands in the CLI; cleared on the idle edge that
@@ -710,6 +713,17 @@ export class Daemon extends EventEmitter {
710
713
  lastBuiltInstructions = "";
711
714
  pasteQueueDepth = 0;
712
715
  firstDeliveryDelay = new FirstDeliveryDelay();
716
+ /** Orders /steer pastes against each other. Deliberately NOT pasteLock:
717
+ * a steer must not queue behind the normal deliveries it exists to
718
+ * overtake. Pane-level exclusion still comes from paneWriteLock inside
719
+ * deliverMessage, so a steer and a normal delivery can never interleave
720
+ * their PTY writes — the steer just doesn't wait for idle. */
721
+ steerLock = Promise.resolve();
722
+ /** Tool lines shown in the channel processing bubble for the current turn. */
723
+ turnProgress = new ProgressAccumulator();
724
+ lastProgressBroadcast = "";
725
+ progressBroadcastTimer = null;
726
+ lastProgressBroadcastAt = 0;
713
727
  // PTY error pattern monitoring
714
728
  errorMonitorTimer = null;
715
729
  interactivePromptDetector = new InteractivePromptDetector();
@@ -929,6 +943,10 @@ export class Daemon extends EventEmitter {
929
943
  });
930
944
  }
931
945
  }
946
+ else if (msg.type === "steer") {
947
+ const meta = (msg.meta ?? {});
948
+ this.steerMessage(msg.content, meta);
949
+ }
932
950
  else if (msg.type === "fleet_schedule_trigger") {
933
951
  const payload = msg.payload;
934
952
  const meta = msg.meta;
@@ -1032,8 +1050,11 @@ export class Daemon extends EventEmitter {
1032
1050
  const outputLog = join(this.instanceDir, "output.log");
1033
1051
  rotateLogIfNeeded(outputLog);
1034
1052
  await this.tmux.pipeOutput(outputLog).catch(() => { });
1035
- // 4. Transcript monitor
1036
- this.transcriptMonitor = new TranscriptMonitor(this.instanceDir, this.logger);
1053
+ // 4. Transcript monitor. claude-code is handled inside the monitor
1054
+ // (statusline transcript); codex/kiro/opencode read their CLI's own
1055
+ // conversation store via a pluggable source. Backends with no known
1056
+ // source stay inert exactly as before.
1057
+ this.transcriptMonitor = new TranscriptMonitor(this.instanceDir, this.logger, createTranscriptSource(this.config.backend ?? "claude-code", this.config.working_directory));
1037
1058
  // 5. Wire transcript events
1038
1059
  const ackIfPending = () => {
1039
1060
  if (!this.pendingAckMessage || !this.adapter)
@@ -1049,6 +1070,7 @@ export class Daemon extends EventEmitter {
1049
1070
  this.recordRecentEvent({ type: "tool_use", name, preview: this.summarizeTool(name, input) });
1050
1071
  this.recordRecentToolActivity(this.summarizeTool(name, input));
1051
1072
  this.publishActivity(this.summarizeTool(name, input));
1073
+ this.recordToolProgress(name, input);
1052
1074
  });
1053
1075
  this.transcriptMonitor.on("tool_result", (name, _output) => {
1054
1076
  this.recordRecentEvent({ type: "tool_result", name });
@@ -1260,7 +1282,7 @@ export class Daemon extends EventEmitter {
1260
1282
  this.logger.info("CLI exited normally (code 0) — pausing health check");
1261
1283
  await this.tmux.killWindow();
1262
1284
  this.healthCheckPaused = true;
1263
- this.emitSupervisionEnded("the CLI exited normally (code 0)", "Nothing crashed — start it again when you need it.");
1285
+ this.emitSupervisionEnded("the CLI exited normally (code 0)", "Nothing crashed — start it again when you need it.", 0);
1264
1286
  return;
1265
1287
  }
1266
1288
  // Distinguish tmux server crash from single window crash.
@@ -1357,6 +1379,17 @@ export class Daemon extends EventEmitter {
1357
1379
  catch (err) {
1358
1380
  this.logger.error({ err: err.message }, "Recovery from background session conflict failed");
1359
1381
  }
1382
+ // Keep the loop alive: every health-check exit must either
1383
+ // scheduleNext() or deliberately set healthCheckPaused. This one
1384
+ // did neither, so a recovered instance ran UNMONITORED (while
1385
+ // isHealthCheckEffectivelyPaused still said monitoring was on)
1386
+ // until the next pause→wake cycle or fleet restart. The next tick
1387
+ // does the right thing in both outcomes: a healthy new window
1388
+ // passes the alive check, and a failed recovery hits the dead
1389
+ // window again with backgroundSessionRecoveryAttempted set — so
1390
+ // it falls through to the normal crash handling (crash counting,
1391
+ // backoff, supervision_ended when retries are exhausted).
1392
+ scheduleNext();
1360
1393
  return; // Don't count as crash
1361
1394
  }
1362
1395
  // Already attempted recovery — fall through to normal crash handling
@@ -1506,8 +1539,13 @@ export class Daemon extends EventEmitter {
1506
1539
  * `crash_loop` already had this treatment; this is the same bridge for the other
1507
1540
  * four, carrying a human-readable cause and the operator's next step.
1508
1541
  */
1509
- emitSupervisionEnded(reason, remedy) {
1510
- this.emit("supervision_ended", { name: this.name, reason, remedy });
1542
+ emitSupervisionEnded(reason, remedy, exitCode) {
1543
+ this.emit("supervision_ended", {
1544
+ name: this.name,
1545
+ reason,
1546
+ remedy,
1547
+ ...(exitCode !== undefined ? { exitCode } : {}),
1548
+ });
1511
1549
  }
1512
1550
  setProcessStatus(status) {
1513
1551
  if (this.processStatus === status)
@@ -1988,6 +2026,14 @@ export class Daemon extends EventEmitter {
1988
2026
  applyInstanceStateSnapshot(snapshot, pane) {
1989
2027
  const previous = this.instanceState;
1990
2028
  this.instanceState = snapshot.state;
2029
+ // OpenCode creates its session lazily on the first submitted message.
2030
+ // Waiting until stop/pause to persist that id loses resume state when the
2031
+ // fleet is SIGKILLed or the host reboots.
2032
+ // Idle observations are safe checkpoints; the 60s safety sweep also gives
2033
+ // us a bounded retry if session creation produced no visible state edge.
2034
+ if (snapshot.state === "idle" && this.backend?.binaryName === "opencode") {
2035
+ this.saveSessionId();
2036
+ }
1991
2037
  // Only a transition back to idle completes pending work. Repeated idle
1992
2038
  // observations between enqueue and paste must not clear a newer inbound.
1993
2039
  if (snapshot.state === "idle" && previous !== "idle") {
@@ -1996,6 +2042,7 @@ export class Daemon extends EventEmitter {
1996
2042
  // tool_result (interrupted, crashed, cancelled), which would otherwise leave
1997
2043
  // the last tool pinned to the progress line for the rest of the session.
1998
2044
  this.publishActivity(null);
2045
+ this.resetToolProgress();
1999
2046
  // Must run before the mcpRestartPending branch below: the pane text is the
2000
2047
  // only copy of the answer, and the revival restart is about to clear it.
2001
2048
  this.maybeProxyReplyOnTurnEnd(pane);
@@ -2393,6 +2440,69 @@ export class Daemon extends EventEmitter {
2393
2440
  activity: next,
2394
2441
  });
2395
2442
  }
2443
+ /** Effective tool_progress level, hardened against junk config values.
2444
+ * Off unless explicitly enabled: an update must never make bubbles start
2445
+ * listing tool activity the operator did not ask to broadcast. */
2446
+ toolProgressLevel() {
2447
+ const raw = this.config.tool_progress;
2448
+ return raw === "standard" || raw === "verbose" ? raw : "off";
2449
+ }
2450
+ /**
2451
+ * Accumulate one semantic progress line for the channel bubble and schedule
2452
+ * a coalesced broadcast. Separate from publishActivity on purpose: that one
2453
+ * is the single-line statusline detail (terse, operator-facing), this is the
2454
+ * multi-line channel list (semantic, argument-free at `standard`) — see
2455
+ * tool-progress.ts for why the two labellers must not merge.
2456
+ */
2457
+ recordToolProgress(name, input) {
2458
+ const level = this.toolProgressLevel();
2459
+ if (level === "off")
2460
+ return;
2461
+ if (this.turnProgress.add(summarizeProgress(name, input, level))) {
2462
+ this.scheduleProgressBroadcast();
2463
+ }
2464
+ }
2465
+ /** Coalesce progress broadcasts: at most one per 3s, trailing edge kept. */
2466
+ scheduleProgressBroadcast() {
2467
+ const MIN_INTERVAL_MS = 3_000;
2468
+ const since = Date.now() - this.lastProgressBroadcastAt;
2469
+ if (since >= MIN_INTERVAL_MS) {
2470
+ this.broadcastToolProgress();
2471
+ return;
2472
+ }
2473
+ if (this.progressBroadcastTimer)
2474
+ return;
2475
+ this.progressBroadcastTimer = setTimeout(() => {
2476
+ this.progressBroadcastTimer = null;
2477
+ this.broadcastToolProgress();
2478
+ }, MIN_INTERVAL_MS - since);
2479
+ this.progressBroadcastTimer.unref?.();
2480
+ }
2481
+ broadcastToolProgress() {
2482
+ const rendered = this.turnProgress.render();
2483
+ if (rendered === this.lastProgressBroadcast)
2484
+ return;
2485
+ this.lastProgressBroadcast = rendered;
2486
+ this.lastProgressBroadcastAt = Date.now();
2487
+ this.ipcServer?.broadcast({
2488
+ type: "instance_progress",
2489
+ instanceName: this.name,
2490
+ progress: rendered,
2491
+ });
2492
+ }
2493
+ /** New turn (or turn over): drop the list and tell the fleet to clear it. */
2494
+ resetToolProgress() {
2495
+ if (this.progressBroadcastTimer) {
2496
+ clearTimeout(this.progressBroadcastTimer);
2497
+ this.progressBroadcastTimer = null;
2498
+ }
2499
+ this.turnProgress.reset();
2500
+ if (this.lastProgressBroadcast !== "") {
2501
+ this.lastProgressBroadcast = "";
2502
+ this.lastProgressBroadcastAt = Date.now();
2503
+ this.ipcServer?.broadcast({ type: "instance_progress", instanceName: this.name, progress: "" });
2504
+ }
2505
+ }
2396
2506
  /**
2397
2507
  * Options for every system-initiated paste (startup notice, session snapshot,
2398
2508
  * runtime-dialog keys).
@@ -2428,6 +2538,82 @@ export class Daemon extends EventEmitter {
2428
2538
  return ""; // skip channel tools
2429
2539
  return name;
2430
2540
  }
2541
+ /**
2542
+ * The one place an inbound message grows its metadata wrapper ([user:]/
2543
+ * [from:] prefix, pending reactions, handoff metadata, reply instructions).
2544
+ * Both the normal queued path (pushChannelMessage) and /steer go through
2545
+ * here — a steered message must read EXACTLY like a queued one to the
2546
+ * agent, or the two drift apart in what the agent is told about replying.
2547
+ */
2548
+ formatInboundMessage(content, meta) {
2549
+ const user = meta.user || "unknown";
2550
+ const fromInstance = meta.from_instance;
2551
+ let formatted;
2552
+ if (fromInstance) {
2553
+ // #77: show the sender's display name for readability, keeping the machine
2554
+ // instance name in parens so the recipient's send_to_instance target is valid.
2555
+ const fromLabel = meta.from_display ? `${meta.from_display} (${fromInstance})` : fromInstance;
2556
+ formatted = `[from:${fromLabel}] ${content}`;
2557
+ formatted += renderHandoffMetadata(meta);
2558
+ // A delegated task that requires a reply must not read like a chatty FYI —
2559
+ // the "you may stay silent" line is for the latter only.
2560
+ formatted += meta.requires_reply === "true"
2561
+ ? "\n(A reply IS required: use report_result with the correlation_id above — or send_to_instance. Not direct text.)"
2562
+ : "\n(If you need to reply, use send_to_instance tool, NOT direct text. If there is nothing to add, you may stay silent.)";
2563
+ }
2564
+ else {
2565
+ const via = meta.source ? ` via ${meta.source}` : "";
2566
+ const idTag = meta.user_id ? `, id:${meta.user_id}` : "";
2567
+ formatted = `[user:${user}${via}${idTag}] ${content}`;
2568
+ // Reactions queued since the last real message (#432). One leading line of
2569
+ // context, present only when something is pending — a reaction no longer
2570
+ // costs a turn of its own.
2571
+ if (meta.pending_reactions) {
2572
+ formatted = `[Recent reactions: ${meta.pending_reactions}]\n${formatted}`;
2573
+ }
2574
+ formatted += renderHandoffMetadata(meta);
2575
+ formatted += "\n(Reply using the reply tool — do NOT respond with direct text)";
2576
+ }
2577
+ if (meta.reply_to_text) {
2578
+ formatted += `\n(reply_to: "${meta.reply_to_text}")`;
2579
+ }
2580
+ return formatted;
2581
+ }
2582
+ /**
2583
+ * /steer: interject into the CURRENT turn instead of queueing for idle.
2584
+ *
2585
+ * Differences from pushChannelMessage, and nothing else:
2586
+ * - serialized on steerLock, not pasteLock — it must overtake, not queue
2587
+ * - deliverMessage runs with { steer: true }, which takes the busy branch
2588
+ * that pastes immediately (the codex native-queue transaction) instead of
2589
+ * waiting for idle. Verification and silent-loss fallback are the ones
2590
+ * that path already has: pane-capture visibility check, then one
2591
+ * idle-gated redelivery — so on a TUI that swallows busy input the steer
2592
+ * degrades to "next message after this turn", never silently vanishes.
2593
+ *
2594
+ * The steered text goes through formatInboundMessage so the agent sees a
2595
+ * normal inbound message, with a steering notice prepended for context.
2596
+ */
2597
+ steerMessage(content, meta) {
2598
+ this.updateLastChat(meta.chat_id, meta.thread_id, meta.adapter_id);
2599
+ this.pendingWork.recordInbound();
2600
+ this.recordRecentUserMessage(content, meta);
2601
+ const formatted = "[STEERING — mid-task course correction from the user. Fold this into the CURRENT work.]\n"
2602
+ + this.formatInboundMessage(content, meta);
2603
+ const chatId = meta.chat_id;
2604
+ const messageId = meta.message_id;
2605
+ const status = (chatId && messageId)
2606
+ ? { chatId: meta.thread_id || chatId, messageId }
2607
+ : undefined;
2608
+ this.steerLock = this.steerLock.then(async () => {
2609
+ await this.wake();
2610
+ if (await this.deliverMessage(formatted, status, { steer: true })) {
2611
+ this.markTurnStarted(meta, formatted);
2612
+ }
2613
+ }).catch(err => {
2614
+ this.logger.warn({ err: err.message }, "steer delivery error");
2615
+ });
2616
+ }
2431
2617
  /**
2432
2618
  * Push an inbound channel message to a specific MCP session.
2433
2619
  * If targetSession is provided, only send to the matching socket.
@@ -2474,35 +2660,7 @@ export class Daemon extends EventEmitter {
2474
2660
  });
2475
2661
  return;
2476
2662
  }
2477
- let formatted;
2478
- if (fromInstance) {
2479
- // #77: show the sender's display name for readability, keeping the machine
2480
- // instance name in parens so the recipient's send_to_instance target is valid.
2481
- const fromLabel = meta.from_display ? `${meta.from_display} (${fromInstance})` : fromInstance;
2482
- formatted = `[from:${fromLabel}] ${content}`;
2483
- formatted += renderHandoffMetadata(meta);
2484
- // A delegated task that requires a reply must not read like a chatty FYI —
2485
- // the "you may stay silent" line is for the latter only.
2486
- formatted += meta.requires_reply === "true"
2487
- ? "\n(A reply IS required: use report_result with the correlation_id above — or send_to_instance. Not direct text.)"
2488
- : "\n(If you need to reply, use send_to_instance tool, NOT direct text. If there is nothing to add, you may stay silent.)";
2489
- }
2490
- else {
2491
- const via = meta.source ? ` via ${meta.source}` : "";
2492
- const idTag = meta.user_id ? `, id:${meta.user_id}` : "";
2493
- formatted = `[user:${user}${via}${idTag}] ${content}`;
2494
- // Reactions queued since the last real message (#432). One leading line of
2495
- // context, present only when something is pending — a reaction no longer
2496
- // costs a turn of its own.
2497
- if (meta.pending_reactions) {
2498
- formatted = `[Recent reactions: ${meta.pending_reactions}]\n${formatted}`;
2499
- }
2500
- formatted += renderHandoffMetadata(meta);
2501
- formatted += "\n(Reply using the reply tool — do NOT respond with direct text)";
2502
- }
2503
- if (meta.reply_to_text) {
2504
- formatted += `\n(reply_to: "${meta.reply_to_text}")`;
2505
- }
2663
+ const formatted = this.formatInboundMessage(content, meta);
2506
2664
  // Serialize deliveries: each message waits for the previous to complete,
2507
2665
  // and each waits for the CLI to be idle before pasting. Messages are never
2508
2666
  // dropped for age — a long-busy CLI just queues them until it frees up
@@ -2529,6 +2687,9 @@ export class Daemon extends EventEmitter {
2529
2687
  const status = (chatId && messageId)
2530
2688
  ? { chatId: meta.thread_id || chatId, messageId }
2531
2689
  : undefined;
2690
+ // A fresh delivery begins a fresh turn — its bubble must not inherit
2691
+ // the previous turn's tool list.
2692
+ this.resetToolProgress();
2532
2693
  if (await this.deliverMessage(formatted, status))
2533
2694
  this.markTurnStarted(meta, formatted);
2534
2695
  }
@@ -2556,7 +2717,7 @@ export class Daemon extends EventEmitter {
2556
2717
  * immediately and own the application-level queue themselves. The pasteLock remains
2557
2718
  * serial in both cases so separate PTY writes can never overlap.
2558
2719
  */
2559
- async deliverMessage(formatted, status) {
2720
+ async deliverMessage(formatted, status, opts) {
2560
2721
  // Sanitize unclosed code fences — they cause CLI to wait for closure on Enter
2561
2722
  const fenceCount = (formatted.match(/```/g) || []).length;
2562
2723
  if (fenceCount % 2 !== 0) {
@@ -2574,9 +2735,14 @@ export class Daemon extends EventEmitter {
2574
2735
  if (windowId && this.controlClient && !this.controlClient.isIdle(windowId)) {
2575
2736
  if (status)
2576
2737
  this.emit("message_queued", status);
2577
- if (supportsQueuedInput) {
2738
+ if (supportsQueuedInput || opts?.steer) {
2739
+ // Native queue (codex), or an explicit /steer: hand the complete
2740
+ // paste+Enter transaction to the busy CLI now. For steer this is the
2741
+ // point — the user asked to interject, and the transaction's
2742
+ // visibility check + idle-gated fallback catch TUIs that swallow
2743
+ // busy input (see steerMessage).
2578
2744
  handingOffToNativeQueue = true;
2579
- this.logger.debug("CLI busy — handing message to backend-native input queue");
2745
+ this.logger.debug(opts?.steer ? "CLI busy — steering into the running turn" : "CLI busy — handing message to backend-native input queue");
2580
2746
  }
2581
2747
  else {
2582
2748
  this.logger.debug("CLI busy — queuing message until idle");
@@ -3229,6 +3395,8 @@ export class Daemon extends EventEmitter {
3229
3395
  });
3230
3396
  }
3231
3397
  const agentPort = parseInt(process.env.AGEND_PORT ?? "19280", 10);
3398
+ const backendName = this.config.backend ?? "claude-code";
3399
+ const backendOptions = this.config.backend_options?.[backendName];
3232
3400
  return {
3233
3401
  workingDirectory: this.config.working_directory,
3234
3402
  instanceDir: this.instanceDir,
@@ -3248,6 +3416,7 @@ export class Daemon extends EventEmitter {
3248
3416
  instructions,
3249
3417
  agentMode: isCliMode ? "cli" : "mcp",
3250
3418
  agentPort: isCliMode ? agentPort : undefined,
3419
+ backendOptions,
3251
3420
  };
3252
3421
  }
3253
3422
  /**
@@ -3611,9 +3780,29 @@ export class Daemon extends EventEmitter {
3611
3780
  // back from statusline.json — that would re-arm --continue and re-loop.
3612
3781
  if (this.skipResume)
3613
3782
  return;
3614
- const sid = this.backend?.getSessionId();
3615
- if (sid) {
3616
- writeFileSync(join(this.instanceDir, "session-id"), sid);
3783
+ try {
3784
+ const sid = this.backend?.getSessionId();
3785
+ if (!sid)
3786
+ return;
3787
+ const path = join(this.instanceDir, "session-id");
3788
+ // Idle observations happen after turns. Avoid needless writes (and mtime
3789
+ // churn) while still updating the marker when /new changes the session.
3790
+ try {
3791
+ if (readFileSync(path, "utf-8").trim() === sid)
3792
+ return;
3793
+ }
3794
+ catch { /* first checkpoint */ }
3795
+ writeFileSync(path, sid);
3796
+ this.sessionCheckpointWarningEmitted = false;
3797
+ this.logger.debug("Session id checkpointed");
3798
+ }
3799
+ catch (err) {
3800
+ // Session discovery is best-effort and must never break the pane state
3801
+ // monitor. Existing stop/pause checkpoints get another chance later.
3802
+ if (!this.sessionCheckpointWarningEmitted) {
3803
+ this.sessionCheckpointWarningEmitted = true;
3804
+ this.logger.warn({ err: err.message }, "Session id checkpoint failed");
3805
+ }
3617
3806
  }
3618
3807
  }
3619
3808
  readContextPercentage() {