@slock-ai/daemon 0.40.1 → 0.41.0
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/chat-bridge.js +1041 -758
- package/dist/{chunk-E6OOH3IC.js → chunk-JG7ONJZ6.js} +46 -46
- package/dist/{chunk-6YLMU56U.js → chunk-KFVDXO5Y.js} +779 -51
- package/dist/cli/index.js +170 -1
- package/dist/core.js +2 -2
- package/dist/index.js +2 -2
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
buildWebSocketOptions,
|
|
3
3
|
logger
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-JG7ONJZ6.js";
|
|
5
5
|
|
|
6
6
|
// src/core.ts
|
|
7
7
|
import path11 from "path";
|
|
@@ -10,6 +10,109 @@ import { createRequire } from "module";
|
|
|
10
10
|
import { accessSync } from "fs";
|
|
11
11
|
import { fileURLToPath } from "url";
|
|
12
12
|
|
|
13
|
+
// ../shared/src/tracing/index.ts
|
|
14
|
+
var DEFAULT_TRACE_FLAGS = "00";
|
|
15
|
+
var TRACEPARENT_VERSION = "00";
|
|
16
|
+
var TRACE_ID_HEX_LENGTH = 32;
|
|
17
|
+
var SPAN_ID_HEX_LENGTH = 16;
|
|
18
|
+
var TRACE_FLAGS_HEX_LENGTH = 2;
|
|
19
|
+
var TRACE_ID_PATTERN = /^[0-9a-f]{32}$/;
|
|
20
|
+
var SPAN_ID_PATTERN = /^[0-9a-f]{16}$/;
|
|
21
|
+
var TRACE_FLAGS_PATTERN = /^[0-9a-f]{2}$/;
|
|
22
|
+
var TRACEPARENT_PATTERN = /^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/;
|
|
23
|
+
function isTraceId(value) {
|
|
24
|
+
return TRACE_ID_PATTERN.test(value) && value !== "0".repeat(TRACE_ID_HEX_LENGTH);
|
|
25
|
+
}
|
|
26
|
+
function isSpanId(value) {
|
|
27
|
+
return SPAN_ID_PATTERN.test(value) && value !== "0".repeat(SPAN_ID_HEX_LENGTH);
|
|
28
|
+
}
|
|
29
|
+
function isTraceFlags(value) {
|
|
30
|
+
return TRACE_FLAGS_PATTERN.test(value);
|
|
31
|
+
}
|
|
32
|
+
function assertTraceContext(context) {
|
|
33
|
+
if (!isTraceId(context.traceId)) {
|
|
34
|
+
throw new Error(`Invalid traceId: expected ${TRACE_ID_HEX_LENGTH} lowercase hex chars`);
|
|
35
|
+
}
|
|
36
|
+
if (!isSpanId(context.spanId)) {
|
|
37
|
+
throw new Error(`Invalid spanId: expected ${SPAN_ID_HEX_LENGTH} lowercase hex chars`);
|
|
38
|
+
}
|
|
39
|
+
if (context.parentSpanId !== null && !isSpanId(context.parentSpanId)) {
|
|
40
|
+
throw new Error(`Invalid parentSpanId: expected null or ${SPAN_ID_HEX_LENGTH} lowercase hex chars`);
|
|
41
|
+
}
|
|
42
|
+
if (!isTraceFlags(context.traceFlags)) {
|
|
43
|
+
throw new Error(`Invalid traceFlags: expected ${TRACE_FLAGS_HEX_LENGTH} lowercase hex chars`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function formatTraceparent(context) {
|
|
47
|
+
assertTraceContext(context);
|
|
48
|
+
return `${TRACEPARENT_VERSION}-${context.traceId}-${context.spanId}-${context.traceFlags}`;
|
|
49
|
+
}
|
|
50
|
+
function parseTraceparent(value) {
|
|
51
|
+
if (!value) return null;
|
|
52
|
+
const match = TRACEPARENT_PATTERN.exec(value);
|
|
53
|
+
if (!match) return null;
|
|
54
|
+
const [, version, traceId, spanId, traceFlags] = match;
|
|
55
|
+
if (version !== TRACEPARENT_VERSION) return null;
|
|
56
|
+
if (!isTraceId(traceId) || !isSpanId(spanId) || !isTraceFlags(traceFlags)) return null;
|
|
57
|
+
return {
|
|
58
|
+
traceId,
|
|
59
|
+
spanId,
|
|
60
|
+
parentSpanId: null,
|
|
61
|
+
traceFlags
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function createTraceContext({
|
|
65
|
+
parent = null,
|
|
66
|
+
traceId,
|
|
67
|
+
spanId,
|
|
68
|
+
traceFlags,
|
|
69
|
+
traceIdGenerator = generateTraceId,
|
|
70
|
+
spanIdGenerator = generateSpanId
|
|
71
|
+
} = {}) {
|
|
72
|
+
const context = {
|
|
73
|
+
traceId: traceId ?? parent?.traceId ?? traceIdGenerator(),
|
|
74
|
+
spanId: spanId ?? spanIdGenerator(),
|
|
75
|
+
parentSpanId: parent?.spanId ?? null,
|
|
76
|
+
traceFlags: traceFlags ?? parent?.traceFlags ?? DEFAULT_TRACE_FLAGS
|
|
77
|
+
};
|
|
78
|
+
assertTraceContext(context);
|
|
79
|
+
return context;
|
|
80
|
+
}
|
|
81
|
+
var NoopTracer = class {
|
|
82
|
+
startSpan(_name, options) {
|
|
83
|
+
return new NoopActiveSpan(createTraceContext({ parent: options.parent ?? null }));
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
var NoopActiveSpan = class {
|
|
87
|
+
context;
|
|
88
|
+
constructor(context) {
|
|
89
|
+
this.context = context;
|
|
90
|
+
}
|
|
91
|
+
addEvent() {
|
|
92
|
+
}
|
|
93
|
+
end() {
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
var noopTracer = new NoopTracer();
|
|
97
|
+
function generateTraceId() {
|
|
98
|
+
return randomNonZeroHex(TRACE_ID_HEX_LENGTH);
|
|
99
|
+
}
|
|
100
|
+
function generateSpanId() {
|
|
101
|
+
return randomNonZeroHex(SPAN_ID_HEX_LENGTH);
|
|
102
|
+
}
|
|
103
|
+
function randomNonZeroHex(length) {
|
|
104
|
+
let value = randomHex(length);
|
|
105
|
+
while (value === "0".repeat(length)) {
|
|
106
|
+
value = randomHex(length);
|
|
107
|
+
}
|
|
108
|
+
return value;
|
|
109
|
+
}
|
|
110
|
+
function randomHex(length) {
|
|
111
|
+
const bytes = new Uint8Array(length / 2);
|
|
112
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
113
|
+
return [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
114
|
+
}
|
|
115
|
+
|
|
13
116
|
// ../shared/src/toolDisplay.ts
|
|
14
117
|
var TOOL_DISPLAY_METADATA = {
|
|
15
118
|
send_message: { logLabel: "Sending message", activityLabel: "Sending message\u2026", summaryKind: "message_target" },
|
|
@@ -272,6 +375,10 @@ function resolveSlockCliInvocation(toolName, input) {
|
|
|
272
375
|
return { toolName: "search_messages", input: { query: readOptionValue(rest, "--query") } };
|
|
273
376
|
case "server info":
|
|
274
377
|
return { toolName: "list_server", input: {} };
|
|
378
|
+
case "channel members":
|
|
379
|
+
return { toolName: "list_channel_members", input: { channel: rest[0] } };
|
|
380
|
+
case "channel leave":
|
|
381
|
+
return { toolName: "leave_channel", input: { target: readOptionValue(rest, "--target") } };
|
|
275
382
|
case "task list":
|
|
276
383
|
return { toolName: "list_tasks", input: { channel: readOptionValue(rest, "--channel") } };
|
|
277
384
|
case "task create":
|
|
@@ -481,6 +588,7 @@ import os3 from "os";
|
|
|
481
588
|
|
|
482
589
|
// src/drivers/claude.ts
|
|
483
590
|
import { spawn } from "child_process";
|
|
591
|
+
import { writeFileSync as writeFileSync2 } from "fs";
|
|
484
592
|
import path3 from "path";
|
|
485
593
|
|
|
486
594
|
// src/drivers/cliTransport.ts
|
|
@@ -533,20 +641,23 @@ Use the \`slock\` CLI for chat / task / attachment operations. The daemon inject
|
|
|
533
641
|
1. **\`slock message check\`** \u2014 Non-blocking check for new messages. Use freely during work \u2014 at natural breakpoints or after notifications.
|
|
534
642
|
2. **\`slock message send\`** \u2014 Send a message to a channel or DM.
|
|
535
643
|
3. **\`slock server info\`** \u2014 List channels in this server, which ones you have joined, plus all agents and humans.
|
|
536
|
-
4. **\`slock
|
|
537
|
-
5. **\`slock
|
|
538
|
-
6. **\`slock
|
|
539
|
-
7. **\`slock
|
|
540
|
-
8. **\`slock task
|
|
541
|
-
9. **\`slock task
|
|
542
|
-
10. **\`slock task
|
|
543
|
-
11. **\`slock
|
|
544
|
-
12. **\`slock
|
|
545
|
-
13. **\`slock
|
|
546
|
-
14. **\`slock
|
|
547
|
-
15. **\`slock reminder
|
|
644
|
+
4. **\`slock channel leave\`** \u2014 Leave a regular channel you have joined. This only affects your own agent membership.
|
|
645
|
+
5. **\`slock thread unfollow\`** \u2014 Stop receiving ordinary delivery for a thread you no longer need to follow. This only affects your own agent attention state.
|
|
646
|
+
6. **\`slock message read\`** \u2014 Read past messages from a channel, DM, or thread. Supports \`before\` / \`after\` pagination and \`around\` for centered context.
|
|
647
|
+
7. **\`slock message search\`** \u2014 Search messages visible to you, then inspect a hit with \`slock message read\`.
|
|
648
|
+
8. **\`slock task list\`** \u2014 View a channel's task board.
|
|
649
|
+
9. **\`slock task create\`** \u2014 Create new task-messages in a channel (supports batch titles; equivalent to sending a new message and publishing it as a task-message, not claiming it for yourself).
|
|
650
|
+
10. **\`slock task claim\`** \u2014 Claim tasks by number or message ID (supports batch, handles conflicts).
|
|
651
|
+
11. **\`slock task unclaim\`** \u2014 Release your claim on a task.
|
|
652
|
+
12. **\`slock task update\`** \u2014 Change a task's status (e.g. to in_review or done).
|
|
653
|
+
13. **\`slock attachment upload\`** \u2014 Upload a file to attach to a message. Returns an attachment ID to pass to \`slock message send\`.
|
|
654
|
+
14. **\`slock attachment view\`** \u2014 Download an attached file by its attachment ID so you can inspect it locally.
|
|
655
|
+
15. **\`slock reminder schedule\`** \u2014 Schedule a reminder for yourself later, at a specific time, or on a recurring cadence.
|
|
656
|
+
16. **\`slock reminder list\`** \u2014 List your reminders.
|
|
657
|
+
17. **\`slock reminder cancel\`** \u2014 Cancel one of your reminders by ID.
|
|
548
658
|
|
|
549
659
|
When a user asks you to remind them later, at a specific time, or on a recurring schedule, prefer the reminder commands instead of relying on MEMORY or manual follow-up.
|
|
660
|
+
Do not use runtime-native wake or cron tools such as ScheduleWakeup or CronCreate for user-visible reminders; use \`slock reminder schedule\` so reminders stay anchored, observable, and cancelable in Slock.
|
|
550
661
|
For agent-created reminders, first resolve the anchor message from the current conversation and pass its \`msgId\` explicitly. If you cannot resolve a message id, do not create the reminder.
|
|
551
662
|
|
|
552
663
|
The CLI prints human-readable canonical text on success (matching the format you see in received messages and history). On failure it prints JSON to stderr:
|
|
@@ -562,15 +673,16 @@ You have MCP tools from the "chat" server. Use ONLY these for communication:
|
|
|
562
673
|
1. **${checkCmd}** \u2014 Non-blocking check for new messages. Use freely during work \u2014 at natural breakpoints or after notifications.
|
|
563
674
|
2. **${sendCmd}** \u2014 Send a message to a channel or DM.
|
|
564
675
|
3. **${serverInfoCmd}** \u2014 List all channels in this server, which ones you have joined, plus all agents and humans.
|
|
565
|
-
4.
|
|
566
|
-
5.
|
|
567
|
-
6. **\`${t("
|
|
568
|
-
7.
|
|
569
|
-
8. **${
|
|
570
|
-
9.
|
|
571
|
-
10.
|
|
572
|
-
11.
|
|
573
|
-
12. **\`${t("
|
|
676
|
+
4. **\`${t("leave_channel")}\`** \u2014 Leave a regular channel you have joined. This only affects your own agent membership.
|
|
677
|
+
5. **${readCmd}** \u2014 Read past messages from a channel, DM, or thread. Supports \`before\` / \`after\` pagination and \`around\` for centered context.
|
|
678
|
+
6. **\`${t("search_messages")}\`** \u2014 Search messages visible to you, then inspect a hit with ${readCmd}.
|
|
679
|
+
7. **\`${t("list_tasks")}\`** \u2014 View a channel's task board.
|
|
680
|
+
8. **${taskCreateCmd}** \u2014 Create new task-messages in a channel (supports batch titles; equivalent to sending a new message and publishing it as a task-message, not claiming it for yourself).
|
|
681
|
+
9. **${taskClaimCmd}** \u2014 Claim tasks by number or message ID (supports batch, handles conflicts).
|
|
682
|
+
10. **\`${t("unclaim_task")}\`** \u2014 Release your claim on a task.
|
|
683
|
+
11. **${taskUpdateCmd}** \u2014 Change a task's status (e.g. to in_review or done).
|
|
684
|
+
12. **\`${t("upload_file")}\`** \u2014 Upload a file to attach to a message. Returns an attachment ID to pass to ${sendCmd}.
|
|
685
|
+
13. **\`${t("view_file")}\`** \u2014 Download an attached file by its attachment ID so you can inspect it locally.`;
|
|
574
686
|
const sendingMessagesSection = isCli ? `### Sending messages
|
|
575
687
|
|
|
576
688
|
- **Reply to a channel**: \`slock message send --target "#channel-name" <<'EOF'\` followed by the message body and \`EOF\`
|
|
@@ -602,6 +714,7 @@ Threads are sub-conversations attached to a specific message. They let you discu
|
|
|
602
714
|
- **Start a new thread**: Use the \`msg=\` field from the header as the thread suffix. For example, if you see \`[target=#general msg=a1b2c3d4 ...]\`, reply with \`slock message send --target "#general:a1b2c3d4" <<'EOF'\` followed by the message body and \`EOF\`. The thread will be auto-created if it doesn't exist yet.
|
|
603
715
|
- When you send a message, the response includes the message ID. You can use it to start a thread on your own message.
|
|
604
716
|
- You can read thread history: \`slock message read --channel "#general:a1b2c3d4"\`
|
|
717
|
+
- You can stop receiving ordinary delivery for a thread with \`slock thread unfollow --target "#general:a1b2c3d4"\`. Only do this when your work in that thread is clearly complete or no longer relevant.
|
|
605
718
|
- Threads cannot be nested \u2014 you cannot start a thread inside a thread.` : `### Threads
|
|
606
719
|
|
|
607
720
|
Threads are sub-conversations attached to a specific message. They let you discuss a topic without cluttering the main channel.
|
|
@@ -615,10 +728,10 @@ Threads are sub-conversations attached to a specific message. They let you discu
|
|
|
615
728
|
const discoverySection = isCli ? `### Discovering people and channels
|
|
616
729
|
|
|
617
730
|
Call \`slock server info\` to see all channels in this server, which ones you have joined, other agents, and humans.
|
|
618
|
-
Visible public channels may appear even when \`joined=false\`. In that state you can still inspect them with \`slock message read\`, but you cannot send messages there or receive ordinary channel delivery until a human adds you to the channel
|
|
731
|
+
Visible public channels may appear even when \`joined=false\`. In that state you can still inspect them with \`slock message read\`, but you cannot send messages there or receive ordinary channel delivery until a human adds you to the channel. To leave a regular channel you have joined, use \`slock channel leave --target "#channel-name"\`. To stop following a thread without leaving its parent channel, use \`slock thread unfollow --target "#channel-name:shortid"\`.` : `### Discovering people and channels
|
|
619
732
|
|
|
620
733
|
Call ${serverInfoCmd} to see all channels in this server, which ones you have joined, other agents, and humans.
|
|
621
|
-
Visible public channels may appear even when \`joined=false\`. In that state you can still inspect them with ${readCmd}, but you cannot send messages there or receive ordinary channel delivery until a human adds you to the channel
|
|
734
|
+
Visible public channels may appear even when \`joined=false\`. In that state you can still inspect them with ${readCmd}, but you cannot send messages there or receive ordinary channel delivery until a human adds you to the channel. To leave a regular channel you have joined, use \`${t("leave_channel")}\`.`;
|
|
622
735
|
const channelAwarenessSection = isCli ? `### Channel awareness
|
|
623
736
|
|
|
624
737
|
Each channel has a **name** and optionally a **description** that define its purpose (visible via \`slock server info\`). Respect them:
|
|
@@ -962,6 +1075,7 @@ exec ${shellSingleQuote(process.execPath)} ${shellSingleQuote(ctx.slockCliPath)}
|
|
|
962
1075
|
...ctx.config.envVars || {},
|
|
963
1076
|
...extraEnv,
|
|
964
1077
|
SLOCK_AGENT_ID: ctx.agentId,
|
|
1078
|
+
...ctx.launchId ? { SLOCK_AGENT_LAUNCH_ID: ctx.launchId } : {},
|
|
965
1079
|
SLOCK_SERVER_URL: ctx.config.serverUrl,
|
|
966
1080
|
SLOCK_AGENT_TOKEN_FILE: tokenFile,
|
|
967
1081
|
PATH: `${slockDir}${path.delimiter}${process.env.PATH ?? ""}`
|
|
@@ -1049,9 +1163,12 @@ function resolveHomePath(relativePath, deps = {}) {
|
|
|
1049
1163
|
// src/drivers/claude.ts
|
|
1050
1164
|
var CLAUDE_DESKTOP_CLI_RELATIVE_PATH = path3.join("Applications", "Claude Code URL Handler.app", "Contents", "MacOS", "claude");
|
|
1051
1165
|
var CLAUDE_DESKTOP_CLI_SYSTEM_PATH = "/Applications/Claude Code URL Handler.app/Contents/MacOS/claude";
|
|
1166
|
+
var CLAUDE_SYSTEM_PROMPT_FILE = "claude-system-prompt.md";
|
|
1167
|
+
var CLAUDE_MCP_CONFIG_FILE = "claude-mcp-config.json";
|
|
1052
1168
|
var CLAUDE_DISALLOWED_TOOLS = [
|
|
1053
1169
|
"EnterPlanMode",
|
|
1054
1170
|
"ExitPlanMode",
|
|
1171
|
+
"ScheduleWakeup",
|
|
1055
1172
|
"CronCreate",
|
|
1056
1173
|
"CronList",
|
|
1057
1174
|
"CronDelete"
|
|
@@ -1077,12 +1194,15 @@ var ClaudeDriver = class {
|
|
|
1077
1194
|
id = "claude";
|
|
1078
1195
|
supportsStdinNotification = true;
|
|
1079
1196
|
mcpToolPrefix = "mcp__chat__";
|
|
1080
|
-
|
|
1197
|
+
// Claude Code supports same-turn steering, but raw stdin injection at an
|
|
1198
|
+
// arbitrary busy instant can collide with active signed thinking blocks. The
|
|
1199
|
+
// daemon therefore gates busy delivery on Claude stream-json boundaries.
|
|
1200
|
+
busyDeliveryMode = "gated";
|
|
1081
1201
|
supportsNativeStandingPrompt = true;
|
|
1082
1202
|
probe() {
|
|
1083
1203
|
return probeClaude();
|
|
1084
1204
|
}
|
|
1085
|
-
buildClaudeArgs(config, standingPrompt) {
|
|
1205
|
+
buildClaudeArgs(config, standingPrompt, opts = {}) {
|
|
1086
1206
|
const args = [
|
|
1087
1207
|
"--allow-dangerously-skip-permissions",
|
|
1088
1208
|
"--dangerously-skip-permissions",
|
|
@@ -1091,21 +1211,60 @@ var ClaudeDriver = class {
|
|
|
1091
1211
|
"stream-json",
|
|
1092
1212
|
"--input-format",
|
|
1093
1213
|
"stream-json",
|
|
1094
|
-
"--append-system-prompt",
|
|
1095
|
-
standingPrompt,
|
|
1096
1214
|
"--model",
|
|
1097
1215
|
config.model || "sonnet",
|
|
1098
1216
|
"--disallowed-tools",
|
|
1099
1217
|
CLAUDE_DISALLOWED_TOOLS
|
|
1100
1218
|
];
|
|
1219
|
+
if (opts.standingPromptFilePath) {
|
|
1220
|
+
args.push("--append-system-prompt-file", opts.standingPromptFilePath);
|
|
1221
|
+
} else {
|
|
1222
|
+
args.push("--append-system-prompt", standingPrompt);
|
|
1223
|
+
}
|
|
1101
1224
|
if (config.sessionId) {
|
|
1102
1225
|
args.push("--resume", config.sessionId);
|
|
1103
1226
|
}
|
|
1104
1227
|
return args;
|
|
1105
1228
|
}
|
|
1229
|
+
buildDeprecatedShimMcpConfig(ctx) {
|
|
1230
|
+
const isTsSource = ctx.chatBridgePath.endsWith(".ts");
|
|
1231
|
+
const command = isTsSource ? "npx" : "node";
|
|
1232
|
+
const bridgeArgs = isTsSource ? ["tsx", ctx.chatBridgePath] : [ctx.chatBridgePath];
|
|
1233
|
+
return JSON.stringify({
|
|
1234
|
+
mcpServers: {
|
|
1235
|
+
chat: {
|
|
1236
|
+
command,
|
|
1237
|
+
args: [
|
|
1238
|
+
...bridgeArgs,
|
|
1239
|
+
"--agent-id",
|
|
1240
|
+
ctx.agentId,
|
|
1241
|
+
"--server-url",
|
|
1242
|
+
ctx.config.serverUrl,
|
|
1243
|
+
"--auth-token",
|
|
1244
|
+
ctx.config.authToken || ctx.daemonApiKey,
|
|
1245
|
+
"--runtime",
|
|
1246
|
+
this.id,
|
|
1247
|
+
...ctx.launchId ? ["--launch-id", ctx.launchId] : [],
|
|
1248
|
+
"--deprecated-shim"
|
|
1249
|
+
]
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
writeClaudeLaunchFiles(ctx, slockDir) {
|
|
1255
|
+
const systemPromptPath = path3.join(slockDir, CLAUDE_SYSTEM_PROMPT_FILE);
|
|
1256
|
+
const mcpConfigPath = path3.join(slockDir, CLAUDE_MCP_CONFIG_FILE);
|
|
1257
|
+
writeFileSync2(systemPromptPath, ctx.standingPrompt, { mode: 384 });
|
|
1258
|
+
writeFileSync2(mcpConfigPath, this.buildDeprecatedShimMcpConfig(ctx), { mode: 384 });
|
|
1259
|
+
return { systemPromptPath, mcpConfigPath };
|
|
1260
|
+
}
|
|
1106
1261
|
spawn(ctx) {
|
|
1107
|
-
const { tokenFile, spawnEnv } = prepareCliTransport(ctx);
|
|
1108
|
-
const
|
|
1262
|
+
const { slockDir, tokenFile, spawnEnv } = prepareCliTransport(ctx);
|
|
1263
|
+
const { systemPromptPath, mcpConfigPath } = this.writeClaudeLaunchFiles(ctx, slockDir);
|
|
1264
|
+
const args = this.buildClaudeArgs(ctx.config, ctx.standingPrompt, {
|
|
1265
|
+
standingPromptFilePath: systemPromptPath
|
|
1266
|
+
});
|
|
1267
|
+
args.push("--mcp-config", mcpConfigPath);
|
|
1109
1268
|
delete spawnEnv.CLAUDECODE;
|
|
1110
1269
|
logger.info(
|
|
1111
1270
|
`[Agent ${ctx.agentId}] transport=cli cli=${ctx.slockCliPath} token_file=${tokenFile}`
|
|
@@ -1175,6 +1334,17 @@ var ClaudeDriver = class {
|
|
|
1175
1334
|
}
|
|
1176
1335
|
break;
|
|
1177
1336
|
}
|
|
1337
|
+
case "user": {
|
|
1338
|
+
const content = event.message?.content;
|
|
1339
|
+
if (Array.isArray(content)) {
|
|
1340
|
+
for (const block of content) {
|
|
1341
|
+
if (block.type === "tool_result") {
|
|
1342
|
+
events.push({ kind: "tool_output", name: block.name || block.tool_use_id || "tool_result" });
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
break;
|
|
1347
|
+
}
|
|
1178
1348
|
case "result": {
|
|
1179
1349
|
const subtype = typeof event.subtype === "string" ? event.subtype : "success";
|
|
1180
1350
|
const stopReason = typeof event.stop_reason === "string" ? event.stop_reason : null;
|
|
@@ -1219,9 +1389,12 @@ var ClaudeDriver = class {
|
|
|
1219
1389
|
return buildCliTransportSystemPrompt(config, {
|
|
1220
1390
|
toolPrefix: "mcp__chat__",
|
|
1221
1391
|
extraCriticalRules: [],
|
|
1222
|
-
postStartupNotes: [
|
|
1392
|
+
postStartupNotes: [
|
|
1393
|
+
"**Claude runtime note:** Slock preserves Claude Code same-turn steering through a gated stream-json delivery path. Busy messages are buffered and delivered at Claude-observed safe boundaries; if no earlier safe boundary is available, they are delivered after the current turn ends.",
|
|
1394
|
+
"For long tool runs, you can also use `slock message check` at natural breakpoints to pull pending messages explicitly."
|
|
1395
|
+
],
|
|
1223
1396
|
includeStdinNotificationSection: true,
|
|
1224
|
-
messageNotificationStyle: "
|
|
1397
|
+
messageNotificationStyle: "direct"
|
|
1225
1398
|
});
|
|
1226
1399
|
}
|
|
1227
1400
|
};
|
|
@@ -1325,6 +1498,50 @@ var CodexDriver = class {
|
|
|
1325
1498
|
probe() {
|
|
1326
1499
|
return probeCodex();
|
|
1327
1500
|
}
|
|
1501
|
+
buildDeprecatedShimConfigArgs(ctx) {
|
|
1502
|
+
const isTsSource = ctx.chatBridgePath.endsWith(".ts");
|
|
1503
|
+
const command = isTsSource ? "npx" : "node";
|
|
1504
|
+
const bridgeArgs = isTsSource ? [
|
|
1505
|
+
"tsx",
|
|
1506
|
+
ctx.chatBridgePath,
|
|
1507
|
+
"--agent-id",
|
|
1508
|
+
ctx.agentId,
|
|
1509
|
+
"--server-url",
|
|
1510
|
+
ctx.config.serverUrl,
|
|
1511
|
+
"--auth-token",
|
|
1512
|
+
ctx.config.authToken || ctx.daemonApiKey,
|
|
1513
|
+
"--runtime",
|
|
1514
|
+
this.id,
|
|
1515
|
+
...ctx.launchId ? ["--launch-id", ctx.launchId] : [],
|
|
1516
|
+
"--deprecated-shim"
|
|
1517
|
+
] : [
|
|
1518
|
+
ctx.chatBridgePath,
|
|
1519
|
+
"--agent-id",
|
|
1520
|
+
ctx.agentId,
|
|
1521
|
+
"--server-url",
|
|
1522
|
+
ctx.config.serverUrl,
|
|
1523
|
+
"--auth-token",
|
|
1524
|
+
ctx.config.authToken || ctx.daemonApiKey,
|
|
1525
|
+
"--runtime",
|
|
1526
|
+
this.id,
|
|
1527
|
+
...ctx.launchId ? ["--launch-id", ctx.launchId] : [],
|
|
1528
|
+
"--deprecated-shim"
|
|
1529
|
+
];
|
|
1530
|
+
return [
|
|
1531
|
+
"-c",
|
|
1532
|
+
`mcp_servers.chat.command=${JSON.stringify(command)}`,
|
|
1533
|
+
"-c",
|
|
1534
|
+
`mcp_servers.chat.args=${JSON.stringify(bridgeArgs)}`,
|
|
1535
|
+
"-c",
|
|
1536
|
+
"mcp_servers.chat.startup_timeout_sec=30",
|
|
1537
|
+
"-c",
|
|
1538
|
+
"mcp_servers.chat.tool_timeout_sec=120",
|
|
1539
|
+
"-c",
|
|
1540
|
+
"mcp_servers.chat.enabled=true",
|
|
1541
|
+
"-c",
|
|
1542
|
+
"mcp_servers.chat.required=true"
|
|
1543
|
+
];
|
|
1544
|
+
}
|
|
1328
1545
|
buildThreadRequest(ctx) {
|
|
1329
1546
|
const threadParams = {
|
|
1330
1547
|
cwd: ctx.workingDirectory,
|
|
@@ -1378,6 +1595,7 @@ var CodexDriver = class {
|
|
|
1378
1595
|
this.streamedAgentMessageIds.clear();
|
|
1379
1596
|
this.streamedReasoningIds.clear();
|
|
1380
1597
|
const args = ["app-server", "--listen", "stdio://"];
|
|
1598
|
+
args.push(...this.buildDeprecatedShimConfigArgs(ctx));
|
|
1381
1599
|
const { command, args: spawnArgs } = resolveCodexSpawn(args);
|
|
1382
1600
|
const proc = spawn2(command, spawnArgs, {
|
|
1383
1601
|
cwd: ctx.workingDirectory,
|
|
@@ -1506,6 +1724,9 @@ var CodexDriver = class {
|
|
|
1506
1724
|
if (isStarted && typeof item.command === "string") {
|
|
1507
1725
|
events.push({ kind: "tool_call", name: "shell", input: { command: item.command } });
|
|
1508
1726
|
}
|
|
1727
|
+
if (isCompleted) {
|
|
1728
|
+
events.push({ kind: "tool_output", name: "shell" });
|
|
1729
|
+
}
|
|
1509
1730
|
break;
|
|
1510
1731
|
case "contextCompaction":
|
|
1511
1732
|
if (isStarted) {
|
|
@@ -1531,6 +1752,10 @@ var CodexDriver = class {
|
|
|
1531
1752
|
const toolName = item.server === "chat" ? `${this.mcpToolPrefix}${item.tool}` : `${this.mcpToolPrefix.replace(/_$/, "")}_${item.server}_${item.tool}`;
|
|
1532
1753
|
events.push({ kind: "tool_call", name: toolName, input: item.arguments });
|
|
1533
1754
|
}
|
|
1755
|
+
if (isCompleted) {
|
|
1756
|
+
const toolName = item.server === "chat" ? `${this.mcpToolPrefix}${item.tool}` : `${this.mcpToolPrefix.replace(/_$/, "")}_${item.server}_${item.tool}`;
|
|
1757
|
+
events.push({ kind: "tool_output", name: toolName });
|
|
1758
|
+
}
|
|
1534
1759
|
break;
|
|
1535
1760
|
case "collabAgentToolCall":
|
|
1536
1761
|
if (isStarted) {
|
|
@@ -1681,7 +1906,7 @@ function detectCodexModels(home = os.homedir()) {
|
|
|
1681
1906
|
// src/drivers/copilot.ts
|
|
1682
1907
|
import { spawn as spawn3 } from "child_process";
|
|
1683
1908
|
import path5 from "path";
|
|
1684
|
-
import { writeFileSync as
|
|
1909
|
+
import { writeFileSync as writeFileSync3 } from "fs";
|
|
1685
1910
|
var CopilotDriver = class {
|
|
1686
1911
|
id = "copilot";
|
|
1687
1912
|
supportsStdinNotification = false;
|
|
@@ -1696,7 +1921,7 @@ var CopilotDriver = class {
|
|
|
1696
1921
|
const mcpCommand = isTsSource ? "npx" : "node";
|
|
1697
1922
|
const mcpArgs = isTsSource ? ["tsx", ctx.chatBridgePath, "--agent-id", ctx.agentId, "--server-url", ctx.config.serverUrl, "--auth-token", ctx.config.authToken || ctx.daemonApiKey] : [ctx.chatBridgePath, "--agent-id", ctx.agentId, "--server-url", ctx.config.serverUrl, "--auth-token", ctx.config.authToken || ctx.daemonApiKey];
|
|
1698
1923
|
const mcpConfigPath = path5.join(ctx.workingDirectory, ".slock-copilot-mcp.json");
|
|
1699
|
-
|
|
1924
|
+
writeFileSync3(mcpConfigPath, JSON.stringify({
|
|
1700
1925
|
mcpServers: {
|
|
1701
1926
|
chat: {
|
|
1702
1927
|
command: mcpCommand,
|
|
@@ -1818,7 +2043,7 @@ var CopilotDriver = class {
|
|
|
1818
2043
|
|
|
1819
2044
|
// src/drivers/cursor.ts
|
|
1820
2045
|
import { spawn as spawn4 } from "child_process";
|
|
1821
|
-
import { writeFileSync as
|
|
2046
|
+
import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync2, existsSync as existsSync3 } from "fs";
|
|
1822
2047
|
import path6 from "path";
|
|
1823
2048
|
var CursorDriver = class {
|
|
1824
2049
|
id = "cursor";
|
|
@@ -1834,7 +2059,7 @@ var CursorDriver = class {
|
|
|
1834
2059
|
const mcpCommand = isTsSource ? "npx" : "node";
|
|
1835
2060
|
const mcpArgs = isTsSource ? ["tsx", ctx.chatBridgePath, "--agent-id", ctx.agentId, "--server-url", ctx.config.serverUrl, "--auth-token", ctx.config.authToken || ctx.daemonApiKey] : [ctx.chatBridgePath, "--agent-id", ctx.agentId, "--server-url", ctx.config.serverUrl, "--auth-token", ctx.config.authToken || ctx.daemonApiKey];
|
|
1836
2061
|
const mcpConfigPath = path6.join(cursorDir, "mcp.json");
|
|
1837
|
-
|
|
2062
|
+
writeFileSync4(mcpConfigPath, JSON.stringify({
|
|
1838
2063
|
mcpServers: {
|
|
1839
2064
|
chat: {
|
|
1840
2065
|
command: mcpCommand,
|
|
@@ -1938,7 +2163,7 @@ var CursorDriver = class {
|
|
|
1938
2163
|
|
|
1939
2164
|
// src/drivers/gemini.ts
|
|
1940
2165
|
import { spawn as spawn5 } from "child_process";
|
|
1941
|
-
import { writeFileSync as
|
|
2166
|
+
import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync3, existsSync as existsSync4 } from "fs";
|
|
1942
2167
|
import path7 from "path";
|
|
1943
2168
|
var GeminiDriver = class {
|
|
1944
2169
|
id = "gemini";
|
|
@@ -1958,7 +2183,7 @@ var GeminiDriver = class {
|
|
|
1958
2183
|
const mcpCommand = isTsSource ? "npx" : "node";
|
|
1959
2184
|
const mcpArgs = isTsSource ? ["tsx", ctx.chatBridgePath, "--agent-id", ctx.agentId, "--server-url", ctx.config.serverUrl, "--auth-token", ctx.config.authToken || ctx.daemonApiKey] : [ctx.chatBridgePath, "--agent-id", ctx.agentId, "--server-url", ctx.config.serverUrl, "--auth-token", ctx.config.authToken || ctx.daemonApiKey];
|
|
1960
2185
|
const settingsPath = path7.join(geminiDir, "settings.json");
|
|
1961
|
-
|
|
2186
|
+
writeFileSync5(settingsPath, JSON.stringify({
|
|
1962
2187
|
mcpServers: {
|
|
1963
2188
|
chat: {
|
|
1964
2189
|
command: mcpCommand,
|
|
@@ -2054,7 +2279,7 @@ var GeminiDriver = class {
|
|
|
2054
2279
|
// src/drivers/kimi.ts
|
|
2055
2280
|
import { randomUUID } from "crypto";
|
|
2056
2281
|
import { spawn as spawn6 } from "child_process";
|
|
2057
|
-
import { existsSync as existsSync5, readFileSync as readFileSync2, writeFileSync as
|
|
2282
|
+
import { existsSync as existsSync5, readFileSync as readFileSync2, writeFileSync as writeFileSync6 } from "fs";
|
|
2058
2283
|
import os2 from "os";
|
|
2059
2284
|
import path8 from "path";
|
|
2060
2285
|
var KIMI_WIRE_PROTOCOL_VERSION = "1.3";
|
|
@@ -2077,6 +2302,21 @@ var KimiDriver = class {
|
|
|
2077
2302
|
sessionId = null;
|
|
2078
2303
|
sessionAnnounced = false;
|
|
2079
2304
|
promptRequestId = null;
|
|
2305
|
+
buildChatBridgeArgs(ctx) {
|
|
2306
|
+
const isTsSource = ctx.chatBridgePath.endsWith(".ts");
|
|
2307
|
+
return [
|
|
2308
|
+
...isTsSource ? ["tsx", ctx.chatBridgePath] : [ctx.chatBridgePath],
|
|
2309
|
+
"--agent-id",
|
|
2310
|
+
ctx.agentId,
|
|
2311
|
+
"--server-url",
|
|
2312
|
+
ctx.config.serverUrl,
|
|
2313
|
+
"--auth-token",
|
|
2314
|
+
ctx.config.authToken || ctx.daemonApiKey,
|
|
2315
|
+
"--runtime",
|
|
2316
|
+
"kimi",
|
|
2317
|
+
...ctx.launchId ? ["--launch-id", ctx.launchId] : []
|
|
2318
|
+
];
|
|
2319
|
+
}
|
|
2080
2320
|
spawn(ctx) {
|
|
2081
2321
|
const isResume = !!ctx.config.sessionId;
|
|
2082
2322
|
this.sessionId = ctx.config.sessionId || randomUUID();
|
|
@@ -2084,21 +2324,21 @@ var KimiDriver = class {
|
|
|
2084
2324
|
this.promptRequestId = randomUUID();
|
|
2085
2325
|
const isTsSource = ctx.chatBridgePath.endsWith(".ts");
|
|
2086
2326
|
const command = isTsSource ? "npx" : "node";
|
|
2087
|
-
const bridgeArgs =
|
|
2327
|
+
const bridgeArgs = this.buildChatBridgeArgs(ctx);
|
|
2088
2328
|
const systemPromptPath = path8.join(ctx.workingDirectory, KIMI_SYSTEM_PROMPT_FILE);
|
|
2089
2329
|
const agentFilePath = path8.join(ctx.workingDirectory, KIMI_AGENT_FILE);
|
|
2090
2330
|
const mcpConfigPath = path8.join(ctx.workingDirectory, KIMI_MCP_FILE);
|
|
2091
2331
|
if (!isResume || !existsSync5(systemPromptPath)) {
|
|
2092
|
-
|
|
2332
|
+
writeFileSync6(systemPromptPath, ctx.prompt, "utf8");
|
|
2093
2333
|
}
|
|
2094
|
-
|
|
2334
|
+
writeFileSync6(agentFilePath, [
|
|
2095
2335
|
"version: 1",
|
|
2096
2336
|
"agent:",
|
|
2097
2337
|
" extend: default",
|
|
2098
2338
|
` system_prompt_path: ./${KIMI_SYSTEM_PROMPT_FILE}`,
|
|
2099
2339
|
""
|
|
2100
2340
|
].join("\n"), "utf8");
|
|
2101
|
-
|
|
2341
|
+
writeFileSync6(mcpConfigPath, JSON.stringify({
|
|
2102
2342
|
mcpServers: {
|
|
2103
2343
|
chat: {
|
|
2104
2344
|
command,
|
|
@@ -2471,10 +2711,13 @@ function buildUnreadSummary(messages, excludeChannel) {
|
|
|
2471
2711
|
var MAX_TRAJECTORY_TEXT = 2e3;
|
|
2472
2712
|
var TRAJECTORY_COALESCE_MS = 350;
|
|
2473
2713
|
var ACTIVITY_HEARTBEAT_MS = 6e4;
|
|
2714
|
+
var COMPACTION_STALE_MS = 5 * 6e4;
|
|
2715
|
+
var RUNTIME_PROGRESS_STALE_MS = 15 * 6e4;
|
|
2474
2716
|
var MAX_STDOUT_LINES = 8;
|
|
2475
2717
|
var MAX_STDOUT_LINE_LENGTH = 240;
|
|
2476
2718
|
var MAX_STDERR_LINES = 8;
|
|
2477
2719
|
var MAX_STDERR_LINE_LENGTH = 240;
|
|
2720
|
+
var MAX_GATED_STEERING_EVENTS = 12;
|
|
2478
2721
|
var ONBOARDING_MEMORY_SEED_ENV = "SLOCK_ONBOARDING_MEMORY_SEED";
|
|
2479
2722
|
var FIRST_CINDY_SEED_MODE = "first-cindy";
|
|
2480
2723
|
function getOnboardingSeedMode(config) {
|
|
@@ -2785,6 +3028,31 @@ Success = user starts useful collaboration and setup progresses,
|
|
|
2785
3028
|
not finishing a long onboarding conversation in one channel.
|
|
2786
3029
|
`;
|
|
2787
3030
|
}
|
|
3031
|
+
function createGatedSteeringState() {
|
|
3032
|
+
return {
|
|
3033
|
+
phase: "idle",
|
|
3034
|
+
outstandingToolUses: 0,
|
|
3035
|
+
compacting: false,
|
|
3036
|
+
toolBoundaryFlushDisabled: process.env.SLOCK_CLAUDE_GATED_STEERING_TOOL_BOUNDARY === "0",
|
|
3037
|
+
lastFlushReason: null,
|
|
3038
|
+
recentEvents: [],
|
|
3039
|
+
inFlightBatch: null
|
|
3040
|
+
};
|
|
3041
|
+
}
|
|
3042
|
+
var RUNTIME_PROFILE_MIGRATION_MESSAGE_PREFIX = "runtime-profile-migration-";
|
|
3043
|
+
var RUNTIME_PROFILE_DAEMON_NOTICE_MESSAGE_PREFIX = "runtime-profile-daemon-release-";
|
|
3044
|
+
function runtimeProfileNotificationFromMessage(message) {
|
|
3045
|
+
if (message.message_id?.startsWith(RUNTIME_PROFILE_MIGRATION_MESSAGE_PREFIX)) {
|
|
3046
|
+
return { kind: "migration", key: message.message_id.slice(RUNTIME_PROFILE_MIGRATION_MESSAGE_PREFIX.length) };
|
|
3047
|
+
}
|
|
3048
|
+
if (message.message_id?.startsWith(RUNTIME_PROFILE_DAEMON_NOTICE_MESSAGE_PREFIX)) {
|
|
3049
|
+
return { kind: "daemon_release_notice", key: message.message_id.slice(RUNTIME_PROFILE_DAEMON_NOTICE_MESSAGE_PREFIX.length) };
|
|
3050
|
+
}
|
|
3051
|
+
return null;
|
|
3052
|
+
}
|
|
3053
|
+
function runtimeProfileNotificationTitle(kind) {
|
|
3054
|
+
return kind === "migration" ? "Runtime Profile migration" : "Runtime Profile notice";
|
|
3055
|
+
}
|
|
2788
3056
|
function pushRecentLines(lines, chunk, maxLines, maxLineLength) {
|
|
2789
3057
|
const next = [...lines];
|
|
2790
3058
|
for (const rawLine of chunk.split(/\r?\n/)) {
|
|
@@ -2856,6 +3124,9 @@ function getBusyDeliveryNote(driver) {
|
|
|
2856
3124
|
if (driver.busyDeliveryMode === "direct") {
|
|
2857
3125
|
return "\n\nNote: While you are busy, new messages may be delivered directly into your active turn. Handle them when appropriate and keep working.";
|
|
2858
3126
|
}
|
|
3127
|
+
if (driver.busyDeliveryMode === "gated") {
|
|
3128
|
+
return "\n\nNote: While you are busy, new messages may be delivered at runtime-observed safe boundaries in your active turn. If no safe boundary is available, they will be delivered after the current turn ends.";
|
|
3129
|
+
}
|
|
2859
3130
|
return "\n\nNote: While you are busy, you may receive [System notification: ...] messages. Finish your current step, then call check_messages to check for messages.";
|
|
2860
3131
|
}
|
|
2861
3132
|
var NATIVE_STANDING_PROMPT_STARTUP_INPUT = "Your system prompt contains your standing instructions. Follow it now and begin listening for messages.";
|
|
@@ -2874,6 +3145,7 @@ var AgentProcessManager = class _AgentProcessManager {
|
|
|
2874
3145
|
dataDir;
|
|
2875
3146
|
driverResolver;
|
|
2876
3147
|
defaultAgentEnvVarsProvider;
|
|
3148
|
+
tracer;
|
|
2877
3149
|
constructor(chatBridgePath, sendToServer, daemonApiKey, opts) {
|
|
2878
3150
|
this.chatBridgePath = chatBridgePath;
|
|
2879
3151
|
this.slockCliPath = opts.slockCliPath ?? "";
|
|
@@ -2883,6 +3155,7 @@ var AgentProcessManager = class _AgentProcessManager {
|
|
|
2883
3155
|
this.dataDir = opts.dataDir || DATA_DIR;
|
|
2884
3156
|
this.driverResolver = opts.driverResolver || getDriver;
|
|
2885
3157
|
this.defaultAgentEnvVarsProvider = opts.defaultAgentEnvVarsProvider || null;
|
|
3158
|
+
this.tracer = opts.tracer ?? noopTracer;
|
|
2886
3159
|
}
|
|
2887
3160
|
async startAgent(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId) {
|
|
2888
3161
|
if (this.agents.has(agentId)) {
|
|
@@ -2975,7 +3248,8 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
2975
3248
|
workingDirectory: agentDataDir,
|
|
2976
3249
|
chatBridgePath: this.chatBridgePath,
|
|
2977
3250
|
slockCliPath: this.slockCliPath,
|
|
2978
|
-
daemonApiKey: this.daemonApiKey
|
|
3251
|
+
daemonApiKey: this.daemonApiKey,
|
|
3252
|
+
launchId: launchId || null
|
|
2979
3253
|
});
|
|
2980
3254
|
const agentProcess = {
|
|
2981
3255
|
process: proc,
|
|
@@ -2988,6 +3262,11 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
2988
3262
|
notificationTimer: null,
|
|
2989
3263
|
pendingNotificationCount: 0,
|
|
2990
3264
|
activityHeartbeat: null,
|
|
3265
|
+
compactionWatchdog: null,
|
|
3266
|
+
compactionStartedAt: null,
|
|
3267
|
+
lastRuntimeEventAt: Date.now(),
|
|
3268
|
+
runtimeProgressStaleSince: null,
|
|
3269
|
+
runtimeTraceSpan: null,
|
|
2991
3270
|
lastActivity: "",
|
|
2992
3271
|
lastActivityDetail: "",
|
|
2993
3272
|
recentStdout: [],
|
|
@@ -2996,11 +3275,16 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
2996
3275
|
spawnError: null,
|
|
2997
3276
|
exitCode: null,
|
|
2998
3277
|
exitSignal: null,
|
|
2999
|
-
pendingTrajectory: null
|
|
3278
|
+
pendingTrajectory: null,
|
|
3279
|
+
gatedSteering: createGatedSteeringState()
|
|
3000
3280
|
};
|
|
3001
3281
|
this.startingInboxes.delete(agentId);
|
|
3002
3282
|
this.agents.set(agentId, agentProcess);
|
|
3283
|
+
this.startRuntimeTrace(agentId, agentProcess, "spawn");
|
|
3003
3284
|
this.agentsStarting.delete(agentId);
|
|
3285
|
+
if (wakeMessage) {
|
|
3286
|
+
this.ackInjectedRuntimeProfileMessages(agentId, [wakeMessage], agentProcess.launchId);
|
|
3287
|
+
}
|
|
3004
3288
|
let buffer = "";
|
|
3005
3289
|
proc.stdout?.on("data", (chunk) => {
|
|
3006
3290
|
const chunkText = chunk.toString();
|
|
@@ -3055,10 +3339,20 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
3055
3339
|
if (ap.activityHeartbeat) {
|
|
3056
3340
|
clearInterval(ap.activityHeartbeat);
|
|
3057
3341
|
}
|
|
3058
|
-
this.agents.delete(agentId);
|
|
3059
3342
|
const finalCode = ap.exitCode ?? code;
|
|
3060
3343
|
const finalSignal = ap.exitSignal ?? signal;
|
|
3061
3344
|
const terminalFailureDetail = classifyTerminalFailure(ap);
|
|
3345
|
+
this.endRuntimeTrace(ap, finalCode === 0 ? "ok" : "error", {
|
|
3346
|
+
outcome: finalCode === 0 ? "process-exit" : "process-crash",
|
|
3347
|
+
exitCode: finalCode,
|
|
3348
|
+
exitSignal: finalSignal
|
|
3349
|
+
});
|
|
3350
|
+
if (finalCode === 0) {
|
|
3351
|
+
this.finishCompactionIfActive(agentId, "Context compaction finished (inferred from process exit)");
|
|
3352
|
+
} else {
|
|
3353
|
+
this.clearCompactionWatchdog(ap);
|
|
3354
|
+
}
|
|
3355
|
+
this.agents.delete(agentId);
|
|
3062
3356
|
if (finalCode === 0) {
|
|
3063
3357
|
const queuedWakeMessage = !ap.driver.supportsStdinNotification ? ap.inbox.shift() : void 0;
|
|
3064
3358
|
const unreadSummary2 = queuedWakeMessage ? buildUnreadSummary(ap.inbox, formatChannelLabel(queuedWakeMessage)) : void 0;
|
|
@@ -3194,6 +3488,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
3194
3488
|
if (ap.activityHeartbeat) {
|
|
3195
3489
|
clearInterval(ap.activityHeartbeat);
|
|
3196
3490
|
}
|
|
3491
|
+
this.clearCompactionWatchdog(ap);
|
|
3197
3492
|
this.agents.delete(agentId);
|
|
3198
3493
|
ap.process.kill("SIGTERM");
|
|
3199
3494
|
if (!silent) {
|
|
@@ -3247,6 +3542,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
3247
3542
|
const nextMessages = ap.inbox.splice(0, ap.inbox.length);
|
|
3248
3543
|
nextMessages.push(message);
|
|
3249
3544
|
ap.isIdle = false;
|
|
3545
|
+
this.startRuntimeTrace(agentId, ap, "stdin-idle-delivery");
|
|
3250
3546
|
this.broadcastActivity(agentId, "working", "Message received");
|
|
3251
3547
|
this.deliverMessagesViaStdin(agentId, ap, nextMessages, "idle");
|
|
3252
3548
|
return;
|
|
@@ -3254,6 +3550,14 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
3254
3550
|
ap.inbox.push(message);
|
|
3255
3551
|
if (!ap.driver.supportsStdinNotification) return;
|
|
3256
3552
|
if (!ap.sessionId) return;
|
|
3553
|
+
if (ap.driver.busyDeliveryMode === "gated") {
|
|
3554
|
+
ap.pendingNotificationCount++;
|
|
3555
|
+
this.recordGatedSteeringEvent(agentId, ap, "buffer", {
|
|
3556
|
+
reason: "busy_message",
|
|
3557
|
+
pendingMessages: ap.inbox.length
|
|
3558
|
+
});
|
|
3559
|
+
return;
|
|
3560
|
+
}
|
|
3257
3561
|
ap.pendingNotificationCount++;
|
|
3258
3562
|
if (!ap.notificationTimer) {
|
|
3259
3563
|
ap.notificationTimer = setTimeout(() => {
|
|
@@ -3291,6 +3595,127 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
3291
3595
|
}
|
|
3292
3596
|
return result;
|
|
3293
3597
|
}
|
|
3598
|
+
buildRuntimeProfileReport(agentId, config, sessionId, launchId) {
|
|
3599
|
+
const workspacePath = path10.join(this.dataDir, agentId);
|
|
3600
|
+
return {
|
|
3601
|
+
agentId,
|
|
3602
|
+
launchId,
|
|
3603
|
+
facts: {
|
|
3604
|
+
runtime: config.runtime,
|
|
3605
|
+
model: config.model,
|
|
3606
|
+
reasoningEffort: config.reasoningEffort,
|
|
3607
|
+
executionMode: config.executionMode || "byoc",
|
|
3608
|
+
workspacePathRef: {
|
|
3609
|
+
label: "workspace",
|
|
3610
|
+
path: workspacePath,
|
|
3611
|
+
reachable: true
|
|
3612
|
+
},
|
|
3613
|
+
sessionRef: sessionId ? {
|
|
3614
|
+
label: sessionId,
|
|
3615
|
+
path: sessionId,
|
|
3616
|
+
runtime: config.runtime,
|
|
3617
|
+
reachable: true
|
|
3618
|
+
} : null
|
|
3619
|
+
}
|
|
3620
|
+
};
|
|
3621
|
+
}
|
|
3622
|
+
getAgentRuntimeProfileReport(agentId) {
|
|
3623
|
+
const running = this.agents.get(agentId);
|
|
3624
|
+
if (running) {
|
|
3625
|
+
return this.buildRuntimeProfileReport(agentId, running.config, running.sessionId, running.launchId);
|
|
3626
|
+
}
|
|
3627
|
+
const idle = this.idleAgentConfigs.get(agentId);
|
|
3628
|
+
if (idle) {
|
|
3629
|
+
return this.buildRuntimeProfileReport(agentId, idle.config, idle.sessionId, idle.launchId);
|
|
3630
|
+
}
|
|
3631
|
+
return null;
|
|
3632
|
+
}
|
|
3633
|
+
getAgentRuntimeProfileReports() {
|
|
3634
|
+
const reports = [];
|
|
3635
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3636
|
+
for (const agentId of this.agents.keys()) {
|
|
3637
|
+
const report = this.getAgentRuntimeProfileReport(agentId);
|
|
3638
|
+
if (report) {
|
|
3639
|
+
reports.push(report);
|
|
3640
|
+
seen.add(agentId);
|
|
3641
|
+
}
|
|
3642
|
+
}
|
|
3643
|
+
for (const agentId of this.idleAgentConfigs.keys()) {
|
|
3644
|
+
if (seen.has(agentId)) continue;
|
|
3645
|
+
const report = this.getAgentRuntimeProfileReport(agentId);
|
|
3646
|
+
if (report) reports.push(report);
|
|
3647
|
+
}
|
|
3648
|
+
return reports;
|
|
3649
|
+
}
|
|
3650
|
+
deliverRuntimeProfileNotification(agentId, key, kind, content) {
|
|
3651
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3652
|
+
const message = {
|
|
3653
|
+
channel_id: "system",
|
|
3654
|
+
channel_name: "system",
|
|
3655
|
+
channel_type: "dm",
|
|
3656
|
+
sender_id: "system",
|
|
3657
|
+
sender_name: "system",
|
|
3658
|
+
sender_type: "system",
|
|
3659
|
+
content,
|
|
3660
|
+
timestamp: now,
|
|
3661
|
+
message_id: `${kind === "migration" ? RUNTIME_PROFILE_MIGRATION_MESSAGE_PREFIX : RUNTIME_PROFILE_DAEMON_NOTICE_MESSAGE_PREFIX}${key}`
|
|
3662
|
+
};
|
|
3663
|
+
const ap = this.agents.get(agentId);
|
|
3664
|
+
if (ap?.sessionId && ap.driver.supportsStdinNotification && ap.isIdle) {
|
|
3665
|
+
ap.isIdle = false;
|
|
3666
|
+
this.startRuntimeTrace(agentId, ap, "runtime-profile");
|
|
3667
|
+
this.deliverMessagesViaStdin(agentId, ap, [message], "idle");
|
|
3668
|
+
return;
|
|
3669
|
+
}
|
|
3670
|
+
if (ap?.sessionId && ap.driver.busyDeliveryMode === "direct") {
|
|
3671
|
+
this.deliverMessagesViaStdin(agentId, ap, [message], "busy");
|
|
3672
|
+
return;
|
|
3673
|
+
}
|
|
3674
|
+
const cached = this.idleAgentConfigs.get(agentId);
|
|
3675
|
+
if (cached) {
|
|
3676
|
+
logger.info(`[Agent ${agentId}] Starting from idle state for runtime profile ${kind} ${key}`);
|
|
3677
|
+
this.idleAgentConfigs.delete(agentId);
|
|
3678
|
+
this.startAgent(agentId, cached.config, message, void 0, void 0, cached.launchId || void 0).catch((err) => {
|
|
3679
|
+
logger.error(`[Agent ${agentId}] Failed to auto-restart for runtime profile notification`, err);
|
|
3680
|
+
this.idleAgentConfigs.set(agentId, cached);
|
|
3681
|
+
});
|
|
3682
|
+
return;
|
|
3683
|
+
}
|
|
3684
|
+
logger.warn(`[Agent ${agentId}] Runtime profile ${kind} ${key} has no runtime injection path yet; leaving unacked for retry`);
|
|
3685
|
+
}
|
|
3686
|
+
ackInjectedRuntimeProfileMessages(agentId, messages, launchId) {
|
|
3687
|
+
for (const message of messages) {
|
|
3688
|
+
const notification = runtimeProfileNotificationFromMessage(message);
|
|
3689
|
+
if (!notification) continue;
|
|
3690
|
+
const title = runtimeProfileNotificationTitle(notification.kind);
|
|
3691
|
+
this.broadcastActivity(agentId, "working", title, [{ kind: "system", title, text: message.content }], launchId);
|
|
3692
|
+
if (notification.kind === "migration") {
|
|
3693
|
+
this.sendToServer({
|
|
3694
|
+
type: "agent:runtime_profile:migration:ack",
|
|
3695
|
+
agentId,
|
|
3696
|
+
migrationKey: notification.key,
|
|
3697
|
+
launchId: launchId || void 0
|
|
3698
|
+
});
|
|
3699
|
+
} else {
|
|
3700
|
+
this.sendToServer({
|
|
3701
|
+
type: "agent:runtime_profile:daemon_release_notice:ack",
|
|
3702
|
+
agentId,
|
|
3703
|
+
noticeKey: notification.key,
|
|
3704
|
+
launchId: launchId || void 0
|
|
3705
|
+
});
|
|
3706
|
+
}
|
|
3707
|
+
}
|
|
3708
|
+
}
|
|
3709
|
+
sendRuntimeProfileReport(agentId) {
|
|
3710
|
+
const report = this.getAgentRuntimeProfileReport(agentId);
|
|
3711
|
+
if (!report) return;
|
|
3712
|
+
this.sendToServer({
|
|
3713
|
+
type: "agent:runtime_profile",
|
|
3714
|
+
agentId: report.agentId,
|
|
3715
|
+
facts: report.facts,
|
|
3716
|
+
launchId: report.launchId || void 0
|
|
3717
|
+
});
|
|
3718
|
+
}
|
|
3294
3719
|
// Machine-level workspace scanning
|
|
3295
3720
|
async scanAllWorkspaces() {
|
|
3296
3721
|
return scanWorkspaceDirectories(this.dataDir);
|
|
@@ -3473,6 +3898,10 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
3473
3898
|
if (activity === "working" || activity === "thinking") {
|
|
3474
3899
|
if (!ap.activityHeartbeat) {
|
|
3475
3900
|
ap.activityHeartbeat = setInterval(() => {
|
|
3901
|
+
if (this.markRuntimeProgressStaleIfNeeded(agentId, ap)) return;
|
|
3902
|
+
this.recordRuntimeTraceEvent(agentId, ap, "activity.heartbeat.sent", {
|
|
3903
|
+
activity: ap.lastActivity
|
|
3904
|
+
});
|
|
3476
3905
|
this.sendToServer({
|
|
3477
3906
|
type: "agent:activity",
|
|
3478
3907
|
agentId,
|
|
@@ -3528,27 +3957,202 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
3528
3957
|
timer: setTimeout(() => this.flushPendingTrajectory(agentId), TRAJECTORY_COALESCE_MS)
|
|
3529
3958
|
};
|
|
3530
3959
|
}
|
|
3960
|
+
clearCompactionWatchdog(ap) {
|
|
3961
|
+
if (ap.compactionWatchdog) {
|
|
3962
|
+
clearTimeout(ap.compactionWatchdog);
|
|
3963
|
+
ap.compactionWatchdog = null;
|
|
3964
|
+
}
|
|
3965
|
+
ap.compactionStartedAt = null;
|
|
3966
|
+
}
|
|
3967
|
+
startCompactionWatchdog(agentId, ap) {
|
|
3968
|
+
this.clearCompactionWatchdog(ap);
|
|
3969
|
+
const startedAt = Date.now();
|
|
3970
|
+
ap.compactionStartedAt = startedAt;
|
|
3971
|
+
ap.compactionWatchdog = setTimeout(() => {
|
|
3972
|
+
this.markCompactionStale(agentId, startedAt);
|
|
3973
|
+
}, COMPACTION_STALE_MS);
|
|
3974
|
+
}
|
|
3975
|
+
markCompactionStale(agentId, startedAt) {
|
|
3976
|
+
const ap = this.agents.get(agentId);
|
|
3977
|
+
if (!ap || ap.compactionStartedAt !== startedAt) return;
|
|
3978
|
+
ap.compactionWatchdog = null;
|
|
3979
|
+
this.broadcastActivity(agentId, "working", "Context compaction still running; no finish event observed");
|
|
3980
|
+
}
|
|
3981
|
+
finishCompactionIfActive(agentId, detail = "Context compaction finished") {
|
|
3982
|
+
const ap = this.agents.get(agentId);
|
|
3983
|
+
if (!ap || !ap.compactionStartedAt) return;
|
|
3984
|
+
this.clearCompactionWatchdog(ap);
|
|
3985
|
+
this.broadcastActivity(agentId, "working", detail, [{ kind: "compaction_finished" }]);
|
|
3986
|
+
}
|
|
3987
|
+
startRuntimeTrace(agentId, ap, reason) {
|
|
3988
|
+
if (ap.runtimeTraceSpan) return ap.runtimeTraceSpan;
|
|
3989
|
+
const span = this.tracer.startSpan("daemon.runtime.turn", {
|
|
3990
|
+
surface: "daemon",
|
|
3991
|
+
kind: "internal",
|
|
3992
|
+
attrs: {
|
|
3993
|
+
agentId,
|
|
3994
|
+
runtime: ap.config.runtime,
|
|
3995
|
+
model: ap.config.model,
|
|
3996
|
+
reason,
|
|
3997
|
+
hasSession: Boolean(ap.sessionId)
|
|
3998
|
+
}
|
|
3999
|
+
});
|
|
4000
|
+
span.addEvent("daemon.turn.started", { reason });
|
|
4001
|
+
ap.runtimeTraceSpan = span;
|
|
4002
|
+
return span;
|
|
4003
|
+
}
|
|
4004
|
+
endRuntimeTrace(ap, status, attrs) {
|
|
4005
|
+
if (!ap.runtimeTraceSpan) return;
|
|
4006
|
+
ap.runtimeTraceSpan.end(status, attrs ? { attrs } : void 0);
|
|
4007
|
+
ap.runtimeTraceSpan = null;
|
|
4008
|
+
}
|
|
4009
|
+
recordRuntimeTraceEvent(agentId, ap, name, attrs) {
|
|
4010
|
+
this.startRuntimeTrace(agentId, ap, "runtime-progress").addEvent(name, attrs);
|
|
4011
|
+
}
|
|
4012
|
+
noteRuntimeProgress(ap) {
|
|
4013
|
+
ap.lastRuntimeEventAt = Date.now();
|
|
4014
|
+
ap.runtimeProgressStaleSince = null;
|
|
4015
|
+
}
|
|
4016
|
+
recordGatedSteeringEvent(agentId, ap, event, attrs = {}) {
|
|
4017
|
+
if (ap.driver.busyDeliveryMode !== "gated") return;
|
|
4018
|
+
const summary = `${event}:${ap.gatedSteering.phase}:tools=${ap.gatedSteering.outstandingToolUses}:compact=${ap.gatedSteering.compacting}`;
|
|
4019
|
+
ap.gatedSteering.recentEvents.push(summary);
|
|
4020
|
+
ap.gatedSteering.recentEvents = ap.gatedSteering.recentEvents.slice(-MAX_GATED_STEERING_EVENTS);
|
|
4021
|
+
this.recordRuntimeTraceEvent(agentId, ap, `runtime.gated_steering.${event}`, {
|
|
4022
|
+
phase: ap.gatedSteering.phase,
|
|
4023
|
+
outstandingToolUses: ap.gatedSteering.outstandingToolUses,
|
|
4024
|
+
compacting: ap.gatedSteering.compacting,
|
|
4025
|
+
toolBoundaryFlushDisabled: ap.gatedSteering.toolBoundaryFlushDisabled,
|
|
4026
|
+
pendingMessages: ap.inbox.length,
|
|
4027
|
+
...attrs
|
|
4028
|
+
});
|
|
4029
|
+
}
|
|
4030
|
+
setGatedSteeringPhase(agentId, ap, phase, attrs = {}) {
|
|
4031
|
+
if (!ap || ap.driver.busyDeliveryMode !== "gated") return;
|
|
4032
|
+
ap.gatedSteering.phase = phase;
|
|
4033
|
+
this.recordGatedSteeringEvent(agentId, ap, "phase", attrs);
|
|
4034
|
+
}
|
|
4035
|
+
tryFlushGatedSteering(agentId, ap, reason) {
|
|
4036
|
+
if (ap.driver.busyDeliveryMode !== "gated") return false;
|
|
4037
|
+
if (!ap.sessionId || ap.inbox.length === 0) return false;
|
|
4038
|
+
if (reason === "tool_batch_complete") {
|
|
4039
|
+
if (ap.gatedSteering.toolBoundaryFlushDisabled) return false;
|
|
4040
|
+
if (ap.gatedSteering.compacting || ap.gatedSteering.outstandingToolUses > 0) return false;
|
|
4041
|
+
}
|
|
4042
|
+
const pendingMessages = ap.inbox.length;
|
|
4043
|
+
const pendingNotificationCount = ap.pendingNotificationCount;
|
|
4044
|
+
const nextMessages = ap.inbox.splice(0, ap.inbox.length);
|
|
4045
|
+
ap.pendingNotificationCount = 0;
|
|
4046
|
+
ap.gatedSteering.lastFlushReason = reason;
|
|
4047
|
+
if (reason === "tool_batch_complete") {
|
|
4048
|
+
ap.gatedSteering.inFlightBatch = {
|
|
4049
|
+
reason,
|
|
4050
|
+
messages: nextMessages
|
|
4051
|
+
};
|
|
4052
|
+
} else {
|
|
4053
|
+
ap.gatedSteering.inFlightBatch = null;
|
|
4054
|
+
}
|
|
4055
|
+
this.recordGatedSteeringEvent(agentId, ap, "flush", { reason, messageCount: nextMessages.length });
|
|
4056
|
+
logger.info(
|
|
4057
|
+
`[Agent ${agentId}] Claude gated steering flush reason=${reason} messages=${nextMessages.length}`
|
|
4058
|
+
);
|
|
4059
|
+
this.broadcastActivity(agentId, "working", "Message received");
|
|
4060
|
+
if (this.deliverMessagesViaStdin(agentId, ap, nextMessages, reason === "turn_end" ? "idle" : "busy")) {
|
|
4061
|
+
return true;
|
|
4062
|
+
}
|
|
4063
|
+
if (reason === "tool_batch_complete") {
|
|
4064
|
+
ap.gatedSteering.inFlightBatch = null;
|
|
4065
|
+
}
|
|
4066
|
+
ap.pendingNotificationCount += pendingNotificationCount || pendingMessages;
|
|
4067
|
+
return false;
|
|
4068
|
+
}
|
|
4069
|
+
clearGatedInFlightBatch(agentId, ap, reason) {
|
|
4070
|
+
if (ap.driver.busyDeliveryMode !== "gated" || !ap.gatedSteering.inFlightBatch) return;
|
|
4071
|
+
const messageCount = ap.gatedSteering.inFlightBatch.messages.length;
|
|
4072
|
+
ap.gatedSteering.inFlightBatch = null;
|
|
4073
|
+
this.recordGatedSteeringEvent(agentId, ap, "ack", { reason, messageCount });
|
|
4074
|
+
}
|
|
4075
|
+
requeueGatedInFlightBatch(agentId, ap, reason) {
|
|
4076
|
+
if (ap.driver.busyDeliveryMode !== "gated" || !ap.gatedSteering.inFlightBatch) return;
|
|
4077
|
+
const batch = ap.gatedSteering.inFlightBatch;
|
|
4078
|
+
ap.gatedSteering.inFlightBatch = null;
|
|
4079
|
+
ap.inbox.unshift(...batch.messages);
|
|
4080
|
+
ap.pendingNotificationCount += batch.messages.length;
|
|
4081
|
+
this.recordGatedSteeringEvent(agentId, ap, "requeue", {
|
|
4082
|
+
reason,
|
|
4083
|
+
originalFlushReason: batch.reason,
|
|
4084
|
+
messageCount: batch.messages.length
|
|
4085
|
+
});
|
|
4086
|
+
}
|
|
4087
|
+
isThinkingBlockMutationError(message) {
|
|
4088
|
+
return /thinking.*redacted_thinking|redacted_thinking.*thinking/i.test(message) && /cannot be modified/i.test(message);
|
|
4089
|
+
}
|
|
4090
|
+
markRuntimeProgressStaleIfNeeded(agentId, ap) {
|
|
4091
|
+
if (ap.lastActivity !== "working" && ap.lastActivity !== "thinking") return false;
|
|
4092
|
+
if (ap.runtimeProgressStaleSince) return true;
|
|
4093
|
+
const staleForMs = Date.now() - ap.lastRuntimeEventAt;
|
|
4094
|
+
if (staleForMs < RUNTIME_PROGRESS_STALE_MS) return false;
|
|
4095
|
+
ap.runtimeProgressStaleSince = Date.now();
|
|
4096
|
+
const staleForMinutes = Math.max(1, Math.floor(staleForMs / 6e4));
|
|
4097
|
+
this.recordRuntimeTraceEvent(agentId, ap, "runtime.progress.stalled", {
|
|
4098
|
+
ageMs: staleForMs,
|
|
4099
|
+
staleForMinutes,
|
|
4100
|
+
lastActivity: ap.lastActivity
|
|
4101
|
+
});
|
|
4102
|
+
this.endRuntimeTrace(ap, "error", {
|
|
4103
|
+
outcome: "runtime-stalled",
|
|
4104
|
+
ageMs: staleForMs,
|
|
4105
|
+
lastActivity: ap.lastActivity
|
|
4106
|
+
});
|
|
4107
|
+
this.broadcastActivity(agentId, "error", `Runtime stalled: no runtime events for ${staleForMinutes}m`);
|
|
4108
|
+
return true;
|
|
4109
|
+
}
|
|
3531
4110
|
/** Handle a single ParsedEvent from any runtime driver */
|
|
3532
4111
|
handleParsedEvent(agentId, event, driver) {
|
|
3533
4112
|
const ap = this.agents.get(agentId);
|
|
4113
|
+
if (ap) {
|
|
4114
|
+
const wasStalled = Boolean(ap.runtimeProgressStaleSince);
|
|
4115
|
+
this.recordRuntimeTraceEvent(agentId, ap, "runtime.event.received", { kind: event.kind });
|
|
4116
|
+
if (wasStalled) {
|
|
4117
|
+
this.recordRuntimeTraceEvent(agentId, ap, "runtime.progress.observed", { afterStall: true });
|
|
4118
|
+
}
|
|
4119
|
+
this.noteRuntimeProgress(ap);
|
|
4120
|
+
}
|
|
3534
4121
|
switch (event.kind) {
|
|
3535
4122
|
case "session_init":
|
|
3536
4123
|
if (ap) ap.sessionId = event.sessionId;
|
|
3537
4124
|
this.sendToServer({ type: "agent:session", agentId, sessionId: event.sessionId, launchId: ap?.launchId || void 0 });
|
|
4125
|
+
this.sendRuntimeProfileReport(agentId);
|
|
3538
4126
|
break;
|
|
3539
4127
|
case "thinking": {
|
|
4128
|
+
this.finishCompactionIfActive(agentId, "Context compaction finished (inferred from resumed output)");
|
|
3540
4129
|
this.queueTrajectoryText(agentId, "thinking", event.text);
|
|
3541
4130
|
if (ap) ap.isIdle = false;
|
|
4131
|
+
if (ap) this.clearGatedInFlightBatch(agentId, ap, "non_error_progress");
|
|
4132
|
+
this.setGatedSteeringPhase(agentId, ap, "assistant_continuation", { event: "thinking" });
|
|
3542
4133
|
break;
|
|
3543
4134
|
}
|
|
3544
4135
|
case "text": {
|
|
4136
|
+
this.finishCompactionIfActive(agentId, "Context compaction finished (inferred from resumed output)");
|
|
3545
4137
|
this.queueTrajectoryText(agentId, "text", event.text);
|
|
3546
4138
|
if (ap) ap.isIdle = false;
|
|
4139
|
+
if (ap) this.clearGatedInFlightBatch(agentId, ap, "non_error_progress");
|
|
4140
|
+
this.setGatedSteeringPhase(agentId, ap, "assistant_continuation", { event: "text" });
|
|
3547
4141
|
break;
|
|
3548
4142
|
}
|
|
3549
4143
|
case "tool_call": {
|
|
4144
|
+
this.finishCompactionIfActive(agentId, "Context compaction finished (inferred from resumed tool use)");
|
|
3550
4145
|
this.flushPendingTrajectory(agentId);
|
|
3551
4146
|
const invocation = normalizeToolDisplayInvocation(event.name, event.input);
|
|
4147
|
+
if (ap) {
|
|
4148
|
+
ap.gatedSteering.outstandingToolUses++;
|
|
4149
|
+
this.clearGatedInFlightBatch(agentId, ap, "non_error_progress");
|
|
4150
|
+
this.recordRuntimeTraceEvent(agentId, ap, "tool.call.started", { tool: invocation.toolName });
|
|
4151
|
+
this.setGatedSteeringPhase(agentId, ap, "tool_wait", {
|
|
4152
|
+
event: "tool_call",
|
|
4153
|
+
tool: invocation.toolName
|
|
4154
|
+
});
|
|
4155
|
+
}
|
|
3552
4156
|
const inputSummary = summarizeToolInput(invocation.toolName, invocation.input);
|
|
3553
4157
|
const detail = getToolActivityLabel(invocation.toolName);
|
|
3554
4158
|
this.broadcastActivity(agentId, "working", detail, [{
|
|
@@ -3559,22 +4163,66 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
3559
4163
|
if (ap) ap.isIdle = false;
|
|
3560
4164
|
break;
|
|
3561
4165
|
}
|
|
4166
|
+
case "tool_output": {
|
|
4167
|
+
const invocation = normalizeToolDisplayInvocation(event.name, {});
|
|
4168
|
+
if (ap) {
|
|
4169
|
+
const hadOutstandingToolUse = ap.gatedSteering.outstandingToolUses > 0;
|
|
4170
|
+
ap.gatedSteering.outstandingToolUses = Math.max(0, ap.gatedSteering.outstandingToolUses - 1);
|
|
4171
|
+
this.recordRuntimeTraceEvent(agentId, ap, "tool.output.observed", { tool: invocation.toolName });
|
|
4172
|
+
this.recordRuntimeTraceEvent(agentId, ap, "runtime.continuation.expected");
|
|
4173
|
+
this.setGatedSteeringPhase(agentId, ap, "tool_boundary", {
|
|
4174
|
+
event: "tool_output",
|
|
4175
|
+
tool: invocation.toolName
|
|
4176
|
+
});
|
|
4177
|
+
ap.isIdle = false;
|
|
4178
|
+
if (hadOutstandingToolUse && ap.gatedSteering.outstandingToolUses === 0) {
|
|
4179
|
+
this.tryFlushGatedSteering(agentId, ap, "tool_batch_complete");
|
|
4180
|
+
}
|
|
4181
|
+
}
|
|
4182
|
+
break;
|
|
4183
|
+
}
|
|
3562
4184
|
case "compaction_started":
|
|
3563
4185
|
this.flushPendingTrajectory(agentId);
|
|
4186
|
+
if (ap) this.recordRuntimeTraceEvent(agentId, ap, "runtime.context_compaction.started");
|
|
4187
|
+
if (ap) this.startCompactionWatchdog(agentId, ap);
|
|
3564
4188
|
this.broadcastActivity(agentId, "working", "Compacting context", [{ kind: "compaction_started" }]);
|
|
3565
|
-
if (ap)
|
|
4189
|
+
if (ap) {
|
|
4190
|
+
ap.gatedSteering.compacting = true;
|
|
4191
|
+
this.setGatedSteeringPhase(agentId, ap, "compacting", { event: "compaction_started" });
|
|
4192
|
+
ap.isIdle = false;
|
|
4193
|
+
}
|
|
3566
4194
|
break;
|
|
3567
4195
|
case "compaction_finished":
|
|
3568
4196
|
this.flushPendingTrajectory(agentId);
|
|
4197
|
+
if (ap) this.recordRuntimeTraceEvent(agentId, ap, "runtime.context_compaction.finished");
|
|
4198
|
+
if (ap) this.clearCompactionWatchdog(ap);
|
|
3569
4199
|
this.broadcastActivity(agentId, "working", "Context compaction finished", [{ kind: "compaction_finished" }]);
|
|
3570
|
-
if (ap)
|
|
4200
|
+
if (ap) {
|
|
4201
|
+
ap.gatedSteering.compacting = false;
|
|
4202
|
+
this.setGatedSteeringPhase(agentId, ap, "assistant_continuation", { event: "compaction_finished" });
|
|
4203
|
+
ap.isIdle = false;
|
|
4204
|
+
}
|
|
3571
4205
|
break;
|
|
3572
4206
|
case "turn_end":
|
|
4207
|
+
if (ap) this.recordRuntimeTraceEvent(agentId, ap, "runtime.turn.completed");
|
|
4208
|
+
this.finishCompactionIfActive(agentId, "Context compaction finished (inferred from turn end)");
|
|
3573
4209
|
this.flushPendingTrajectory(agentId);
|
|
3574
4210
|
if (ap) {
|
|
4211
|
+
this.clearGatedInFlightBatch(agentId, ap, "turn_end");
|
|
3575
4212
|
if (event.sessionId) ap.sessionId = event.sessionId;
|
|
4213
|
+
ap.gatedSteering.outstandingToolUses = 0;
|
|
4214
|
+
ap.gatedSteering.compacting = false;
|
|
4215
|
+
this.setGatedSteeringPhase(agentId, ap, "idle", { event: "turn_end" });
|
|
3576
4216
|
if (ap.inbox.length > 0 && ap.driver.supportsStdinNotification && ap.sessionId) {
|
|
3577
4217
|
const nextMessages = ap.inbox.splice(0, ap.inbox.length);
|
|
4218
|
+
ap.pendingNotificationCount = 0;
|
|
4219
|
+
if (ap.driver.busyDeliveryMode === "gated") {
|
|
4220
|
+
ap.gatedSteering.lastFlushReason = "turn_end";
|
|
4221
|
+
this.recordGatedSteeringEvent(agentId, ap, "flush", {
|
|
4222
|
+
reason: "turn_end",
|
|
4223
|
+
messageCount: nextMessages.length
|
|
4224
|
+
});
|
|
4225
|
+
}
|
|
3578
4226
|
this.broadcastActivity(agentId, "working", "Message received");
|
|
3579
4227
|
if (!this.deliverMessagesViaStdin(agentId, ap, nextMessages, "idle")) {
|
|
3580
4228
|
ap.isIdle = true;
|
|
@@ -3584,14 +4232,34 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
3584
4232
|
ap.isIdle = true;
|
|
3585
4233
|
this.broadcastActivity(agentId, "online", "Idle");
|
|
3586
4234
|
}
|
|
4235
|
+
this.endRuntimeTrace(ap, "ok", { outcome: "turn-completed" });
|
|
3587
4236
|
}
|
|
3588
4237
|
if (event.sessionId) {
|
|
3589
4238
|
this.sendToServer({ type: "agent:session", agentId, sessionId: event.sessionId, launchId: ap?.launchId || void 0 });
|
|
4239
|
+
this.sendRuntimeProfileReport(agentId);
|
|
3590
4240
|
}
|
|
3591
4241
|
break;
|
|
3592
4242
|
case "error": {
|
|
4243
|
+
this.finishCompactionIfActive(agentId, "Context compaction interrupted by runtime error");
|
|
3593
4244
|
this.flushPendingTrajectory(agentId);
|
|
3594
4245
|
if (ap) ap.lastRuntimeError = event.message;
|
|
4246
|
+
if (ap) {
|
|
4247
|
+
this.setGatedSteeringPhase(agentId, ap, "error", { event: "error" });
|
|
4248
|
+
if (ap.driver.busyDeliveryMode === "gated" && this.isThinkingBlockMutationError(event.message)) {
|
|
4249
|
+
this.requeueGatedInFlightBatch(agentId, ap, "thinking_block_mutation_error");
|
|
4250
|
+
ap.gatedSteering.toolBoundaryFlushDisabled = true;
|
|
4251
|
+
this.recordGatedSteeringEvent(agentId, ap, "disabled", {
|
|
4252
|
+
reason: "thinking_block_mutation_error",
|
|
4253
|
+
lastFlushReason: ap.gatedSteering.lastFlushReason,
|
|
4254
|
+
recentEvents: ap.gatedSteering.recentEvents.join(" | ")
|
|
4255
|
+
});
|
|
4256
|
+
logger.warn(
|
|
4257
|
+
`[Agent ${agentId}] Disabled Claude tool-boundary gated steering after thinking-block mutation error; lastFlushReason=${ap.gatedSteering.lastFlushReason || "none"}`
|
|
4258
|
+
);
|
|
4259
|
+
}
|
|
4260
|
+
this.recordRuntimeTraceEvent(agentId, ap, "runtime.error", { message: event.message });
|
|
4261
|
+
this.endRuntimeTrace(ap, "error", { outcome: "runtime-error", errorMessage: event.message });
|
|
4262
|
+
}
|
|
3595
4263
|
this.broadcastActivity(agentId, "error", event.message, [
|
|
3596
4264
|
{ kind: "text", text: `Error: ${event.message}` }
|
|
3597
4265
|
]);
|
|
@@ -3612,6 +4280,17 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
3612
4280
|
if (count === 0) return;
|
|
3613
4281
|
if (ap.isIdle) return;
|
|
3614
4282
|
if (!ap.sessionId) return;
|
|
4283
|
+
if (ap.driver.busyDeliveryMode === "gated") {
|
|
4284
|
+
this.recordGatedSteeringEvent(agentId, ap, "suppress", {
|
|
4285
|
+
reason: "timer_notification_not_safe_boundary",
|
|
4286
|
+
pendingNotificationCount: count
|
|
4287
|
+
});
|
|
4288
|
+
ap.pendingNotificationCount += count;
|
|
4289
|
+
logger.info(
|
|
4290
|
+
`[Agent ${agentId}] Suppressing raw busy stdin notification until Claude gated steering boundary; pending=${ap.inbox.length}`
|
|
4291
|
+
);
|
|
4292
|
+
return;
|
|
4293
|
+
}
|
|
3615
4294
|
if (ap.driver.busyDeliveryMode === "direct" && ap.inbox.length > 0) {
|
|
3616
4295
|
const queuedMessages = ap.inbox.splice(0, ap.inbox.length);
|
|
3617
4296
|
console.log(`[Agent ${agentId}] Delivering queued message via stdin while busy`);
|
|
@@ -3657,6 +4336,7 @@ Respond as appropriate. Complete all your work before stopping.`;
|
|
|
3657
4336
|
`[Agent ${agentId}] Delivering ${mode} ${messages.length === 1 ? "message" : `${messages.length} messages`} via stdin from ${senders}`
|
|
3658
4337
|
);
|
|
3659
4338
|
ap.process.stdin?.write(encoded + "\n");
|
|
4339
|
+
this.ackInjectedRuntimeProfileMessages(agentId, messages, ap.launchId);
|
|
3660
4340
|
return true;
|
|
3661
4341
|
}
|
|
3662
4342
|
/** List ONE level of a directory — directories returned without children (lazy-loaded on demand) */
|
|
@@ -3987,6 +4667,10 @@ function summarizeIncomingMessage(msg) {
|
|
|
3987
4667
|
return `(agent=${msg.agentId})`;
|
|
3988
4668
|
case "agent:deliver":
|
|
3989
4669
|
return `(agent=${msg.agentId}, seq=${msg.seq}, from=@${msg.message.sender_name}, target=${formatChannelTarget(msg)})`;
|
|
4670
|
+
case "agent:runtime_profile:migration":
|
|
4671
|
+
return `(agent=${msg.agentId}, migration=${msg.migrationKey})`;
|
|
4672
|
+
case "agent:runtime_profile:daemon_release_notice":
|
|
4673
|
+
return `(agent=${msg.agentId}, notice=${msg.noticeKey})`;
|
|
3990
4674
|
case "agent:workspace:list":
|
|
3991
4675
|
return `(agent=${msg.agentId}, dir=${msg.dirPath || "."})`;
|
|
3992
4676
|
case "agent:workspace:read":
|
|
@@ -4016,12 +4700,14 @@ var DaemonCore = class {
|
|
|
4016
4700
|
agentManager;
|
|
4017
4701
|
connection;
|
|
4018
4702
|
reminderCache;
|
|
4703
|
+
tracer;
|
|
4019
4704
|
constructor(options) {
|
|
4020
4705
|
this.options = options;
|
|
4021
4706
|
this.daemonVersion = options.daemonVersion ?? readDaemonVersion();
|
|
4022
4707
|
this.chatBridgePath = options.chatBridgePath ?? resolveChatBridgePath();
|
|
4023
4708
|
this.slockCliPath = options.slockCliPath ?? resolveSlockCliPath();
|
|
4024
4709
|
this.runtimeDetector = options.runtimeDetector ?? detectRuntimes;
|
|
4710
|
+
this.tracer = options.tracer ?? noopTracer;
|
|
4025
4711
|
this.reminderCache = new ReminderCache({
|
|
4026
4712
|
clock: options.reminderClock,
|
|
4027
4713
|
onFire: (job) => this.onReminderFire(job)
|
|
@@ -4082,10 +4768,44 @@ var DaemonCore = class {
|
|
|
4082
4768
|
logger.info(`[Agent ${msg.agentId}] Workspace reset requested`);
|
|
4083
4769
|
this.agentManager.resetWorkspace(msg.agentId);
|
|
4084
4770
|
break;
|
|
4085
|
-
case "agent:deliver":
|
|
4771
|
+
case "agent:deliver": {
|
|
4772
|
+
const parent = parseTraceparent(msg.traceparent);
|
|
4773
|
+
const span = this.tracer.startSpan("daemon.agent.delivery", {
|
|
4774
|
+
parent,
|
|
4775
|
+
surface: "daemon",
|
|
4776
|
+
kind: "consumer",
|
|
4777
|
+
attrs: {
|
|
4778
|
+
agentId: msg.agentId,
|
|
4779
|
+
seq: msg.seq
|
|
4780
|
+
}
|
|
4781
|
+
});
|
|
4086
4782
|
logger.info(`[Agent ${msg.agentId}] Delivery received (seq=${msg.seq}, from=@${msg.message.sender_name}, target=${formatChannelTarget(msg)})`);
|
|
4087
|
-
|
|
4088
|
-
|
|
4783
|
+
try {
|
|
4784
|
+
span.addEvent("daemon.receive", { seq: msg.seq });
|
|
4785
|
+
this.agentManager.deliverMessage(msg.agentId, msg.message);
|
|
4786
|
+
span.addEvent("daemon.deliver_to_agent_manager");
|
|
4787
|
+
const ackSeq = msg.seq > 0 ? msg.seq : msg.message.seq ?? 0;
|
|
4788
|
+
span.addEvent("daemon.ack.sent", { seq: ackSeq });
|
|
4789
|
+
this.connection.send({
|
|
4790
|
+
type: "agent:deliver:ack",
|
|
4791
|
+
agentId: msg.agentId,
|
|
4792
|
+
seq: ackSeq,
|
|
4793
|
+
traceparent: formatTraceparent(span.context)
|
|
4794
|
+
});
|
|
4795
|
+
span.end("ok", { attrs: { outcome: "ack-sent", ackSeq } });
|
|
4796
|
+
} catch (err) {
|
|
4797
|
+
span.end("error", { attrs: { errorMessage: err instanceof Error ? err.message : String(err) } });
|
|
4798
|
+
throw err;
|
|
4799
|
+
}
|
|
4800
|
+
break;
|
|
4801
|
+
}
|
|
4802
|
+
case "agent:runtime_profile:migration":
|
|
4803
|
+
logger.info(`[Agent ${msg.agentId}] Runtime profile migration received (${msg.migrationKey})`);
|
|
4804
|
+
this.agentManager.deliverRuntimeProfileNotification(msg.agentId, msg.migrationKey, "migration", msg.message);
|
|
4805
|
+
break;
|
|
4806
|
+
case "agent:runtime_profile:daemon_release_notice":
|
|
4807
|
+
logger.info(`[Agent ${msg.agentId}] Runtime profile daemon release notice received (${msg.noticeKey})`);
|
|
4808
|
+
this.agentManager.deliverRuntimeProfileNotification(msg.agentId, msg.noticeKey, "daemon_release_notice", msg.message);
|
|
4089
4809
|
break;
|
|
4090
4810
|
case "agent:workspace:list":
|
|
4091
4811
|
this.agentManager.getFileTree(msg.agentId, msg.dirPath).then((files) => {
|
|
@@ -4194,6 +4914,14 @@ var DaemonCore = class {
|
|
|
4194
4914
|
for (const { agentId, sessionId, launchId } of this.agentManager.getIdleAgentSessionIds()) {
|
|
4195
4915
|
this.connection.send({ type: "agent:session", agentId, sessionId, launchId: launchId || void 0 });
|
|
4196
4916
|
}
|
|
4917
|
+
for (const report of this.agentManager.getAgentRuntimeProfileReports()) {
|
|
4918
|
+
this.connection.send({
|
|
4919
|
+
type: "agent:runtime_profile",
|
|
4920
|
+
agentId: report.agentId,
|
|
4921
|
+
facts: report.facts,
|
|
4922
|
+
launchId: report.launchId || void 0
|
|
4923
|
+
});
|
|
4924
|
+
}
|
|
4197
4925
|
const agentsForSnapshot = new Set(this.agentManager.getRunningAgentIds());
|
|
4198
4926
|
for (const { agentId } of this.agentManager.getIdleAgentSessionIds()) {
|
|
4199
4927
|
agentsForSnapshot.add(agentId);
|