@songsid/agend 2.1.4-beta.7 → 2.1.4-beta.9
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/channel/adapters/discord.js +7 -0
- 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/config-validator.js +3 -0
- package/dist/config-validator.js.map +1 -1
- package/dist/daemon.d.ts +50 -0
- package/dist/daemon.js +173 -34
- package/dist/daemon.js.map +1 -1
- package/dist/fleet-manager.d.ts +60 -1
- package/dist/fleet-manager.js +416 -195
- 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 +2 -0
- package/dist/instance-lifecycle.js +1 -0
- 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 +10 -0
- package/dist/locale.js.map +1 -1
- package/dist/outbound-schemas.d.ts +1 -0
- package/dist/outbound-schemas.js +1 -0
- package/dist/outbound-schemas.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 +15 -0
- package/dist/topic-commands.js +56 -0
- 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 +6 -0
- package/dist/workflow-templates/default.md +2 -1
- package/package.json +1 -1
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";
|
|
@@ -711,6 +713,17 @@ export class Daemon extends EventEmitter {
|
|
|
711
713
|
lastBuiltInstructions = "";
|
|
712
714
|
pasteQueueDepth = 0;
|
|
713
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;
|
|
714
727
|
// PTY error pattern monitoring
|
|
715
728
|
errorMonitorTimer = null;
|
|
716
729
|
interactivePromptDetector = new InteractivePromptDetector();
|
|
@@ -930,6 +943,10 @@ export class Daemon extends EventEmitter {
|
|
|
930
943
|
});
|
|
931
944
|
}
|
|
932
945
|
}
|
|
946
|
+
else if (msg.type === "steer") {
|
|
947
|
+
const meta = (msg.meta ?? {});
|
|
948
|
+
this.steerMessage(msg.content, meta);
|
|
949
|
+
}
|
|
933
950
|
else if (msg.type === "fleet_schedule_trigger") {
|
|
934
951
|
const payload = msg.payload;
|
|
935
952
|
const meta = msg.meta;
|
|
@@ -1033,8 +1050,11 @@ export class Daemon extends EventEmitter {
|
|
|
1033
1050
|
const outputLog = join(this.instanceDir, "output.log");
|
|
1034
1051
|
rotateLogIfNeeded(outputLog);
|
|
1035
1052
|
await this.tmux.pipeOutput(outputLog).catch(() => { });
|
|
1036
|
-
// 4. Transcript monitor
|
|
1037
|
-
|
|
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));
|
|
1038
1058
|
// 5. Wire transcript events
|
|
1039
1059
|
const ackIfPending = () => {
|
|
1040
1060
|
if (!this.pendingAckMessage || !this.adapter)
|
|
@@ -1050,6 +1070,7 @@ export class Daemon extends EventEmitter {
|
|
|
1050
1070
|
this.recordRecentEvent({ type: "tool_use", name, preview: this.summarizeTool(name, input) });
|
|
1051
1071
|
this.recordRecentToolActivity(this.summarizeTool(name, input));
|
|
1052
1072
|
this.publishActivity(this.summarizeTool(name, input));
|
|
1073
|
+
this.recordToolProgress(name, input);
|
|
1053
1074
|
});
|
|
1054
1075
|
this.transcriptMonitor.on("tool_result", (name, _output) => {
|
|
1055
1076
|
this.recordRecentEvent({ type: "tool_result", name });
|
|
@@ -2021,6 +2042,7 @@ export class Daemon extends EventEmitter {
|
|
|
2021
2042
|
// tool_result (interrupted, crashed, cancelled), which would otherwise leave
|
|
2022
2043
|
// the last tool pinned to the progress line for the rest of the session.
|
|
2023
2044
|
this.publishActivity(null);
|
|
2045
|
+
this.resetToolProgress();
|
|
2024
2046
|
// Must run before the mcpRestartPending branch below: the pane text is the
|
|
2025
2047
|
// only copy of the answer, and the revival restart is about to clear it.
|
|
2026
2048
|
this.maybeProxyReplyOnTurnEnd(pane);
|
|
@@ -2418,6 +2440,67 @@ export class Daemon extends EventEmitter {
|
|
|
2418
2440
|
activity: next,
|
|
2419
2441
|
});
|
|
2420
2442
|
}
|
|
2443
|
+
/** Effective tool_progress level, hardened against junk config values. */
|
|
2444
|
+
toolProgressLevel() {
|
|
2445
|
+
const raw = this.config.tool_progress;
|
|
2446
|
+
return raw === "off" || raw === "verbose" ? raw : "standard";
|
|
2447
|
+
}
|
|
2448
|
+
/**
|
|
2449
|
+
* Accumulate one semantic progress line for the channel bubble and schedule
|
|
2450
|
+
* a coalesced broadcast. Separate from publishActivity on purpose: that one
|
|
2451
|
+
* is the single-line statusline detail (terse, operator-facing), this is the
|
|
2452
|
+
* multi-line channel list (semantic, argument-free at `standard`) — see
|
|
2453
|
+
* tool-progress.ts for why the two labellers must not merge.
|
|
2454
|
+
*/
|
|
2455
|
+
recordToolProgress(name, input) {
|
|
2456
|
+
const level = this.toolProgressLevel();
|
|
2457
|
+
if (level === "off")
|
|
2458
|
+
return;
|
|
2459
|
+
if (this.turnProgress.add(summarizeProgress(name, input, level))) {
|
|
2460
|
+
this.scheduleProgressBroadcast();
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
/** Coalesce progress broadcasts: at most one per 3s, trailing edge kept. */
|
|
2464
|
+
scheduleProgressBroadcast() {
|
|
2465
|
+
const MIN_INTERVAL_MS = 3_000;
|
|
2466
|
+
const since = Date.now() - this.lastProgressBroadcastAt;
|
|
2467
|
+
if (since >= MIN_INTERVAL_MS) {
|
|
2468
|
+
this.broadcastToolProgress();
|
|
2469
|
+
return;
|
|
2470
|
+
}
|
|
2471
|
+
if (this.progressBroadcastTimer)
|
|
2472
|
+
return;
|
|
2473
|
+
this.progressBroadcastTimer = setTimeout(() => {
|
|
2474
|
+
this.progressBroadcastTimer = null;
|
|
2475
|
+
this.broadcastToolProgress();
|
|
2476
|
+
}, MIN_INTERVAL_MS - since);
|
|
2477
|
+
this.progressBroadcastTimer.unref?.();
|
|
2478
|
+
}
|
|
2479
|
+
broadcastToolProgress() {
|
|
2480
|
+
const rendered = this.turnProgress.render();
|
|
2481
|
+
if (rendered === this.lastProgressBroadcast)
|
|
2482
|
+
return;
|
|
2483
|
+
this.lastProgressBroadcast = rendered;
|
|
2484
|
+
this.lastProgressBroadcastAt = Date.now();
|
|
2485
|
+
this.ipcServer?.broadcast({
|
|
2486
|
+
type: "instance_progress",
|
|
2487
|
+
instanceName: this.name,
|
|
2488
|
+
progress: rendered,
|
|
2489
|
+
});
|
|
2490
|
+
}
|
|
2491
|
+
/** New turn (or turn over): drop the list and tell the fleet to clear it. */
|
|
2492
|
+
resetToolProgress() {
|
|
2493
|
+
if (this.progressBroadcastTimer) {
|
|
2494
|
+
clearTimeout(this.progressBroadcastTimer);
|
|
2495
|
+
this.progressBroadcastTimer = null;
|
|
2496
|
+
}
|
|
2497
|
+
this.turnProgress.reset();
|
|
2498
|
+
if (this.lastProgressBroadcast !== "") {
|
|
2499
|
+
this.lastProgressBroadcast = "";
|
|
2500
|
+
this.lastProgressBroadcastAt = Date.now();
|
|
2501
|
+
this.ipcServer?.broadcast({ type: "instance_progress", instanceName: this.name, progress: "" });
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2421
2504
|
/**
|
|
2422
2505
|
* Options for every system-initiated paste (startup notice, session snapshot,
|
|
2423
2506
|
* runtime-dialog keys).
|
|
@@ -2453,6 +2536,82 @@ export class Daemon extends EventEmitter {
|
|
|
2453
2536
|
return ""; // skip channel tools
|
|
2454
2537
|
return name;
|
|
2455
2538
|
}
|
|
2539
|
+
/**
|
|
2540
|
+
* The one place an inbound message grows its metadata wrapper ([user:]/
|
|
2541
|
+
* [from:] prefix, pending reactions, handoff metadata, reply instructions).
|
|
2542
|
+
* Both the normal queued path (pushChannelMessage) and /steer go through
|
|
2543
|
+
* here — a steered message must read EXACTLY like a queued one to the
|
|
2544
|
+
* agent, or the two drift apart in what the agent is told about replying.
|
|
2545
|
+
*/
|
|
2546
|
+
formatInboundMessage(content, meta) {
|
|
2547
|
+
const user = meta.user || "unknown";
|
|
2548
|
+
const fromInstance = meta.from_instance;
|
|
2549
|
+
let formatted;
|
|
2550
|
+
if (fromInstance) {
|
|
2551
|
+
// #77: show the sender's display name for readability, keeping the machine
|
|
2552
|
+
// instance name in parens so the recipient's send_to_instance target is valid.
|
|
2553
|
+
const fromLabel = meta.from_display ? `${meta.from_display} (${fromInstance})` : fromInstance;
|
|
2554
|
+
formatted = `[from:${fromLabel}] ${content}`;
|
|
2555
|
+
formatted += renderHandoffMetadata(meta);
|
|
2556
|
+
// A delegated task that requires a reply must not read like a chatty FYI —
|
|
2557
|
+
// the "you may stay silent" line is for the latter only.
|
|
2558
|
+
formatted += meta.requires_reply === "true"
|
|
2559
|
+
? "\n(A reply IS required: use report_result with the correlation_id above — or send_to_instance. Not direct text.)"
|
|
2560
|
+
: "\n(If you need to reply, use send_to_instance tool, NOT direct text. If there is nothing to add, you may stay silent.)";
|
|
2561
|
+
}
|
|
2562
|
+
else {
|
|
2563
|
+
const via = meta.source ? ` via ${meta.source}` : "";
|
|
2564
|
+
const idTag = meta.user_id ? `, id:${meta.user_id}` : "";
|
|
2565
|
+
formatted = `[user:${user}${via}${idTag}] ${content}`;
|
|
2566
|
+
// Reactions queued since the last real message (#432). One leading line of
|
|
2567
|
+
// context, present only when something is pending — a reaction no longer
|
|
2568
|
+
// costs a turn of its own.
|
|
2569
|
+
if (meta.pending_reactions) {
|
|
2570
|
+
formatted = `[Recent reactions: ${meta.pending_reactions}]\n${formatted}`;
|
|
2571
|
+
}
|
|
2572
|
+
formatted += renderHandoffMetadata(meta);
|
|
2573
|
+
formatted += "\n(Reply using the reply tool — do NOT respond with direct text)";
|
|
2574
|
+
}
|
|
2575
|
+
if (meta.reply_to_text) {
|
|
2576
|
+
formatted += `\n(reply_to: "${meta.reply_to_text}")`;
|
|
2577
|
+
}
|
|
2578
|
+
return formatted;
|
|
2579
|
+
}
|
|
2580
|
+
/**
|
|
2581
|
+
* /steer: interject into the CURRENT turn instead of queueing for idle.
|
|
2582
|
+
*
|
|
2583
|
+
* Differences from pushChannelMessage, and nothing else:
|
|
2584
|
+
* - serialized on steerLock, not pasteLock — it must overtake, not queue
|
|
2585
|
+
* - deliverMessage runs with { steer: true }, which takes the busy branch
|
|
2586
|
+
* that pastes immediately (the codex native-queue transaction) instead of
|
|
2587
|
+
* waiting for idle. Verification and silent-loss fallback are the ones
|
|
2588
|
+
* that path already has: pane-capture visibility check, then one
|
|
2589
|
+
* idle-gated redelivery — so on a TUI that swallows busy input the steer
|
|
2590
|
+
* degrades to "next message after this turn", never silently vanishes.
|
|
2591
|
+
*
|
|
2592
|
+
* The steered text goes through formatInboundMessage so the agent sees a
|
|
2593
|
+
* normal inbound message, with a steering notice prepended for context.
|
|
2594
|
+
*/
|
|
2595
|
+
steerMessage(content, meta) {
|
|
2596
|
+
this.updateLastChat(meta.chat_id, meta.thread_id, meta.adapter_id);
|
|
2597
|
+
this.pendingWork.recordInbound();
|
|
2598
|
+
this.recordRecentUserMessage(content, meta);
|
|
2599
|
+
const formatted = "[STEERING — mid-task course correction from the user. Fold this into the CURRENT work.]\n"
|
|
2600
|
+
+ this.formatInboundMessage(content, meta);
|
|
2601
|
+
const chatId = meta.chat_id;
|
|
2602
|
+
const messageId = meta.message_id;
|
|
2603
|
+
const status = (chatId && messageId)
|
|
2604
|
+
? { chatId: meta.thread_id || chatId, messageId }
|
|
2605
|
+
: undefined;
|
|
2606
|
+
this.steerLock = this.steerLock.then(async () => {
|
|
2607
|
+
await this.wake();
|
|
2608
|
+
if (await this.deliverMessage(formatted, status, { steer: true })) {
|
|
2609
|
+
this.markTurnStarted(meta, formatted);
|
|
2610
|
+
}
|
|
2611
|
+
}).catch(err => {
|
|
2612
|
+
this.logger.warn({ err: err.message }, "steer delivery error");
|
|
2613
|
+
});
|
|
2614
|
+
}
|
|
2456
2615
|
/**
|
|
2457
2616
|
* Push an inbound channel message to a specific MCP session.
|
|
2458
2617
|
* If targetSession is provided, only send to the matching socket.
|
|
@@ -2499,35 +2658,7 @@ export class Daemon extends EventEmitter {
|
|
|
2499
2658
|
});
|
|
2500
2659
|
return;
|
|
2501
2660
|
}
|
|
2502
|
-
|
|
2503
|
-
if (fromInstance) {
|
|
2504
|
-
// #77: show the sender's display name for readability, keeping the machine
|
|
2505
|
-
// instance name in parens so the recipient's send_to_instance target is valid.
|
|
2506
|
-
const fromLabel = meta.from_display ? `${meta.from_display} (${fromInstance})` : fromInstance;
|
|
2507
|
-
formatted = `[from:${fromLabel}] ${content}`;
|
|
2508
|
-
formatted += renderHandoffMetadata(meta);
|
|
2509
|
-
// A delegated task that requires a reply must not read like a chatty FYI —
|
|
2510
|
-
// the "you may stay silent" line is for the latter only.
|
|
2511
|
-
formatted += meta.requires_reply === "true"
|
|
2512
|
-
? "\n(A reply IS required: use report_result with the correlation_id above — or send_to_instance. Not direct text.)"
|
|
2513
|
-
: "\n(If you need to reply, use send_to_instance tool, NOT direct text. If there is nothing to add, you may stay silent.)";
|
|
2514
|
-
}
|
|
2515
|
-
else {
|
|
2516
|
-
const via = meta.source ? ` via ${meta.source}` : "";
|
|
2517
|
-
const idTag = meta.user_id ? `, id:${meta.user_id}` : "";
|
|
2518
|
-
formatted = `[user:${user}${via}${idTag}] ${content}`;
|
|
2519
|
-
// Reactions queued since the last real message (#432). One leading line of
|
|
2520
|
-
// context, present only when something is pending — a reaction no longer
|
|
2521
|
-
// costs a turn of its own.
|
|
2522
|
-
if (meta.pending_reactions) {
|
|
2523
|
-
formatted = `[Recent reactions: ${meta.pending_reactions}]\n${formatted}`;
|
|
2524
|
-
}
|
|
2525
|
-
formatted += renderHandoffMetadata(meta);
|
|
2526
|
-
formatted += "\n(Reply using the reply tool — do NOT respond with direct text)";
|
|
2527
|
-
}
|
|
2528
|
-
if (meta.reply_to_text) {
|
|
2529
|
-
formatted += `\n(reply_to: "${meta.reply_to_text}")`;
|
|
2530
|
-
}
|
|
2661
|
+
const formatted = this.formatInboundMessage(content, meta);
|
|
2531
2662
|
// Serialize deliveries: each message waits for the previous to complete,
|
|
2532
2663
|
// and each waits for the CLI to be idle before pasting. Messages are never
|
|
2533
2664
|
// dropped for age — a long-busy CLI just queues them until it frees up
|
|
@@ -2554,6 +2685,9 @@ export class Daemon extends EventEmitter {
|
|
|
2554
2685
|
const status = (chatId && messageId)
|
|
2555
2686
|
? { chatId: meta.thread_id || chatId, messageId }
|
|
2556
2687
|
: undefined;
|
|
2688
|
+
// A fresh delivery begins a fresh turn — its bubble must not inherit
|
|
2689
|
+
// the previous turn's tool list.
|
|
2690
|
+
this.resetToolProgress();
|
|
2557
2691
|
if (await this.deliverMessage(formatted, status))
|
|
2558
2692
|
this.markTurnStarted(meta, formatted);
|
|
2559
2693
|
}
|
|
@@ -2581,7 +2715,7 @@ export class Daemon extends EventEmitter {
|
|
|
2581
2715
|
* immediately and own the application-level queue themselves. The pasteLock remains
|
|
2582
2716
|
* serial in both cases so separate PTY writes can never overlap.
|
|
2583
2717
|
*/
|
|
2584
|
-
async deliverMessage(formatted, status) {
|
|
2718
|
+
async deliverMessage(formatted, status, opts) {
|
|
2585
2719
|
// Sanitize unclosed code fences — they cause CLI to wait for closure on Enter
|
|
2586
2720
|
const fenceCount = (formatted.match(/```/g) || []).length;
|
|
2587
2721
|
if (fenceCount % 2 !== 0) {
|
|
@@ -2599,9 +2733,14 @@ export class Daemon extends EventEmitter {
|
|
|
2599
2733
|
if (windowId && this.controlClient && !this.controlClient.isIdle(windowId)) {
|
|
2600
2734
|
if (status)
|
|
2601
2735
|
this.emit("message_queued", status);
|
|
2602
|
-
if (supportsQueuedInput) {
|
|
2736
|
+
if (supportsQueuedInput || opts?.steer) {
|
|
2737
|
+
// Native queue (codex), or an explicit /steer: hand the complete
|
|
2738
|
+
// paste+Enter transaction to the busy CLI now. For steer this is the
|
|
2739
|
+
// point — the user asked to interject, and the transaction's
|
|
2740
|
+
// visibility check + idle-gated fallback catch TUIs that swallow
|
|
2741
|
+
// busy input (see steerMessage).
|
|
2603
2742
|
handingOffToNativeQueue = true;
|
|
2604
|
-
this.logger.debug("CLI busy — handing message to backend-native input queue");
|
|
2743
|
+
this.logger.debug(opts?.steer ? "CLI busy — steering into the running turn" : "CLI busy — handing message to backend-native input queue");
|
|
2605
2744
|
}
|
|
2606
2745
|
else {
|
|
2607
2746
|
this.logger.debug("CLI busy — queuing message until idle");
|