@songsid/agend 2.1.2-beta.1 → 2.1.2-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 (45) hide show
  1. package/dist/backend/antigravity.js +5 -2
  2. package/dist/backend/antigravity.js.map +1 -1
  3. package/dist/backend/codex.d.ts +16 -0
  4. package/dist/backend/codex.js +226 -56
  5. package/dist/backend/codex.js.map +1 -1
  6. package/dist/backend/grok.js +20 -0
  7. package/dist/backend/grok.js.map +1 -1
  8. package/dist/backend/kiro.js +6 -2
  9. package/dist/backend/kiro.js.map +1 -1
  10. package/dist/backend/types.d.ts +7 -0
  11. package/dist/backend/types.js.map +1 -1
  12. package/dist/channel/ipc-bridge.d.ts +8 -0
  13. package/dist/channel/ipc-bridge.js +41 -14
  14. package/dist/channel/ipc-bridge.js.map +1 -1
  15. package/dist/channel/mcp-server.js +5 -0
  16. package/dist/channel/mcp-server.js.map +1 -1
  17. package/dist/cli.js +3 -2
  18. package/dist/cli.js.map +1 -1
  19. package/dist/daemon.d.ts +17 -8
  20. package/dist/daemon.js +126 -15
  21. package/dist/daemon.js.map +1 -1
  22. package/dist/fleet-manager.d.ts +7 -1
  23. package/dist/fleet-manager.js +58 -9
  24. package/dist/fleet-manager.js.map +1 -1
  25. package/dist/instance-lifecycle.d.ts +2 -1
  26. package/dist/instance-lifecycle.js +6 -2
  27. package/dist/instance-lifecycle.js.map +1 -1
  28. package/dist/instructions.d.ts +5 -0
  29. package/dist/instructions.js +9 -11
  30. package/dist/instructions.js.map +1 -1
  31. package/dist/logger.js +14 -0
  32. package/dist/logger.js.map +1 -1
  33. package/dist/outbound-handlers.d.ts +9 -0
  34. package/dist/outbound-handlers.js +46 -26
  35. package/dist/outbound-handlers.js.map +1 -1
  36. package/dist/topic-commands.d.ts +1 -1
  37. package/dist/topic-commands.js +11 -2
  38. package/dist/topic-commands.js.map +1 -1
  39. package/dist/tui-glyphs.d.ts +25 -0
  40. package/dist/tui-glyphs.js +26 -0
  41. package/dist/tui-glyphs.js.map +1 -0
  42. package/dist/view-api.d.ts +3 -0
  43. package/dist/view-api.js +6 -2
  44. package/dist/view-api.js.map +1 -1
  45. package/package.json +1 -1
package/dist/daemon.js CHANGED
@@ -3,6 +3,7 @@ import { mkdirSync, writeFileSync, readFileSync, existsSync, unlinkSync, rmSync,
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { createHash, randomBytes } from "node:crypto";
5
5
  import { EventEmitter } from "node:events";
6
+ import { rotateLogIfNeeded } from "./logger.js";
6
7
  import { clearPausedMarker, writePausedMarker } from "./pause-marker.js";
7
8
  import { TmuxManager, resolveTmuxLogicalSize } from "./tmux-manager.js";
8
9
  import { TranscriptMonitor } from "./transcript-monitor.js";
@@ -193,6 +194,8 @@ export class PendingWorkTracker {
193
194
  const NORMAL_ENTER_SETTLE_MS = 500;
194
195
  const FIRST_ENTER_SETTLE_MS = 1_750;
195
196
  const FIRST_DELIVERY_WINDOW_MS = 5_000;
197
+ /** After busy native-queue paste+Enter, wait before checking the pane for silent loss. */
198
+ const NATIVE_QUEUE_PASTE_VERIFY_MS = 2_000;
196
199
  /**
197
200
  * One-shot timing gate for the first paste after a CLI reaches its ready
198
201
  * prompt. Recent tmux versions expose a short interval where the prompt is
@@ -244,6 +247,7 @@ export class Daemon extends EventEmitter {
244
247
  topicMode;
245
248
  backend;
246
249
  controlClient;
250
+ runtimeIdentity;
247
251
  logger;
248
252
  tmuxSessionName;
249
253
  tmux = null;
@@ -371,7 +375,7 @@ export class Daemon extends EventEmitter {
371
375
  this.errorRecoveryDeadlineAt = 0;
372
376
  this.activeErrorPatternKey = null;
373
377
  }
374
- constructor(name, config, instanceDir, topicMode = false, backend, controlClient, rootLogger) {
378
+ constructor(name, config, instanceDir, topicMode = false, backend, controlClient, rootLogger, runtimeIdentity) {
375
379
  super();
376
380
  this.name = name;
377
381
  this.config = config;
@@ -379,8 +383,14 @@ export class Daemon extends EventEmitter {
379
383
  this.topicMode = topicMode;
380
384
  this.backend = backend;
381
385
  this.controlClient = controlClient;
386
+ this.runtimeIdentity = runtimeIdentity;
382
387
  if (!rootLogger)
383
388
  throw new Error("Daemon requires a shared root logger");
389
+ this.runtimeIdentity ??= {
390
+ kind: "fleet-topic",
391
+ backend: config.backend ?? backend?.binaryName ?? "unknown",
392
+ model: config.model ?? "default",
393
+ };
384
394
  this.logger = rootLogger.child({ instance: name }, { level: config.log_level });
385
395
  this.tmuxSessionName = getTmuxSession();
386
396
  this.messageBus = new MessageBus();
@@ -612,8 +622,12 @@ export class Daemon extends EventEmitter {
612
622
  catch { /* non-fatal */ }
613
623
  })();
614
624
  if (!this.config.lightweight) {
615
- // 3. Pipe-pane for prompt detection
625
+ // 3. Pipe-pane for prompt detection. Rotate first so a ballooned log from a
626
+ // previous stuck splash (hundreds of MB of ANSI frames) is truncated before
627
+ // we attach — pipe-pane uses `cat >>` on the same inode, so copytruncate
628
+ // keeps the writer attached after size resets.
616
629
  const outputLog = join(this.instanceDir, "output.log");
630
+ rotateLogIfNeeded(outputLog);
617
631
  await this.tmux.pipeOutput(outputLog).catch(() => { });
618
632
  // 4. Transcript monitor
619
633
  this.transcriptMonitor = new TranscriptMonitor(this.instanceDir, this.logger);
@@ -716,6 +730,11 @@ export class Daemon extends EventEmitter {
716
730
  return;
717
731
  }
718
732
  if (paneStatus?.alive) {
733
+ // Instance output.log is fed by tmux pipe-pane and was previously never
734
+ // rotated (only fleet.log / daemon.log were). Cap growth every tick.
735
+ if (!this.config.lightweight) {
736
+ rotateLogIfNeeded(join(this.instanceDir, "output.log"));
737
+ }
719
738
  scheduleNext();
720
739
  return;
721
740
  }
@@ -1755,18 +1774,19 @@ export class Daemon extends EventEmitter {
1755
1774
  }
1756
1775
  /**
1757
1776
  * Deliver a single message and drive its status reactions:
1758
- * ⏳ message_queued — CLI busy; queued, waiting for idle
1777
+ * ⏳ message_queued — CLI busy; queued locally or by the backend
1759
1778
  * 👀 message_delivered — pasted + Enter sent; agent now has it
1760
- * ✅ message_confirmed — idle→busy transition observed; agent is processing
1779
+ * ✅ message_confirmed — processing observed, or native queue handoff succeeded
1761
1780
  * ❌ message_failed — tmux window gone, paste retries exhausted
1762
1781
  * Returns true once the message is in the CLI, false only on real delivery failure.
1763
1782
  *
1764
1783
  * Bug A (silent message loss): paste failures retry with backoff (window recovery)
1765
1784
  * and emit `message_failed` if all attempts fail.
1766
- * Busy handling (UX): we never force-paste into a busy CLI and never give up on a
1767
- * busy one — we show ⏳ and wait for idle indefinitely (a genuinely hung CLI is the
1768
- * hang detector's job; a user who can't wait presses Cancel → Escape → idle). The
1769
- * pasteLock is serial, so later messages naturally queue behind this wait.
1785
+ * Busy handling (UX): backends without a native input queue show and wait for
1786
+ * idle indefinitely (a genuinely hung CLI is the hang detector's job). Backends
1787
+ * that explicitly support queued input receive one complete paste+Enter transaction
1788
+ * immediately and own the application-level queue themselves. The pasteLock remains
1789
+ * serial in both cases so separate PTY writes can never overlap.
1770
1790
  */
1771
1791
  async deliverMessage(formatted, status) {
1772
1792
  // Sanitize unclosed code fences — they cause CLI to wait for closure on Enter
@@ -1776,12 +1796,22 @@ export class Daemon extends EventEmitter {
1776
1796
  formatted = formatted.replace(/```/g, "");
1777
1797
  }
1778
1798
  let windowId = this.getWindowId();
1779
- // If the CLI is busy, show ⏳ and wait for it to go idle — no timeout, no force.
1799
+ const supportsQueuedInput = this.backend?.supportsQueuedInput?.() === true;
1800
+ let handingOffToNativeQueue = false;
1801
+ // If the CLI is busy, either hand the complete submission to its native input
1802
+ // queue or wait for idle. Native queue support is an explicit backend capability:
1803
+ // normal Enter input has steering/interrupt semantics in several other CLIs.
1780
1804
  if (windowId && this.controlClient && !this.controlClient.isIdle(windowId)) {
1781
1805
  if (status)
1782
1806
  this.emit("message_queued", status);
1783
- this.logger.debug("CLI busy — queuing message until idle");
1784
- await this.controlClient.waitUntilIdle(windowId);
1807
+ if (supportsQueuedInput) {
1808
+ handingOffToNativeQueue = true;
1809
+ this.logger.debug("CLI busy — handing message to backend-native input queue");
1810
+ }
1811
+ else {
1812
+ this.logger.debug("CLI busy — queuing message until idle");
1813
+ await this.controlClient.waitUntilIdle(windowId);
1814
+ }
1785
1815
  }
1786
1816
  // Bug A: paste with backoff. Transient failures are usually a stale window id
1787
1817
  // after a crash/respawn — recover by name and retry (max 3 attempts, 2s apart).
@@ -1807,9 +1837,56 @@ export class Daemon extends EventEmitter {
1807
1837
  await this.tmux.sendSpecialKey("Enter");
1808
1838
  if (status)
1809
1839
  this.emit("message_delivered", status); // 👀
1810
- // Confirm the CLI accepted the message by transitioning idle→busy (new output
1811
- // after Enter). If still idle after ~2s the Enter was likely swallowed while
1812
- // the TUI was redrawing re-send Enter once and re-check.
1840
+ // Busy queue-capable CLIs (codex) may accept paste without an idle→busy
1841
+ // transition. Do NOT probe for busy a second bare Enter can mutate the
1842
+ // queue. Instead verify the paste actually landed in the pane (TUI redraw
1843
+ // can silently swallow it). Idle submissions keep the swallowed-Enter path.
1844
+ if (handingOffToNativeQueue) {
1845
+ await new Promise(r => setTimeout(r, NATIVE_QUEUE_PASTE_VERIFY_MS));
1846
+ if (await this.nativeQueuePasteVisible(formatted)) {
1847
+ if (status)
1848
+ this.emit("message_confirmed", status); // ✅ native queue accepted
1849
+ return true;
1850
+ }
1851
+ // Silent loss: fall back once to the normal idle-gated path.
1852
+ this.logger.warn("Native-queue paste not visible in pane — retrying via idle-gated delivery");
1853
+ if (windowId && this.controlClient) {
1854
+ await this.controlClient.waitUntilIdle(windowId);
1855
+ }
1856
+ const repasted = await this.tmux.pasteBuffer(formatted);
1857
+ if (!repasted) {
1858
+ this.logger.error("Idle-gated redelivery paste failed after native-queue silent loss");
1859
+ if (status)
1860
+ this.emit("message_failed", status); // ❌
1861
+ return false;
1862
+ }
1863
+ await new Promise(r => setTimeout(r, NORMAL_ENTER_SETTLE_MS));
1864
+ const retryAt = Date.now();
1865
+ await this.tmux.sendSpecialKey("Enter");
1866
+ if (windowId && this.controlClient) {
1867
+ let becameBusy = await this.confirmBusyAfterEnter(windowId, retryAt);
1868
+ if (!becameBusy) {
1869
+ this.logger.warn("No idle→busy after idle-gated redelivery — re-sending Enter once");
1870
+ const retry2At = Date.now();
1871
+ await this.tmux.sendSpecialKey("Enter");
1872
+ becameBusy = await this.confirmBusyAfterEnter(windowId, retry2At);
1873
+ }
1874
+ if (becameBusy) {
1875
+ if (status)
1876
+ this.emit("message_confirmed", status); // ✅
1877
+ return true;
1878
+ }
1879
+ }
1880
+ else if (await this.nativeQueuePasteVisible(formatted)) {
1881
+ if (status)
1882
+ this.emit("message_confirmed", status); // ✅
1883
+ return true;
1884
+ }
1885
+ this.logger.error("Idle-gated redelivery also failed after native-queue silent loss");
1886
+ if (status)
1887
+ this.emit("message_failed", status); // ❌
1888
+ return false;
1889
+ }
1813
1890
  if (windowId && this.controlClient) {
1814
1891
  let becameBusy = await this.confirmBusyAfterEnter(windowId, enterAt);
1815
1892
  if (!becameBusy) {
@@ -1835,6 +1912,33 @@ export class Daemon extends EventEmitter {
1835
1912
  this.emit("message_failed", status); // ❌
1836
1913
  return false;
1837
1914
  }
1915
+ /**
1916
+ * True when a busy native-queue paste appears to have landed: Codex shows a
1917
+ * `↳` queue marker, or a distinctive slice of the pasted text is on screen.
1918
+ * Used only to detect silent paste loss — not as a general ready check.
1919
+ */
1920
+ async nativeQueuePasteVisible(formatted) {
1921
+ if (!this.tmux)
1922
+ return false;
1923
+ try {
1924
+ const pane = await this.tmux.capturePane();
1925
+ if (pane.includes("↳"))
1926
+ return true;
1927
+ for (const line of formatted.split(/\r?\n/)) {
1928
+ const t = line.trim();
1929
+ if (t.length >= 8 && pane.includes(t))
1930
+ return true;
1931
+ }
1932
+ const compact = formatted.replace(/\s+/g, " ").trim();
1933
+ if (compact.length >= 8 && pane.includes(compact.slice(0, Math.min(80, compact.length)))) {
1934
+ return true;
1935
+ }
1936
+ return false;
1937
+ }
1938
+ catch {
1939
+ return false;
1940
+ }
1941
+ }
1838
1942
  /** Re-resolve this instance's tmux window by name (stale id after crash/respawn). */
1839
1943
  async recoverWindow() {
1840
1944
  try {
@@ -2124,6 +2228,9 @@ export class Daemon extends EventEmitter {
2124
2228
  AGEND_SOCKET_PATH: sockPath,
2125
2229
  AGEND_INSTANCE_NAME: this.name,
2126
2230
  AGEND_WORKING_DIR: this.config.working_directory,
2231
+ AGEND_INSTANCE_KIND: this.runtimeIdentity?.kind ?? "fleet-topic",
2232
+ AGEND_BACKEND: this.runtimeIdentity?.backend ?? this.config.backend ?? this.backend?.binaryName ?? "unknown",
2233
+ AGEND_MODEL: this.runtimeIdentity?.model ?? this.config.model ?? "default",
2127
2234
  };
2128
2235
  if (this.config.tool_set)
2129
2236
  mcpEnv.AGEND_TOOL_SET = this.config.tool_set;
@@ -2159,6 +2266,7 @@ export class Daemon extends EventEmitter {
2159
2266
  instructions = buildFleetInstructions({
2160
2267
  instanceName: this.name,
2161
2268
  workingDirectory: this.config.working_directory,
2269
+ runtimeIdentity: this.runtimeIdentity,
2162
2270
  displayName: this.config.display_name,
2163
2271
  description: this.config.description,
2164
2272
  customPrompt: resolvedCustomPrompt,
@@ -2171,6 +2279,7 @@ export class Daemon extends EventEmitter {
2171
2279
  instructions = buildFleetInstructions({
2172
2280
  instanceName: this.name,
2173
2281
  workingDirectory: this.config.working_directory,
2282
+ runtimeIdentity: this.runtimeIdentity,
2174
2283
  displayName: this.config.display_name,
2175
2284
  description: this.config.description,
2176
2285
  customPrompt: resolvedCustomPrompt,
@@ -2366,7 +2475,9 @@ export class Daemon extends EventEmitter {
2366
2475
  this.logger.warn({ err }, "Failed to set remain-on-exit — exit codes will not be captured");
2367
2476
  });
2368
2477
  if (reuseWindow && !this.config.lightweight) {
2369
- await this.tmux.pipeOutput(join(this.instanceDir, "output.log")).catch(err => {
2478
+ const outputLog = join(this.instanceDir, "output.log");
2479
+ rotateLogIfNeeded(outputLog);
2480
+ await this.tmux.pipeOutput(outputLog).catch(err => {
2370
2481
  this.logger.warn({ err }, "Failed to restore pipe-pane after wake");
2371
2482
  });
2372
2483
  }