@slock-ai/daemon 0.40.2 → 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 +127 -1
- package/dist/{chunk-PB75DRIF.js → chunk-KFVDXO5Y.js} +711 -51
- package/dist/cli/index.js +170 -1
- package/dist/core.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
|
@@ -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,13 +1211,16 @@ 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
|
}
|
|
@@ -1121,16 +1244,27 @@ var ClaudeDriver = class {
|
|
|
1121
1244
|
ctx.config.authToken || ctx.daemonApiKey,
|
|
1122
1245
|
"--runtime",
|
|
1123
1246
|
this.id,
|
|
1247
|
+
...ctx.launchId ? ["--launch-id", ctx.launchId] : [],
|
|
1124
1248
|
"--deprecated-shim"
|
|
1125
1249
|
]
|
|
1126
1250
|
}
|
|
1127
1251
|
}
|
|
1128
1252
|
});
|
|
1129
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
|
+
}
|
|
1130
1261
|
spawn(ctx) {
|
|
1131
|
-
const { tokenFile, spawnEnv } = prepareCliTransport(ctx);
|
|
1132
|
-
const
|
|
1133
|
-
args.
|
|
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);
|
|
1134
1268
|
delete spawnEnv.CLAUDECODE;
|
|
1135
1269
|
logger.info(
|
|
1136
1270
|
`[Agent ${ctx.agentId}] transport=cli cli=${ctx.slockCliPath} token_file=${tokenFile}`
|
|
@@ -1200,6 +1334,17 @@ var ClaudeDriver = class {
|
|
|
1200
1334
|
}
|
|
1201
1335
|
break;
|
|
1202
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
|
+
}
|
|
1203
1348
|
case "result": {
|
|
1204
1349
|
const subtype = typeof event.subtype === "string" ? event.subtype : "success";
|
|
1205
1350
|
const stopReason = typeof event.stop_reason === "string" ? event.stop_reason : null;
|
|
@@ -1244,9 +1389,12 @@ var ClaudeDriver = class {
|
|
|
1244
1389
|
return buildCliTransportSystemPrompt(config, {
|
|
1245
1390
|
toolPrefix: "mcp__chat__",
|
|
1246
1391
|
extraCriticalRules: [],
|
|
1247
|
-
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
|
+
],
|
|
1248
1396
|
includeStdinNotificationSection: true,
|
|
1249
|
-
messageNotificationStyle: "
|
|
1397
|
+
messageNotificationStyle: "direct"
|
|
1250
1398
|
});
|
|
1251
1399
|
}
|
|
1252
1400
|
};
|
|
@@ -1364,6 +1512,7 @@ var CodexDriver = class {
|
|
|
1364
1512
|
ctx.config.authToken || ctx.daemonApiKey,
|
|
1365
1513
|
"--runtime",
|
|
1366
1514
|
this.id,
|
|
1515
|
+
...ctx.launchId ? ["--launch-id", ctx.launchId] : [],
|
|
1367
1516
|
"--deprecated-shim"
|
|
1368
1517
|
] : [
|
|
1369
1518
|
ctx.chatBridgePath,
|
|
@@ -1375,6 +1524,7 @@ var CodexDriver = class {
|
|
|
1375
1524
|
ctx.config.authToken || ctx.daemonApiKey,
|
|
1376
1525
|
"--runtime",
|
|
1377
1526
|
this.id,
|
|
1527
|
+
...ctx.launchId ? ["--launch-id", ctx.launchId] : [],
|
|
1378
1528
|
"--deprecated-shim"
|
|
1379
1529
|
];
|
|
1380
1530
|
return [
|
|
@@ -1574,6 +1724,9 @@ var CodexDriver = class {
|
|
|
1574
1724
|
if (isStarted && typeof item.command === "string") {
|
|
1575
1725
|
events.push({ kind: "tool_call", name: "shell", input: { command: item.command } });
|
|
1576
1726
|
}
|
|
1727
|
+
if (isCompleted) {
|
|
1728
|
+
events.push({ kind: "tool_output", name: "shell" });
|
|
1729
|
+
}
|
|
1577
1730
|
break;
|
|
1578
1731
|
case "contextCompaction":
|
|
1579
1732
|
if (isStarted) {
|
|
@@ -1599,6 +1752,10 @@ var CodexDriver = class {
|
|
|
1599
1752
|
const toolName = item.server === "chat" ? `${this.mcpToolPrefix}${item.tool}` : `${this.mcpToolPrefix.replace(/_$/, "")}_${item.server}_${item.tool}`;
|
|
1600
1753
|
events.push({ kind: "tool_call", name: toolName, input: item.arguments });
|
|
1601
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
|
+
}
|
|
1602
1759
|
break;
|
|
1603
1760
|
case "collabAgentToolCall":
|
|
1604
1761
|
if (isStarted) {
|
|
@@ -1749,7 +1906,7 @@ function detectCodexModels(home = os.homedir()) {
|
|
|
1749
1906
|
// src/drivers/copilot.ts
|
|
1750
1907
|
import { spawn as spawn3 } from "child_process";
|
|
1751
1908
|
import path5 from "path";
|
|
1752
|
-
import { writeFileSync as
|
|
1909
|
+
import { writeFileSync as writeFileSync3 } from "fs";
|
|
1753
1910
|
var CopilotDriver = class {
|
|
1754
1911
|
id = "copilot";
|
|
1755
1912
|
supportsStdinNotification = false;
|
|
@@ -1764,7 +1921,7 @@ var CopilotDriver = class {
|
|
|
1764
1921
|
const mcpCommand = isTsSource ? "npx" : "node";
|
|
1765
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];
|
|
1766
1923
|
const mcpConfigPath = path5.join(ctx.workingDirectory, ".slock-copilot-mcp.json");
|
|
1767
|
-
|
|
1924
|
+
writeFileSync3(mcpConfigPath, JSON.stringify({
|
|
1768
1925
|
mcpServers: {
|
|
1769
1926
|
chat: {
|
|
1770
1927
|
command: mcpCommand,
|
|
@@ -1886,7 +2043,7 @@ var CopilotDriver = class {
|
|
|
1886
2043
|
|
|
1887
2044
|
// src/drivers/cursor.ts
|
|
1888
2045
|
import { spawn as spawn4 } from "child_process";
|
|
1889
|
-
import { writeFileSync as
|
|
2046
|
+
import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync2, existsSync as existsSync3 } from "fs";
|
|
1890
2047
|
import path6 from "path";
|
|
1891
2048
|
var CursorDriver = class {
|
|
1892
2049
|
id = "cursor";
|
|
@@ -1902,7 +2059,7 @@ var CursorDriver = class {
|
|
|
1902
2059
|
const mcpCommand = isTsSource ? "npx" : "node";
|
|
1903
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];
|
|
1904
2061
|
const mcpConfigPath = path6.join(cursorDir, "mcp.json");
|
|
1905
|
-
|
|
2062
|
+
writeFileSync4(mcpConfigPath, JSON.stringify({
|
|
1906
2063
|
mcpServers: {
|
|
1907
2064
|
chat: {
|
|
1908
2065
|
command: mcpCommand,
|
|
@@ -2006,7 +2163,7 @@ var CursorDriver = class {
|
|
|
2006
2163
|
|
|
2007
2164
|
// src/drivers/gemini.ts
|
|
2008
2165
|
import { spawn as spawn5 } from "child_process";
|
|
2009
|
-
import { writeFileSync as
|
|
2166
|
+
import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync3, existsSync as existsSync4 } from "fs";
|
|
2010
2167
|
import path7 from "path";
|
|
2011
2168
|
var GeminiDriver = class {
|
|
2012
2169
|
id = "gemini";
|
|
@@ -2026,7 +2183,7 @@ var GeminiDriver = class {
|
|
|
2026
2183
|
const mcpCommand = isTsSource ? "npx" : "node";
|
|
2027
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];
|
|
2028
2185
|
const settingsPath = path7.join(geminiDir, "settings.json");
|
|
2029
|
-
|
|
2186
|
+
writeFileSync5(settingsPath, JSON.stringify({
|
|
2030
2187
|
mcpServers: {
|
|
2031
2188
|
chat: {
|
|
2032
2189
|
command: mcpCommand,
|
|
@@ -2122,7 +2279,7 @@ var GeminiDriver = class {
|
|
|
2122
2279
|
// src/drivers/kimi.ts
|
|
2123
2280
|
import { randomUUID } from "crypto";
|
|
2124
2281
|
import { spawn as spawn6 } from "child_process";
|
|
2125
|
-
import { existsSync as existsSync5, readFileSync as readFileSync2, writeFileSync as
|
|
2282
|
+
import { existsSync as existsSync5, readFileSync as readFileSync2, writeFileSync as writeFileSync6 } from "fs";
|
|
2126
2283
|
import os2 from "os";
|
|
2127
2284
|
import path8 from "path";
|
|
2128
2285
|
var KIMI_WIRE_PROTOCOL_VERSION = "1.3";
|
|
@@ -2145,6 +2302,21 @@ var KimiDriver = class {
|
|
|
2145
2302
|
sessionId = null;
|
|
2146
2303
|
sessionAnnounced = false;
|
|
2147
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
|
+
}
|
|
2148
2320
|
spawn(ctx) {
|
|
2149
2321
|
const isResume = !!ctx.config.sessionId;
|
|
2150
2322
|
this.sessionId = ctx.config.sessionId || randomUUID();
|
|
@@ -2152,21 +2324,21 @@ var KimiDriver = class {
|
|
|
2152
2324
|
this.promptRequestId = randomUUID();
|
|
2153
2325
|
const isTsSource = ctx.chatBridgePath.endsWith(".ts");
|
|
2154
2326
|
const command = isTsSource ? "npx" : "node";
|
|
2155
|
-
const bridgeArgs =
|
|
2327
|
+
const bridgeArgs = this.buildChatBridgeArgs(ctx);
|
|
2156
2328
|
const systemPromptPath = path8.join(ctx.workingDirectory, KIMI_SYSTEM_PROMPT_FILE);
|
|
2157
2329
|
const agentFilePath = path8.join(ctx.workingDirectory, KIMI_AGENT_FILE);
|
|
2158
2330
|
const mcpConfigPath = path8.join(ctx.workingDirectory, KIMI_MCP_FILE);
|
|
2159
2331
|
if (!isResume || !existsSync5(systemPromptPath)) {
|
|
2160
|
-
|
|
2332
|
+
writeFileSync6(systemPromptPath, ctx.prompt, "utf8");
|
|
2161
2333
|
}
|
|
2162
|
-
|
|
2334
|
+
writeFileSync6(agentFilePath, [
|
|
2163
2335
|
"version: 1",
|
|
2164
2336
|
"agent:",
|
|
2165
2337
|
" extend: default",
|
|
2166
2338
|
` system_prompt_path: ./${KIMI_SYSTEM_PROMPT_FILE}`,
|
|
2167
2339
|
""
|
|
2168
2340
|
].join("\n"), "utf8");
|
|
2169
|
-
|
|
2341
|
+
writeFileSync6(mcpConfigPath, JSON.stringify({
|
|
2170
2342
|
mcpServers: {
|
|
2171
2343
|
chat: {
|
|
2172
2344
|
command,
|
|
@@ -2539,10 +2711,13 @@ function buildUnreadSummary(messages, excludeChannel) {
|
|
|
2539
2711
|
var MAX_TRAJECTORY_TEXT = 2e3;
|
|
2540
2712
|
var TRAJECTORY_COALESCE_MS = 350;
|
|
2541
2713
|
var ACTIVITY_HEARTBEAT_MS = 6e4;
|
|
2714
|
+
var COMPACTION_STALE_MS = 5 * 6e4;
|
|
2715
|
+
var RUNTIME_PROGRESS_STALE_MS = 15 * 6e4;
|
|
2542
2716
|
var MAX_STDOUT_LINES = 8;
|
|
2543
2717
|
var MAX_STDOUT_LINE_LENGTH = 240;
|
|
2544
2718
|
var MAX_STDERR_LINES = 8;
|
|
2545
2719
|
var MAX_STDERR_LINE_LENGTH = 240;
|
|
2720
|
+
var MAX_GATED_STEERING_EVENTS = 12;
|
|
2546
2721
|
var ONBOARDING_MEMORY_SEED_ENV = "SLOCK_ONBOARDING_MEMORY_SEED";
|
|
2547
2722
|
var FIRST_CINDY_SEED_MODE = "first-cindy";
|
|
2548
2723
|
function getOnboardingSeedMode(config) {
|
|
@@ -2853,6 +3028,31 @@ Success = user starts useful collaboration and setup progresses,
|
|
|
2853
3028
|
not finishing a long onboarding conversation in one channel.
|
|
2854
3029
|
`;
|
|
2855
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
|
+
}
|
|
2856
3056
|
function pushRecentLines(lines, chunk, maxLines, maxLineLength) {
|
|
2857
3057
|
const next = [...lines];
|
|
2858
3058
|
for (const rawLine of chunk.split(/\r?\n/)) {
|
|
@@ -2924,6 +3124,9 @@ function getBusyDeliveryNote(driver) {
|
|
|
2924
3124
|
if (driver.busyDeliveryMode === "direct") {
|
|
2925
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.";
|
|
2926
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
|
+
}
|
|
2927
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.";
|
|
2928
3131
|
}
|
|
2929
3132
|
var NATIVE_STANDING_PROMPT_STARTUP_INPUT = "Your system prompt contains your standing instructions. Follow it now and begin listening for messages.";
|
|
@@ -2942,6 +3145,7 @@ var AgentProcessManager = class _AgentProcessManager {
|
|
|
2942
3145
|
dataDir;
|
|
2943
3146
|
driverResolver;
|
|
2944
3147
|
defaultAgentEnvVarsProvider;
|
|
3148
|
+
tracer;
|
|
2945
3149
|
constructor(chatBridgePath, sendToServer, daemonApiKey, opts) {
|
|
2946
3150
|
this.chatBridgePath = chatBridgePath;
|
|
2947
3151
|
this.slockCliPath = opts.slockCliPath ?? "";
|
|
@@ -2951,6 +3155,7 @@ var AgentProcessManager = class _AgentProcessManager {
|
|
|
2951
3155
|
this.dataDir = opts.dataDir || DATA_DIR;
|
|
2952
3156
|
this.driverResolver = opts.driverResolver || getDriver;
|
|
2953
3157
|
this.defaultAgentEnvVarsProvider = opts.defaultAgentEnvVarsProvider || null;
|
|
3158
|
+
this.tracer = opts.tracer ?? noopTracer;
|
|
2954
3159
|
}
|
|
2955
3160
|
async startAgent(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId) {
|
|
2956
3161
|
if (this.agents.has(agentId)) {
|
|
@@ -3043,7 +3248,8 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
3043
3248
|
workingDirectory: agentDataDir,
|
|
3044
3249
|
chatBridgePath: this.chatBridgePath,
|
|
3045
3250
|
slockCliPath: this.slockCliPath,
|
|
3046
|
-
daemonApiKey: this.daemonApiKey
|
|
3251
|
+
daemonApiKey: this.daemonApiKey,
|
|
3252
|
+
launchId: launchId || null
|
|
3047
3253
|
});
|
|
3048
3254
|
const agentProcess = {
|
|
3049
3255
|
process: proc,
|
|
@@ -3056,6 +3262,11 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
3056
3262
|
notificationTimer: null,
|
|
3057
3263
|
pendingNotificationCount: 0,
|
|
3058
3264
|
activityHeartbeat: null,
|
|
3265
|
+
compactionWatchdog: null,
|
|
3266
|
+
compactionStartedAt: null,
|
|
3267
|
+
lastRuntimeEventAt: Date.now(),
|
|
3268
|
+
runtimeProgressStaleSince: null,
|
|
3269
|
+
runtimeTraceSpan: null,
|
|
3059
3270
|
lastActivity: "",
|
|
3060
3271
|
lastActivityDetail: "",
|
|
3061
3272
|
recentStdout: [],
|
|
@@ -3064,11 +3275,16 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
3064
3275
|
spawnError: null,
|
|
3065
3276
|
exitCode: null,
|
|
3066
3277
|
exitSignal: null,
|
|
3067
|
-
pendingTrajectory: null
|
|
3278
|
+
pendingTrajectory: null,
|
|
3279
|
+
gatedSteering: createGatedSteeringState()
|
|
3068
3280
|
};
|
|
3069
3281
|
this.startingInboxes.delete(agentId);
|
|
3070
3282
|
this.agents.set(agentId, agentProcess);
|
|
3283
|
+
this.startRuntimeTrace(agentId, agentProcess, "spawn");
|
|
3071
3284
|
this.agentsStarting.delete(agentId);
|
|
3285
|
+
if (wakeMessage) {
|
|
3286
|
+
this.ackInjectedRuntimeProfileMessages(agentId, [wakeMessage], agentProcess.launchId);
|
|
3287
|
+
}
|
|
3072
3288
|
let buffer = "";
|
|
3073
3289
|
proc.stdout?.on("data", (chunk) => {
|
|
3074
3290
|
const chunkText = chunk.toString();
|
|
@@ -3123,10 +3339,20 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
3123
3339
|
if (ap.activityHeartbeat) {
|
|
3124
3340
|
clearInterval(ap.activityHeartbeat);
|
|
3125
3341
|
}
|
|
3126
|
-
this.agents.delete(agentId);
|
|
3127
3342
|
const finalCode = ap.exitCode ?? code;
|
|
3128
3343
|
const finalSignal = ap.exitSignal ?? signal;
|
|
3129
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);
|
|
3130
3356
|
if (finalCode === 0) {
|
|
3131
3357
|
const queuedWakeMessage = !ap.driver.supportsStdinNotification ? ap.inbox.shift() : void 0;
|
|
3132
3358
|
const unreadSummary2 = queuedWakeMessage ? buildUnreadSummary(ap.inbox, formatChannelLabel(queuedWakeMessage)) : void 0;
|
|
@@ -3262,6 +3488,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
3262
3488
|
if (ap.activityHeartbeat) {
|
|
3263
3489
|
clearInterval(ap.activityHeartbeat);
|
|
3264
3490
|
}
|
|
3491
|
+
this.clearCompactionWatchdog(ap);
|
|
3265
3492
|
this.agents.delete(agentId);
|
|
3266
3493
|
ap.process.kill("SIGTERM");
|
|
3267
3494
|
if (!silent) {
|
|
@@ -3315,6 +3542,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
3315
3542
|
const nextMessages = ap.inbox.splice(0, ap.inbox.length);
|
|
3316
3543
|
nextMessages.push(message);
|
|
3317
3544
|
ap.isIdle = false;
|
|
3545
|
+
this.startRuntimeTrace(agentId, ap, "stdin-idle-delivery");
|
|
3318
3546
|
this.broadcastActivity(agentId, "working", "Message received");
|
|
3319
3547
|
this.deliverMessagesViaStdin(agentId, ap, nextMessages, "idle");
|
|
3320
3548
|
return;
|
|
@@ -3322,6 +3550,14 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
3322
3550
|
ap.inbox.push(message);
|
|
3323
3551
|
if (!ap.driver.supportsStdinNotification) return;
|
|
3324
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
|
+
}
|
|
3325
3561
|
ap.pendingNotificationCount++;
|
|
3326
3562
|
if (!ap.notificationTimer) {
|
|
3327
3563
|
ap.notificationTimer = setTimeout(() => {
|
|
@@ -3359,6 +3595,127 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
3359
3595
|
}
|
|
3360
3596
|
return result;
|
|
3361
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
|
+
}
|
|
3362
3719
|
// Machine-level workspace scanning
|
|
3363
3720
|
async scanAllWorkspaces() {
|
|
3364
3721
|
return scanWorkspaceDirectories(this.dataDir);
|
|
@@ -3541,6 +3898,10 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
3541
3898
|
if (activity === "working" || activity === "thinking") {
|
|
3542
3899
|
if (!ap.activityHeartbeat) {
|
|
3543
3900
|
ap.activityHeartbeat = setInterval(() => {
|
|
3901
|
+
if (this.markRuntimeProgressStaleIfNeeded(agentId, ap)) return;
|
|
3902
|
+
this.recordRuntimeTraceEvent(agentId, ap, "activity.heartbeat.sent", {
|
|
3903
|
+
activity: ap.lastActivity
|
|
3904
|
+
});
|
|
3544
3905
|
this.sendToServer({
|
|
3545
3906
|
type: "agent:activity",
|
|
3546
3907
|
agentId,
|
|
@@ -3596,27 +3957,202 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
3596
3957
|
timer: setTimeout(() => this.flushPendingTrajectory(agentId), TRAJECTORY_COALESCE_MS)
|
|
3597
3958
|
};
|
|
3598
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
|
+
}
|
|
3599
4110
|
/** Handle a single ParsedEvent from any runtime driver */
|
|
3600
4111
|
handleParsedEvent(agentId, event, driver) {
|
|
3601
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
|
+
}
|
|
3602
4121
|
switch (event.kind) {
|
|
3603
4122
|
case "session_init":
|
|
3604
4123
|
if (ap) ap.sessionId = event.sessionId;
|
|
3605
4124
|
this.sendToServer({ type: "agent:session", agentId, sessionId: event.sessionId, launchId: ap?.launchId || void 0 });
|
|
4125
|
+
this.sendRuntimeProfileReport(agentId);
|
|
3606
4126
|
break;
|
|
3607
4127
|
case "thinking": {
|
|
4128
|
+
this.finishCompactionIfActive(agentId, "Context compaction finished (inferred from resumed output)");
|
|
3608
4129
|
this.queueTrajectoryText(agentId, "thinking", event.text);
|
|
3609
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" });
|
|
3610
4133
|
break;
|
|
3611
4134
|
}
|
|
3612
4135
|
case "text": {
|
|
4136
|
+
this.finishCompactionIfActive(agentId, "Context compaction finished (inferred from resumed output)");
|
|
3613
4137
|
this.queueTrajectoryText(agentId, "text", event.text);
|
|
3614
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" });
|
|
3615
4141
|
break;
|
|
3616
4142
|
}
|
|
3617
4143
|
case "tool_call": {
|
|
4144
|
+
this.finishCompactionIfActive(agentId, "Context compaction finished (inferred from resumed tool use)");
|
|
3618
4145
|
this.flushPendingTrajectory(agentId);
|
|
3619
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
|
+
}
|
|
3620
4156
|
const inputSummary = summarizeToolInput(invocation.toolName, invocation.input);
|
|
3621
4157
|
const detail = getToolActivityLabel(invocation.toolName);
|
|
3622
4158
|
this.broadcastActivity(agentId, "working", detail, [{
|
|
@@ -3627,22 +4163,66 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
3627
4163
|
if (ap) ap.isIdle = false;
|
|
3628
4164
|
break;
|
|
3629
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
|
+
}
|
|
3630
4184
|
case "compaction_started":
|
|
3631
4185
|
this.flushPendingTrajectory(agentId);
|
|
4186
|
+
if (ap) this.recordRuntimeTraceEvent(agentId, ap, "runtime.context_compaction.started");
|
|
4187
|
+
if (ap) this.startCompactionWatchdog(agentId, ap);
|
|
3632
4188
|
this.broadcastActivity(agentId, "working", "Compacting context", [{ kind: "compaction_started" }]);
|
|
3633
|
-
if (ap)
|
|
4189
|
+
if (ap) {
|
|
4190
|
+
ap.gatedSteering.compacting = true;
|
|
4191
|
+
this.setGatedSteeringPhase(agentId, ap, "compacting", { event: "compaction_started" });
|
|
4192
|
+
ap.isIdle = false;
|
|
4193
|
+
}
|
|
3634
4194
|
break;
|
|
3635
4195
|
case "compaction_finished":
|
|
3636
4196
|
this.flushPendingTrajectory(agentId);
|
|
4197
|
+
if (ap) this.recordRuntimeTraceEvent(agentId, ap, "runtime.context_compaction.finished");
|
|
4198
|
+
if (ap) this.clearCompactionWatchdog(ap);
|
|
3637
4199
|
this.broadcastActivity(agentId, "working", "Context compaction finished", [{ kind: "compaction_finished" }]);
|
|
3638
|
-
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
|
+
}
|
|
3639
4205
|
break;
|
|
3640
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)");
|
|
3641
4209
|
this.flushPendingTrajectory(agentId);
|
|
3642
4210
|
if (ap) {
|
|
4211
|
+
this.clearGatedInFlightBatch(agentId, ap, "turn_end");
|
|
3643
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" });
|
|
3644
4216
|
if (ap.inbox.length > 0 && ap.driver.supportsStdinNotification && ap.sessionId) {
|
|
3645
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
|
+
}
|
|
3646
4226
|
this.broadcastActivity(agentId, "working", "Message received");
|
|
3647
4227
|
if (!this.deliverMessagesViaStdin(agentId, ap, nextMessages, "idle")) {
|
|
3648
4228
|
ap.isIdle = true;
|
|
@@ -3652,14 +4232,34 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
3652
4232
|
ap.isIdle = true;
|
|
3653
4233
|
this.broadcastActivity(agentId, "online", "Idle");
|
|
3654
4234
|
}
|
|
4235
|
+
this.endRuntimeTrace(ap, "ok", { outcome: "turn-completed" });
|
|
3655
4236
|
}
|
|
3656
4237
|
if (event.sessionId) {
|
|
3657
4238
|
this.sendToServer({ type: "agent:session", agentId, sessionId: event.sessionId, launchId: ap?.launchId || void 0 });
|
|
4239
|
+
this.sendRuntimeProfileReport(agentId);
|
|
3658
4240
|
}
|
|
3659
4241
|
break;
|
|
3660
4242
|
case "error": {
|
|
4243
|
+
this.finishCompactionIfActive(agentId, "Context compaction interrupted by runtime error");
|
|
3661
4244
|
this.flushPendingTrajectory(agentId);
|
|
3662
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
|
+
}
|
|
3663
4263
|
this.broadcastActivity(agentId, "error", event.message, [
|
|
3664
4264
|
{ kind: "text", text: `Error: ${event.message}` }
|
|
3665
4265
|
]);
|
|
@@ -3680,6 +4280,17 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
3680
4280
|
if (count === 0) return;
|
|
3681
4281
|
if (ap.isIdle) return;
|
|
3682
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
|
+
}
|
|
3683
4294
|
if (ap.driver.busyDeliveryMode === "direct" && ap.inbox.length > 0) {
|
|
3684
4295
|
const queuedMessages = ap.inbox.splice(0, ap.inbox.length);
|
|
3685
4296
|
console.log(`[Agent ${agentId}] Delivering queued message via stdin while busy`);
|
|
@@ -3725,6 +4336,7 @@ Respond as appropriate. Complete all your work before stopping.`;
|
|
|
3725
4336
|
`[Agent ${agentId}] Delivering ${mode} ${messages.length === 1 ? "message" : `${messages.length} messages`} via stdin from ${senders}`
|
|
3726
4337
|
);
|
|
3727
4338
|
ap.process.stdin?.write(encoded + "\n");
|
|
4339
|
+
this.ackInjectedRuntimeProfileMessages(agentId, messages, ap.launchId);
|
|
3728
4340
|
return true;
|
|
3729
4341
|
}
|
|
3730
4342
|
/** List ONE level of a directory — directories returned without children (lazy-loaded on demand) */
|
|
@@ -4055,6 +4667,10 @@ function summarizeIncomingMessage(msg) {
|
|
|
4055
4667
|
return `(agent=${msg.agentId})`;
|
|
4056
4668
|
case "agent:deliver":
|
|
4057
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})`;
|
|
4058
4674
|
case "agent:workspace:list":
|
|
4059
4675
|
return `(agent=${msg.agentId}, dir=${msg.dirPath || "."})`;
|
|
4060
4676
|
case "agent:workspace:read":
|
|
@@ -4084,12 +4700,14 @@ var DaemonCore = class {
|
|
|
4084
4700
|
agentManager;
|
|
4085
4701
|
connection;
|
|
4086
4702
|
reminderCache;
|
|
4703
|
+
tracer;
|
|
4087
4704
|
constructor(options) {
|
|
4088
4705
|
this.options = options;
|
|
4089
4706
|
this.daemonVersion = options.daemonVersion ?? readDaemonVersion();
|
|
4090
4707
|
this.chatBridgePath = options.chatBridgePath ?? resolveChatBridgePath();
|
|
4091
4708
|
this.slockCliPath = options.slockCliPath ?? resolveSlockCliPath();
|
|
4092
4709
|
this.runtimeDetector = options.runtimeDetector ?? detectRuntimes;
|
|
4710
|
+
this.tracer = options.tracer ?? noopTracer;
|
|
4093
4711
|
this.reminderCache = new ReminderCache({
|
|
4094
4712
|
clock: options.reminderClock,
|
|
4095
4713
|
onFire: (job) => this.onReminderFire(job)
|
|
@@ -4150,10 +4768,44 @@ var DaemonCore = class {
|
|
|
4150
4768
|
logger.info(`[Agent ${msg.agentId}] Workspace reset requested`);
|
|
4151
4769
|
this.agentManager.resetWorkspace(msg.agentId);
|
|
4152
4770
|
break;
|
|
4153
|
-
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
|
+
});
|
|
4154
4782
|
logger.info(`[Agent ${msg.agentId}] Delivery received (seq=${msg.seq}, from=@${msg.message.sender_name}, target=${formatChannelTarget(msg)})`);
|
|
4155
|
-
|
|
4156
|
-
|
|
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);
|
|
4157
4809
|
break;
|
|
4158
4810
|
case "agent:workspace:list":
|
|
4159
4811
|
this.agentManager.getFileTree(msg.agentId, msg.dirPath).then((files) => {
|
|
@@ -4262,6 +4914,14 @@ var DaemonCore = class {
|
|
|
4262
4914
|
for (const { agentId, sessionId, launchId } of this.agentManager.getIdleAgentSessionIds()) {
|
|
4263
4915
|
this.connection.send({ type: "agent:session", agentId, sessionId, launchId: launchId || void 0 });
|
|
4264
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
|
+
}
|
|
4265
4925
|
const agentsForSnapshot = new Set(this.agentManager.getRunningAgentIds());
|
|
4266
4926
|
for (const { agentId } of this.agentManager.getIdleAgentSessionIds()) {
|
|
4267
4927
|
agentsForSnapshot.add(agentId);
|