@songsid/agend 2.1.3 → 2.1.4-beta.10
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/LICENSE +21 -0
- package/README.md +2 -2
- package/README.zh-TW.md +2 -2
- package/dist/backend/antigravity.d.ts +1 -0
- package/dist/backend/antigravity.js +1 -0
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/claude-code.d.ts +1 -0
- package/dist/backend/claude-code.js +24 -2
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/codex.d.ts +1 -0
- package/dist/backend/codex.js +43 -4
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/gemini-cli.d.ts +1 -0
- package/dist/backend/gemini-cli.js +1 -0
- package/dist/backend/gemini-cli.js.map +1 -1
- package/dist/backend/grok.d.ts +1 -0
- package/dist/backend/grok.js +1 -0
- package/dist/backend/grok.js.map +1 -1
- package/dist/backend/kiro.d.ts +1 -0
- package/dist/backend/kiro.js +1 -0
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/mock.d.ts +1 -0
- package/dist/backend/mock.js +1 -0
- package/dist/backend/mock.js.map +1 -1
- package/dist/backend/opencode.d.ts +40 -0
- package/dist/backend/opencode.js +83 -5
- package/dist/backend/opencode.js.map +1 -1
- package/dist/backend/types.d.ts +10 -0
- package/dist/backend/types.js +7 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/channel/adapters/discord.js +24 -7
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/mcp-server.js +6 -20
- package/dist/channel/mcp-server.js.map +1 -1
- package/dist/channel/mcp-tools.js +1 -1
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/types.d.ts +1 -1
- package/dist/classic-channel-manager.d.ts +34 -3
- package/dist/classic-channel-manager.js +190 -31
- package/dist/classic-channel-manager.js.map +1 -1
- package/dist/cli.js +74 -23
- package/dist/cli.js.map +1 -1
- package/dist/completion-install.d.ts +70 -0
- package/dist/completion-install.js +152 -0
- package/dist/completion-install.js.map +1 -0
- package/dist/completion.d.ts +7 -1
- package/dist/completion.js +14 -4
- package/dist/completion.js.map +1 -1
- package/dist/config-validator.js +35 -7
- package/dist/config-validator.js.map +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/daemon.d.ts +53 -9
- package/dist/daemon.js +222 -86
- package/dist/daemon.js.map +1 -1
- package/dist/fleet-context.d.ts +8 -0
- package/dist/fleet-manager.d.ts +136 -5
- package/dist/fleet-manager.js +1088 -316
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/backend-providers/SKILL.md +114 -0
- package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +3 -1
- package/dist/general-knowledge/skills/delegation-playbook/SKILL.md +52 -0
- package/dist/general-knowledge/skills/development-workflow/SKILL.md +28 -0
- package/dist/general-knowledge/skills/fleet-config/SKILL.md +1 -0
- package/dist/general-knowledge/skills/fleet-health/SKILL.md +1 -0
- package/dist/general-knowledge/skills/fleet-restart/SKILL.md +1 -0
- package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +1 -0
- package/dist/general-knowledge/skills/model-discovery/SKILL.md +1 -0
- package/dist/general-knowledge/skills/multi-channel/SKILL.md +1 -0
- package/dist/general-knowledge/skills/scheduling/SKILL.md +1 -0
- package/dist/general-knowledge/skills/session-management/SKILL.md +1 -0
- package/dist/general-knowledge/skills/tui-effort/SKILL.md +1 -0
- package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +30 -0
- package/dist/instance-lifecycle.d.ts +10 -0
- package/dist/instance-lifecycle.js +50 -33
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instructions.d.ts +12 -0
- package/dist/instructions.js +33 -0
- package/dist/instructions.js.map +1 -1
- package/dist/locale.js +130 -0
- package/dist/locale.js.map +1 -1
- package/dist/outbound-schemas.d.ts +2 -1
- package/dist/outbound-schemas.js +1 -0
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/quickstart.js +29 -2
- package/dist/quickstart.js.map +1 -1
- package/dist/restart-progress.d.ts +7 -1
- package/dist/restart-progress.js +27 -6
- package/dist/restart-progress.js.map +1 -1
- package/dist/setup-wizard.d.ts +4 -2
- package/dist/setup-wizard.js +2 -2
- package/dist/setup-wizard.js.map +1 -1
- package/dist/tool-progress.d.ts +40 -0
- package/dist/tool-progress.js +289 -0
- package/dist/tool-progress.js.map +1 -0
- package/dist/topic-commands.d.ts +26 -0
- package/dist/topic-commands.js +117 -1
- package/dist/topic-commands.js.map +1 -1
- package/dist/transcript-monitor.d.ts +15 -2
- package/dist/transcript-monitor.js +63 -17
- package/dist/transcript-monitor.js.map +1 -1
- package/dist/transcript-sources.d.ts +106 -0
- package/dist/transcript-sources.js +428 -0
- package/dist/transcript-sources.js.map +1 -0
- package/dist/types.d.ts +8 -0
- package/dist/ui/dashboard.html +5 -4
- package/dist/update-marker.d.ts +30 -0
- package/dist/update-marker.js +71 -20
- package/dist/update-marker.js.map +1 -1
- package/dist/update-progress.d.ts +6 -0
- package/dist/update-progress.js +29 -0
- package/dist/update-progress.js.map +1 -0
- package/dist/web-api.js +2 -2
- package/dist/web-api.js.map +1 -1
- package/dist/workflow-templates/default.md +2 -1
- 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";
|
|
@@ -614,12 +616,8 @@ export class Daemon extends EventEmitter {
|
|
|
614
616
|
lastAdapterId;
|
|
615
617
|
// Pending ack: react 🫡 on first transcript activity after receiving a message
|
|
616
618
|
pendingAckMessage = null;
|
|
617
|
-
// Tool status tracking for channel adapter
|
|
618
619
|
/** Last activity published to the fleet manager; null when nothing is running. */
|
|
619
620
|
currentActivity = null;
|
|
620
|
-
toolStatusMessageId = null;
|
|
621
|
-
toolStatusLines = [];
|
|
622
|
-
toolStatusDebounce = null;
|
|
623
621
|
// Session identity: map IPC socket → sessionName (from mcp_ready)
|
|
624
622
|
socketSessionNames = new Map();
|
|
625
623
|
// Crash recovery
|
|
@@ -669,6 +667,7 @@ export class Daemon extends EventEmitter {
|
|
|
669
667
|
instanceStateReadyPattern = null;
|
|
670
668
|
instanceStateBusyPattern = null;
|
|
671
669
|
instanceStateMonitorActive = false;
|
|
670
|
+
sessionCheckpointWarningEmitted = false;
|
|
672
671
|
statePollInFlight = false;
|
|
673
672
|
// ── Dead-MCP proxy reply: per-turn evidence ─────────────────────────────
|
|
674
673
|
// Set when an inbound message lands in the CLI; cleared on the idle edge that
|
|
@@ -714,6 +713,17 @@ export class Daemon extends EventEmitter {
|
|
|
714
713
|
lastBuiltInstructions = "";
|
|
715
714
|
pasteQueueDepth = 0;
|
|
716
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;
|
|
717
727
|
// PTY error pattern monitoring
|
|
718
728
|
errorMonitorTimer = null;
|
|
719
729
|
interactivePromptDetector = new InteractivePromptDetector();
|
|
@@ -933,6 +943,10 @@ export class Daemon extends EventEmitter {
|
|
|
933
943
|
});
|
|
934
944
|
}
|
|
935
945
|
}
|
|
946
|
+
else if (msg.type === "steer") {
|
|
947
|
+
const meta = (msg.meta ?? {});
|
|
948
|
+
this.steerMessage(msg.content, meta);
|
|
949
|
+
}
|
|
936
950
|
else if (msg.type === "fleet_schedule_trigger") {
|
|
937
951
|
const payload = msg.payload;
|
|
938
952
|
const meta = msg.meta;
|
|
@@ -940,10 +954,6 @@ export class Daemon extends EventEmitter {
|
|
|
940
954
|
this.logger.error({ err: err.message }, "Wake failed for scheduled delivery");
|
|
941
955
|
});
|
|
942
956
|
}
|
|
943
|
-
else if (msg.type === "fleet_tool_status_ack") {
|
|
944
|
-
// Fleet manager sent us the messageId for our tool status message
|
|
945
|
-
this.toolStatusMessageId = msg.messageId;
|
|
946
|
-
}
|
|
947
957
|
else if (msg.type === "query_instance_state") {
|
|
948
958
|
const snapshot = this.getInstanceStateSnapshot();
|
|
949
959
|
this.ipcServer?.send(socket, {
|
|
@@ -1040,8 +1050,11 @@ export class Daemon extends EventEmitter {
|
|
|
1040
1050
|
const outputLog = join(this.instanceDir, "output.log");
|
|
1041
1051
|
rotateLogIfNeeded(outputLog);
|
|
1042
1052
|
await this.tmux.pipeOutput(outputLog).catch(() => { });
|
|
1043
|
-
// 4. Transcript monitor
|
|
1044
|
-
|
|
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));
|
|
1045
1058
|
// 5. Wire transcript events
|
|
1046
1059
|
const ackIfPending = () => {
|
|
1047
1060
|
if (!this.pendingAckMessage || !this.adapter)
|
|
@@ -1057,6 +1070,7 @@ export class Daemon extends EventEmitter {
|
|
|
1057
1070
|
this.recordRecentEvent({ type: "tool_use", name, preview: this.summarizeTool(name, input) });
|
|
1058
1071
|
this.recordRecentToolActivity(this.summarizeTool(name, input));
|
|
1059
1072
|
this.publishActivity(this.summarizeTool(name, input));
|
|
1073
|
+
this.recordToolProgress(name, input);
|
|
1060
1074
|
});
|
|
1061
1075
|
this.transcriptMonitor.on("tool_result", (name, _output) => {
|
|
1062
1076
|
this.recordRecentEvent({ type: "tool_result", name });
|
|
@@ -1268,7 +1282,7 @@ export class Daemon extends EventEmitter {
|
|
|
1268
1282
|
this.logger.info("CLI exited normally (code 0) — pausing health check");
|
|
1269
1283
|
await this.tmux.killWindow();
|
|
1270
1284
|
this.healthCheckPaused = true;
|
|
1271
|
-
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);
|
|
1272
1286
|
return;
|
|
1273
1287
|
}
|
|
1274
1288
|
// Distinguish tmux server crash from single window crash.
|
|
@@ -1365,6 +1379,17 @@ export class Daemon extends EventEmitter {
|
|
|
1365
1379
|
catch (err) {
|
|
1366
1380
|
this.logger.error({ err: err.message }, "Recovery from background session conflict failed");
|
|
1367
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();
|
|
1368
1393
|
return; // Don't count as crash
|
|
1369
1394
|
}
|
|
1370
1395
|
// Already attempted recovery — fall through to normal crash handling
|
|
@@ -1514,8 +1539,13 @@ export class Daemon extends EventEmitter {
|
|
|
1514
1539
|
* `crash_loop` already had this treatment; this is the same bridge for the other
|
|
1515
1540
|
* four, carrying a human-readable cause and the operator's next step.
|
|
1516
1541
|
*/
|
|
1517
|
-
emitSupervisionEnded(reason, remedy) {
|
|
1518
|
-
this.emit("supervision_ended", {
|
|
1542
|
+
emitSupervisionEnded(reason, remedy, exitCode) {
|
|
1543
|
+
this.emit("supervision_ended", {
|
|
1544
|
+
name: this.name,
|
|
1545
|
+
reason,
|
|
1546
|
+
remedy,
|
|
1547
|
+
...(exitCode !== undefined ? { exitCode } : {}),
|
|
1548
|
+
});
|
|
1519
1549
|
}
|
|
1520
1550
|
setProcessStatus(status) {
|
|
1521
1551
|
if (this.processStatus === status)
|
|
@@ -1779,10 +1809,6 @@ export class Daemon extends EventEmitter {
|
|
|
1779
1809
|
async stop() {
|
|
1780
1810
|
this.logger.info("Stopping daemon instance");
|
|
1781
1811
|
this.freezeRuntimeMonitors();
|
|
1782
|
-
if (this.toolStatusDebounce) {
|
|
1783
|
-
clearTimeout(this.toolStatusDebounce);
|
|
1784
|
-
this.toolStatusDebounce = null;
|
|
1785
|
-
}
|
|
1786
1812
|
this.pendingIpcRequests.clear();
|
|
1787
1813
|
if (this.adapter)
|
|
1788
1814
|
await this.adapter.stop();
|
|
@@ -2000,6 +2026,14 @@ export class Daemon extends EventEmitter {
|
|
|
2000
2026
|
applyInstanceStateSnapshot(snapshot, pane) {
|
|
2001
2027
|
const previous = this.instanceState;
|
|
2002
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
|
+
}
|
|
2003
2037
|
// Only a transition back to idle completes pending work. Repeated idle
|
|
2004
2038
|
// observations between enqueue and paste must not clear a newer inbound.
|
|
2005
2039
|
if (snapshot.state === "idle" && previous !== "idle") {
|
|
@@ -2008,6 +2042,7 @@ export class Daemon extends EventEmitter {
|
|
|
2008
2042
|
// tool_result (interrupted, crashed, cancelled), which would otherwise leave
|
|
2009
2043
|
// the last tool pinned to the progress line for the rest of the session.
|
|
2010
2044
|
this.publishActivity(null);
|
|
2045
|
+
this.resetToolProgress();
|
|
2011
2046
|
// Must run before the mcpRestartPending branch below: the pane text is the
|
|
2012
2047
|
// only copy of the answer, and the revival restart is about to clear it.
|
|
2013
2048
|
this.maybeProxyReplyOnTurnEnd(pane);
|
|
@@ -2405,6 +2440,69 @@ export class Daemon extends EventEmitter {
|
|
|
2405
2440
|
activity: next,
|
|
2406
2441
|
});
|
|
2407
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
|
+
}
|
|
2408
2506
|
/**
|
|
2409
2507
|
* Options for every system-initiated paste (startup notice, session snapshot,
|
|
2410
2508
|
* runtime-dialog keys).
|
|
@@ -2440,45 +2538,82 @@ export class Daemon extends EventEmitter {
|
|
|
2440
2538
|
return ""; // skip channel tools
|
|
2441
2539
|
return name;
|
|
2442
2540
|
}
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
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.)";
|
|
2449
2563
|
}
|
|
2450
2564
|
else {
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
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}`;
|
|
2457
2573
|
}
|
|
2574
|
+
formatted += renderHandoffMetadata(meta);
|
|
2575
|
+
formatted += "\n(Reply using the reply tool — do NOT respond with direct text)";
|
|
2458
2576
|
}
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
if (this.toolStatusDebounce)
|
|
2464
|
-
clearTimeout(this.toolStatusDebounce);
|
|
2465
|
-
this.toolStatusDebounce = setTimeout(() => this.sendToolStatus(), 500);
|
|
2577
|
+
if (meta.reply_to_text) {
|
|
2578
|
+
formatted += `\n(reply_to: "${meta.reply_to_text}")`;
|
|
2579
|
+
}
|
|
2580
|
+
return formatted;
|
|
2466
2581
|
}
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
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");
|
|
2476
2615
|
});
|
|
2477
2616
|
}
|
|
2478
|
-
/** Called by fleet manager when tool status message is sent (returns messageId) */
|
|
2479
|
-
setToolStatusMessageId(messageId) {
|
|
2480
|
-
this.toolStatusMessageId = messageId;
|
|
2481
|
-
}
|
|
2482
2617
|
/**
|
|
2483
2618
|
* Push an inbound channel message to a specific MCP session.
|
|
2484
2619
|
* If targetSession is provided, only send to the matching socket.
|
|
@@ -2525,35 +2660,7 @@ export class Daemon extends EventEmitter {
|
|
|
2525
2660
|
});
|
|
2526
2661
|
return;
|
|
2527
2662
|
}
|
|
2528
|
-
|
|
2529
|
-
if (fromInstance) {
|
|
2530
|
-
// #77: show the sender's display name for readability, keeping the machine
|
|
2531
|
-
// instance name in parens so the recipient's send_to_instance target is valid.
|
|
2532
|
-
const fromLabel = meta.from_display ? `${meta.from_display} (${fromInstance})` : fromInstance;
|
|
2533
|
-
formatted = `[from:${fromLabel}] ${content}`;
|
|
2534
|
-
formatted += renderHandoffMetadata(meta);
|
|
2535
|
-
// A delegated task that requires a reply must not read like a chatty FYI —
|
|
2536
|
-
// the "you may stay silent" line is for the latter only.
|
|
2537
|
-
formatted += meta.requires_reply === "true"
|
|
2538
|
-
? "\n(A reply IS required: use report_result with the correlation_id above — or send_to_instance. Not direct text.)"
|
|
2539
|
-
: "\n(If you need to reply, use send_to_instance tool, NOT direct text. If there is nothing to add, you may stay silent.)";
|
|
2540
|
-
}
|
|
2541
|
-
else {
|
|
2542
|
-
const via = meta.source ? ` via ${meta.source}` : "";
|
|
2543
|
-
const idTag = meta.user_id ? `, id:${meta.user_id}` : "";
|
|
2544
|
-
formatted = `[user:${user}${via}${idTag}] ${content}`;
|
|
2545
|
-
// Reactions queued since the last real message (#432). One leading line of
|
|
2546
|
-
// context, present only when something is pending — a reaction no longer
|
|
2547
|
-
// costs a turn of its own.
|
|
2548
|
-
if (meta.pending_reactions) {
|
|
2549
|
-
formatted = `[Recent reactions: ${meta.pending_reactions}]\n${formatted}`;
|
|
2550
|
-
}
|
|
2551
|
-
formatted += renderHandoffMetadata(meta);
|
|
2552
|
-
formatted += "\n(Reply using the reply tool — do NOT respond with direct text)";
|
|
2553
|
-
}
|
|
2554
|
-
if (meta.reply_to_text) {
|
|
2555
|
-
formatted += `\n(reply_to: "${meta.reply_to_text}")`;
|
|
2556
|
-
}
|
|
2663
|
+
const formatted = this.formatInboundMessage(content, meta);
|
|
2557
2664
|
// Serialize deliveries: each message waits for the previous to complete,
|
|
2558
2665
|
// and each waits for the CLI to be idle before pasting. Messages are never
|
|
2559
2666
|
// dropped for age — a long-busy CLI just queues them until it frees up
|
|
@@ -2580,6 +2687,9 @@ export class Daemon extends EventEmitter {
|
|
|
2580
2687
|
const status = (chatId && messageId)
|
|
2581
2688
|
? { chatId: meta.thread_id || chatId, messageId }
|
|
2582
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();
|
|
2583
2693
|
if (await this.deliverMessage(formatted, status))
|
|
2584
2694
|
this.markTurnStarted(meta, formatted);
|
|
2585
2695
|
}
|
|
@@ -2607,7 +2717,7 @@ export class Daemon extends EventEmitter {
|
|
|
2607
2717
|
* immediately and own the application-level queue themselves. The pasteLock remains
|
|
2608
2718
|
* serial in both cases so separate PTY writes can never overlap.
|
|
2609
2719
|
*/
|
|
2610
|
-
async deliverMessage(formatted, status) {
|
|
2720
|
+
async deliverMessage(formatted, status, opts) {
|
|
2611
2721
|
// Sanitize unclosed code fences — they cause CLI to wait for closure on Enter
|
|
2612
2722
|
const fenceCount = (formatted.match(/```/g) || []).length;
|
|
2613
2723
|
if (fenceCount % 2 !== 0) {
|
|
@@ -2625,9 +2735,14 @@ export class Daemon extends EventEmitter {
|
|
|
2625
2735
|
if (windowId && this.controlClient && !this.controlClient.isIdle(windowId)) {
|
|
2626
2736
|
if (status)
|
|
2627
2737
|
this.emit("message_queued", status);
|
|
2628
|
-
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).
|
|
2629
2744
|
handingOffToNativeQueue = true;
|
|
2630
|
-
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");
|
|
2631
2746
|
}
|
|
2632
2747
|
else {
|
|
2633
2748
|
this.logger.debug("CLI busy — queuing message until idle");
|
|
@@ -3280,6 +3395,8 @@ export class Daemon extends EventEmitter {
|
|
|
3280
3395
|
});
|
|
3281
3396
|
}
|
|
3282
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];
|
|
3283
3400
|
return {
|
|
3284
3401
|
workingDirectory: this.config.working_directory,
|
|
3285
3402
|
instanceDir: this.instanceDir,
|
|
@@ -3299,6 +3416,7 @@ export class Daemon extends EventEmitter {
|
|
|
3299
3416
|
instructions,
|
|
3300
3417
|
agentMode: isCliMode ? "cli" : "mcp",
|
|
3301
3418
|
agentPort: isCliMode ? agentPort : undefined,
|
|
3419
|
+
backendOptions,
|
|
3302
3420
|
};
|
|
3303
3421
|
}
|
|
3304
3422
|
/**
|
|
@@ -3395,8 +3513,6 @@ export class Daemon extends EventEmitter {
|
|
|
3395
3513
|
this.beginSpawn();
|
|
3396
3514
|
let resumedSuccessfully = false;
|
|
3397
3515
|
try {
|
|
3398
|
-
this.toolStatusLines = [];
|
|
3399
|
-
this.toolStatusMessageId = null;
|
|
3400
3516
|
if (!this.backend) {
|
|
3401
3517
|
throw new Error("No backend configured — cannot spawn CLI window");
|
|
3402
3518
|
}
|
|
@@ -3664,9 +3780,29 @@ export class Daemon extends EventEmitter {
|
|
|
3664
3780
|
// back from statusline.json — that would re-arm --continue and re-loop.
|
|
3665
3781
|
if (this.skipResume)
|
|
3666
3782
|
return;
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
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
|
+
}
|
|
3670
3806
|
}
|
|
3671
3807
|
}
|
|
3672
3808
|
readContextPercentage() {
|