@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.
- package/dist/backend/antigravity.js +5 -2
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/codex.d.ts +16 -0
- package/dist/backend/codex.js +226 -56
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/grok.js +20 -0
- package/dist/backend/grok.js.map +1 -1
- package/dist/backend/kiro.js +6 -2
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/types.d.ts +7 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/channel/ipc-bridge.d.ts +8 -0
- package/dist/channel/ipc-bridge.js +41 -14
- package/dist/channel/ipc-bridge.js.map +1 -1
- package/dist/channel/mcp-server.js +5 -0
- package/dist/channel/mcp-server.js.map +1 -1
- package/dist/cli.js +3 -2
- package/dist/cli.js.map +1 -1
- package/dist/daemon.d.ts +17 -8
- package/dist/daemon.js +126 -15
- package/dist/daemon.js.map +1 -1
- package/dist/fleet-manager.d.ts +7 -1
- package/dist/fleet-manager.js +58 -9
- package/dist/fleet-manager.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +2 -1
- package/dist/instance-lifecycle.js +6 -2
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instructions.d.ts +5 -0
- package/dist/instructions.js +9 -11
- package/dist/instructions.js.map +1 -1
- package/dist/logger.js +14 -0
- package/dist/logger.js.map +1 -1
- package/dist/outbound-handlers.d.ts +9 -0
- package/dist/outbound-handlers.js +46 -26
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/topic-commands.d.ts +1 -1
- package/dist/topic-commands.js +11 -2
- package/dist/topic-commands.js.map +1 -1
- package/dist/tui-glyphs.d.ts +25 -0
- package/dist/tui-glyphs.js +26 -0
- package/dist/tui-glyphs.js.map +1 -0
- package/dist/view-api.d.ts +3 -0
- package/dist/view-api.js +6 -2
- package/dist/view-api.js.map +1 -1
- 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
|
|
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 —
|
|
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):
|
|
1767
|
-
*
|
|
1768
|
-
*
|
|
1769
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
1784
|
-
|
|
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
|
-
//
|
|
1811
|
-
//
|
|
1812
|
-
// the
|
|
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
|
-
|
|
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
|
}
|