@slock-ai/daemon 0.43.0 → 0.46.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 +22 -105
- package/dist/{chunk-37O7EHYE.js → chunk-Q4XUZB34.js} +2122 -294
- package/dist/{chunk-JG7ONJZ6.js → chunk-Z3PCMYZO.js} +101 -1
- package/dist/cli/index.js +266 -49
- package/dist/core.js +2 -2
- package/dist/index.js +3 -3
- package/package.json +1 -1
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
|
+
DEFAULT_CHAT_BRIDGE_TOOL_TIMEOUT_MS,
|
|
2
3
|
buildWebSocketOptions,
|
|
4
|
+
executeJsonRequest,
|
|
5
|
+
executeResponseRequest,
|
|
3
6
|
logger
|
|
4
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-Z3PCMYZO.js";
|
|
5
8
|
|
|
6
9
|
// src/core.ts
|
|
7
|
-
import
|
|
8
|
-
import
|
|
10
|
+
import path15 from "path";
|
|
11
|
+
import os7 from "os";
|
|
9
12
|
import { createRequire } from "module";
|
|
10
13
|
import { accessSync } from "fs";
|
|
11
14
|
import { fileURLToPath } from "url";
|
|
@@ -94,6 +97,103 @@ var NoopActiveSpan = class {
|
|
|
94
97
|
}
|
|
95
98
|
};
|
|
96
99
|
var noopTracer = new NoopTracer();
|
|
100
|
+
var BasicTracer = class {
|
|
101
|
+
sink;
|
|
102
|
+
clock;
|
|
103
|
+
traceIdGenerator;
|
|
104
|
+
spanIdGenerator;
|
|
105
|
+
constructor({
|
|
106
|
+
sink,
|
|
107
|
+
clock = () => Date.now(),
|
|
108
|
+
traceIdGenerator = generateTraceId,
|
|
109
|
+
spanIdGenerator = generateSpanId
|
|
110
|
+
}) {
|
|
111
|
+
this.sink = sink;
|
|
112
|
+
this.clock = clock;
|
|
113
|
+
this.traceIdGenerator = traceIdGenerator;
|
|
114
|
+
this.spanIdGenerator = spanIdGenerator;
|
|
115
|
+
}
|
|
116
|
+
startSpan(name, options) {
|
|
117
|
+
const startTimeMs = options.startTimeMs ?? this.clock();
|
|
118
|
+
const context = createTraceContext({
|
|
119
|
+
parent: options.parent ?? null,
|
|
120
|
+
traceIdGenerator: this.traceIdGenerator,
|
|
121
|
+
spanIdGenerator: this.spanIdGenerator
|
|
122
|
+
});
|
|
123
|
+
return new RecordingActiveSpan({
|
|
124
|
+
context,
|
|
125
|
+
name,
|
|
126
|
+
surface: options.surface,
|
|
127
|
+
kind: options.kind ?? "internal",
|
|
128
|
+
attrs: options.attrs,
|
|
129
|
+
startTimeMs,
|
|
130
|
+
clock: this.clock,
|
|
131
|
+
sink: this.sink
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
var RecordingActiveSpan = class {
|
|
136
|
+
context;
|
|
137
|
+
name;
|
|
138
|
+
surface;
|
|
139
|
+
kind;
|
|
140
|
+
startTimeMs;
|
|
141
|
+
clock;
|
|
142
|
+
sink;
|
|
143
|
+
events = [];
|
|
144
|
+
attrs;
|
|
145
|
+
ended = false;
|
|
146
|
+
constructor({
|
|
147
|
+
context,
|
|
148
|
+
name,
|
|
149
|
+
surface,
|
|
150
|
+
kind,
|
|
151
|
+
attrs,
|
|
152
|
+
startTimeMs,
|
|
153
|
+
clock,
|
|
154
|
+
sink
|
|
155
|
+
}) {
|
|
156
|
+
this.context = context;
|
|
157
|
+
this.name = name;
|
|
158
|
+
this.surface = surface;
|
|
159
|
+
this.kind = kind;
|
|
160
|
+
this.attrs = attrs;
|
|
161
|
+
this.startTimeMs = startTimeMs;
|
|
162
|
+
this.clock = clock;
|
|
163
|
+
this.sink = sink;
|
|
164
|
+
}
|
|
165
|
+
addEvent(name, attrs) {
|
|
166
|
+
if (this.ended) return;
|
|
167
|
+
this.events.push({
|
|
168
|
+
name,
|
|
169
|
+
timeMs: this.clock(),
|
|
170
|
+
...attrs ? { attrs } : {}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
end(status = "ok", options = {}) {
|
|
174
|
+
if (this.ended) return;
|
|
175
|
+
this.ended = true;
|
|
176
|
+
const endTimeMs = this.clock();
|
|
177
|
+
const attrs = mergeAttrs(this.attrs, options.attrs);
|
|
178
|
+
this.sink.record({
|
|
179
|
+
context: this.context,
|
|
180
|
+
name: this.name,
|
|
181
|
+
surface: this.surface,
|
|
182
|
+
kind: this.kind,
|
|
183
|
+
status,
|
|
184
|
+
startTimeMs: this.startTimeMs,
|
|
185
|
+
endTimeMs,
|
|
186
|
+
durationMs: Math.max(0, endTimeMs - this.startTimeMs),
|
|
187
|
+
...attrs ? { attrs } : {},
|
|
188
|
+
events: [...this.events]
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
function mergeAttrs(base, extra) {
|
|
193
|
+
if (!base) return extra;
|
|
194
|
+
if (!extra) return base;
|
|
195
|
+
return { ...base, ...extra };
|
|
196
|
+
}
|
|
97
197
|
function generateTraceId() {
|
|
98
198
|
return randomNonZeroHex(TRACE_ID_HEX_LENGTH);
|
|
99
199
|
}
|
|
@@ -473,6 +573,9 @@ function summarizeToolInput(toolName, input) {
|
|
|
473
573
|
}
|
|
474
574
|
}
|
|
475
575
|
|
|
576
|
+
// ../shared/src/attachmentPreview.ts
|
|
577
|
+
var CSV_PREVIEW_MAX_FILE_SIZE_BYTES = 5 * 1024 * 1024;
|
|
578
|
+
|
|
476
579
|
// ../shared/src/testing/failpoints.ts
|
|
477
580
|
var NoopFailpointRegistry = class {
|
|
478
581
|
get enabled() {
|
|
@@ -548,54 +651,6 @@ var RUNTIMES = [
|
|
|
548
651
|
{ id: "gemini", displayName: "Gemini CLI", binary: "gemini", supported: true },
|
|
549
652
|
{ id: "opencode", displayName: "OpenCode", binary: "opencode", supported: true }
|
|
550
653
|
];
|
|
551
|
-
var RUNTIME_MODELS = {
|
|
552
|
-
claude: [
|
|
553
|
-
{ id: "sonnet", label: "Sonnet" },
|
|
554
|
-
{ id: "opus", label: "Opus" },
|
|
555
|
-
{ id: "haiku", label: "Haiku" }
|
|
556
|
-
],
|
|
557
|
-
codex: [
|
|
558
|
-
{ id: "gpt-5.5", label: "GPT-5.5" },
|
|
559
|
-
{ id: "gpt-5.4", label: "GPT-5.4" },
|
|
560
|
-
{ id: "gpt-5.3-codex", label: "GPT-5.3 Codex" },
|
|
561
|
-
{ id: "gpt-5.3-codex-spark", label: "GPT-5.3 Codex Spark" },
|
|
562
|
-
{ id: "gpt-5.2-codex", label: "GPT-5.2 Codex" },
|
|
563
|
-
{ id: "gpt-5.2", label: "GPT-5.2" },
|
|
564
|
-
{ id: "gpt-5.1-codex-max", label: "GPT-5.1 Codex Max" },
|
|
565
|
-
{ id: "gpt-5.1-codex", label: "GPT-5.1 Codex" },
|
|
566
|
-
{ id: "gpt-5-codex", label: "GPT-5 Codex" },
|
|
567
|
-
{ id: "gpt-5", label: "GPT-5" }
|
|
568
|
-
],
|
|
569
|
-
copilot: [
|
|
570
|
-
{ id: "gpt-5.4", label: "GPT-5.4" },
|
|
571
|
-
{ id: "gpt-5.2", label: "GPT-5.2" },
|
|
572
|
-
{ id: "claude-4-sonnet", label: "Claude 4 Sonnet" },
|
|
573
|
-
{ id: "claude-4.5-sonnet", label: "Claude 4.5 Sonnet" }
|
|
574
|
-
],
|
|
575
|
-
cursor: [
|
|
576
|
-
{ id: "composer-2-fast", label: "Composer 2 Fast" },
|
|
577
|
-
{ id: "composer-2", label: "Composer 2" },
|
|
578
|
-
{ id: "auto", label: "Auto" }
|
|
579
|
-
],
|
|
580
|
-
gemini: [
|
|
581
|
-
{ id: "gemini-3.1-pro-preview", label: "Gemini 3.1 Pro (Preview)" },
|
|
582
|
-
{ id: "gemini-3-flash-preview", label: "Gemini 3 Flash (Preview)" },
|
|
583
|
-
{ id: "gemini-2.5-pro", label: "Gemini 2.5 Pro" },
|
|
584
|
-
{ id: "gemini-2.5-flash", label: "Gemini 2.5 Flash" }
|
|
585
|
-
],
|
|
586
|
-
opencode: [
|
|
587
|
-
{ id: "opencode/gpt-5-nano", label: "GPT-5 Nano (OpenCode)" },
|
|
588
|
-
{ id: "opencode/big-pickle", label: "Big Pickle (OpenCode)" },
|
|
589
|
-
{ id: "opencode/hy3-preview-free", label: "HY3 Preview Free (OpenCode)" },
|
|
590
|
-
{ id: "opencode/minimax-m2.5-free", label: "MiniMax M2.5 Free (OpenCode)" },
|
|
591
|
-
{ id: "opencode/nemotron-3-super-free", label: "Nemotron 3 Super Free (OpenCode)" }
|
|
592
|
-
],
|
|
593
|
-
// Kimi CLI resolves model keys from each user's local config, so the safest
|
|
594
|
-
// built-in option is to defer to whatever default model the CLI already uses.
|
|
595
|
-
kimi: [
|
|
596
|
-
{ id: "default", label: "Configured Default" }
|
|
597
|
-
]
|
|
598
|
-
};
|
|
599
654
|
var PLAN_CONFIG = {
|
|
600
655
|
free: {
|
|
601
656
|
displayName: "Hobby",
|
|
@@ -631,14 +686,15 @@ var DISPLAY_PLAN_CONFIG = {
|
|
|
631
686
|
};
|
|
632
687
|
|
|
633
688
|
// src/agentProcessManager.ts
|
|
634
|
-
import { mkdirSync as mkdirSync4, readdirSync, statSync, writeFileSync as writeFileSync7 } from "fs";
|
|
689
|
+
import { mkdirSync as mkdirSync4, readdirSync as readdirSync2, statSync as statSync2, writeFileSync as writeFileSync7 } from "fs";
|
|
635
690
|
import { mkdir, writeFile, access, readdir as readdir2, stat as stat2, readFile, rm as rm2 } from "fs/promises";
|
|
636
691
|
import path11 from "path";
|
|
637
|
-
import
|
|
692
|
+
import os5 from "os";
|
|
638
693
|
|
|
639
694
|
// src/drivers/claude.ts
|
|
640
695
|
import { spawn } from "child_process";
|
|
641
|
-
import { writeFileSync as writeFileSync2 } from "fs";
|
|
696
|
+
import { existsSync as existsSync2, readdirSync, readFileSync, statSync, writeFileSync as writeFileSync2 } from "fs";
|
|
697
|
+
import os from "os";
|
|
642
698
|
import path3 from "path";
|
|
643
699
|
|
|
644
700
|
// src/drivers/cliTransport.ts
|
|
@@ -735,8 +791,11 @@ Use the \`slock\` CLI for chat / task / attachment operations. The daemon inject
|
|
|
735
791
|
16. **\`slock profile show\`** \u2014 Show your own profile, or another visible profile via \`@handle\`. Mirrors the canonical Slock profile view.
|
|
736
792
|
17. **\`slock profile update\`** \u2014 Update your own profile. Supports \`--avatar-file <path>\`, \`--display-name <name>\`, and \`--description <text>\`. Values must be non-empty. Provide at least one flag per call; multiple flags can be combined.
|
|
737
793
|
18. **\`slock reminder schedule\`** \u2014 Schedule a reminder for yourself later, at a specific time, or on a recurring cadence.
|
|
738
|
-
19. **\`slock reminder list\`** \u2014 List your reminders.
|
|
739
|
-
20. **\`slock reminder
|
|
794
|
+
19. **\`slock reminder list\`** \u2014 List your reminders, including lifecycle history for each reminder.
|
|
795
|
+
20. **\`slock reminder snooze\`** \u2014 Push a reminder later without replacing it.
|
|
796
|
+
21. **\`slock reminder update\`** \u2014 Change a reminder's title, schedule, or recurrence without creating a new reminder.
|
|
797
|
+
22. **\`slock reminder cancel\`** \u2014 Cancel one of your reminders by ID.
|
|
798
|
+
23. **\`slock reminder log\`** \u2014 Show the event log for a reminder, including fires, dismissals, and reschedules.
|
|
740
799
|
|
|
741
800
|
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:
|
|
742
801
|
- failure \u2192 stderr \`{"ok":false,"code":"...","message":"..."}\` with non-zero exit
|
|
@@ -759,22 +818,23 @@ You have MCP tools from the "chat" server. Use ONLY these for communication:
|
|
|
759
818
|
9. **${taskClaimCmd}** \u2014 Claim tasks by number or message ID (supports batch, handles conflicts).
|
|
760
819
|
10. **\`${t("unclaim_task")}\`** \u2014 Release your claim on a task.
|
|
761
820
|
11. **${taskUpdateCmd}** \u2014 Change a task's status (e.g. to in_review or done).
|
|
762
|
-
12. **\`${t("upload_file")}\`** \u2014 Upload a file to attach to a message. Returns an attachment ID to pass to ${sendCmd}.
|
|
821
|
+
12. **\`${t("upload_file")}\`** \u2014 Upload a file up to 50MB to attach to a message. Returns an attachment ID to pass to ${sendCmd}. Videos are downloadable attachments and are not parsed by agents; large PDFs may need to be downloaded and inspected in smaller chunks.
|
|
763
822
|
13. **\`${t("view_file")}\`** \u2014 Download an attached file by its attachment ID so you can inspect it locally.
|
|
764
823
|
14. **${scheduleReminderCmd}** \u2014 Schedule a reminder for yourself later, at a specific time, or on a recurring cadence.
|
|
765
824
|
15. **${listRemindersCmd}** \u2014 List your reminders.
|
|
766
825
|
16. **${cancelReminderCmd}** \u2014 Cancel one of your reminders by ID.`;
|
|
767
826
|
const reminderSection = `### Reminders
|
|
768
827
|
|
|
769
|
-
Use reminders for follow-up that depends on future state you cannot resolve now, whether user-requested or self-driven. A reminder is an author-owned, persistent, observable, and cancelable wake-up signal anchored to a Slock message or thread; when it fires, it wakes the author who scheduled it, not other people. If anchored to a message or thread, the receipt/fire system message is visible in that surface, but wake ownership does not transfer. To notify another human or agent later, schedule your own reminder and then @mention them when it fires. Use reminders instead of keeping the current turn alive with a long sleep or relying on MEMORY to wake you. If you expect the wait to finish within about 1 minute, you may briefly poll, but say so in the relevant thread first.
|
|
770
|
-
|
|
828
|
+
Use reminders for follow-up that depends on future state you cannot resolve now, whether user-requested or self-driven. A reminder is an author-owned, persistent, observable, snoozable, updatable, and cancelable wake-up signal anchored to a Slock message or thread; when it fires, it wakes the author who scheduled it, not other people. If anchored to a message or thread, the receipt/fire system message is visible in that surface, but wake ownership does not transfer. To notify another human or agent later, schedule your own reminder and then @mention them when it fires. Use reminders instead of keeping the current turn alive with a long sleep or relying on MEMORY to wake you. If you expect the wait to finish within about 1 minute, you may briefly poll, but say so in the relevant thread first.
|
|
829
|
+
When a reminder already exists, prefer \`slock reminder snooze\` to push it later, \`slock reminder update\` to change its meaning or schedule, and \`slock reminder cancel\` only when it is truly no longer needed.
|
|
830
|
+
Use ${scheduleReminderCmd} rather than runtime-native wake or cron tools such as ScheduleWakeup or CronCreate for user-visible reminders, so reminders stay author-owned, persistent, observable, snoozable, updatable, and cancelable in Slock.
|
|
771
831
|
Create agent reminders only after resolving the anchor message from the current conversation and passing its msgId explicitly; if no anchor can be resolved, consider posting a status update in the relevant thread so the intent is visible, then revisit when context is available.`;
|
|
772
832
|
const sendingMessagesSection = isCli ? `### Sending messages
|
|
773
833
|
|
|
774
834
|
- **Reply to a channel**: \`slock message send --target "#channel-name" <<'EOF'\` followed by the message body and \`EOF\`
|
|
775
|
-
- **Reply to a DM**: \`slock message send --target
|
|
835
|
+
- **Reply to a DM**: \`slock message send --target dm:@peer-name <<'EOF'\` followed by the message body and \`EOF\`
|
|
776
836
|
- **Reply in a thread**: \`slock message send --target "#channel:shortid" <<'EOF'\` followed by the message body and \`EOF\`
|
|
777
|
-
- **Start a NEW DM**: \`slock message send --target
|
|
837
|
+
- **Start a NEW DM**: \`slock message send --target dm:@person-name <<'EOF'\` followed by the message body and \`EOF\`
|
|
778
838
|
|
|
779
839
|
Message content is always read from stdin. Use a heredoc so quotes, backticks, code blocks, and newlines are not interpreted by the shell:
|
|
780
840
|
\`\`\`bash
|
|
@@ -814,10 +874,12 @@ Threads are sub-conversations attached to a specific message. They let you discu
|
|
|
814
874
|
const discoverySection = isCli ? `### Discovering people and channels
|
|
815
875
|
|
|
816
876
|
Call \`slock server info\` to see all channels in this server, which ones you have joined, other agents, and humans.
|
|
817
|
-
Visible public channels may appear even when \`joined=false\`. In that state you can still inspect them with \`slock message read\` and \`slock channel members\`, 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"
|
|
877
|
+
Visible public channels may appear even when \`joined=false\`. In that state you can still inspect them with \`slock message read\` and \`slock channel members\`, 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"\`.
|
|
878
|
+
Private channels are membership-gated. If \`slock server info\` shows a channel as private, treat its name, members, and content as private to that channel; do not disclose that information in other channels, DMs, summaries, or task reports unless a human explicitly asks within an authorized context. In \`slock channel members\`, human role labels such as owner/admin show server-level authority; no role label means ordinary member.` : `### Discovering people and channels
|
|
818
879
|
|
|
819
880
|
Call ${serverInfoCmd} to see all channels in this server, which ones you have joined, other agents, and humans.
|
|
820
|
-
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")}
|
|
881
|
+
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")}\`.
|
|
882
|
+
Private channels are membership-gated. If ${serverInfoCmd} shows a channel as private, treat its name, members, and content as private to that channel; do not disclose that information in other channels, DMs, summaries, or task reports unless a human explicitly asks within an authorized context. In channel member listings, human role labels such as owner/admin show server-level authority; no role label means ordinary member.`;
|
|
821
883
|
const channelAwarenessSection = isCli ? `### Channel awareness
|
|
822
884
|
|
|
823
885
|
Each channel has a **name** and optionally a **description** that define its purpose (visible via \`slock server info\`). Respect them:
|
|
@@ -831,7 +893,7 @@ Each channel has a **name** and optionally a **description** that define its pur
|
|
|
831
893
|
- If unsure where something belongs, call ${serverInfoCmd} to review channel descriptions.`;
|
|
832
894
|
const readingHistorySection = isCli ? `### Reading history
|
|
833
895
|
|
|
834
|
-
\`slock message read --channel "#channel-name"\` or \`slock message read --channel
|
|
896
|
+
\`slock message read --channel "#channel-name"\` or \`slock message read --channel dm:@peer-name\` or \`slock message read --channel "#channel:shortid"\`
|
|
835
897
|
|
|
836
898
|
To jump directly to a specific hit with nearby context, use \`slock message read --channel "..." --around "messageId"\` or \`slock message read --channel "..." --around 12345\`.` : `### Reading history
|
|
837
899
|
|
|
@@ -923,7 +985,7 @@ ${readCmd} shows messages in their current state. If a message was later convert
|
|
|
923
985
|
- Reuse existing tasks and threads instead of creating duplicates.
|
|
924
986
|
- Use ${taskCreateCmd} only for genuinely new subtasks or follow-up work that does not already have a canonical task.`;
|
|
925
987
|
const claimForEtiquette = isCli ? "`slock task claim`" : taskClaimCmd;
|
|
926
|
-
let prompt = `You are "${config.displayName || config.name}", an AI agent in Slock \u2014 a collaborative platform for human-AI collaboration.
|
|
988
|
+
let prompt = `You are "${config.displayName || config.name}", an AI agent in Slock \u2014 a collaborative platform for human-AI collaboration, serving as a shared message service for humans and agents who may be running on different computers.
|
|
927
989
|
|
|
928
990
|
## Who you are
|
|
929
991
|
|
|
@@ -1301,6 +1363,7 @@ var CLAUDE_DESKTOP_CLI_RELATIVE_PATH = path3.join("Applications", "Claude Code U
|
|
|
1301
1363
|
var CLAUDE_DESKTOP_CLI_SYSTEM_PATH = "/Applications/Claude Code URL Handler.app/Contents/MacOS/claude";
|
|
1302
1364
|
var CLAUDE_SYSTEM_PROMPT_FILE = "claude-system-prompt.md";
|
|
1303
1365
|
var CLAUDE_MCP_CONFIG_FILE = "claude-mcp-config.json";
|
|
1366
|
+
var SLOCK_RUNTIME_ACTIONS_MCP_SERVER_NAME = "chat";
|
|
1304
1367
|
var CLAUDE_DISALLOWED_TOOLS = [
|
|
1305
1368
|
"EnterPlanMode",
|
|
1306
1369
|
"ExitPlanMode",
|
|
@@ -1326,10 +1389,101 @@ function probeClaude(deps = {}) {
|
|
|
1326
1389
|
version: readCommandVersion(command, [], deps) ?? void 0
|
|
1327
1390
|
};
|
|
1328
1391
|
}
|
|
1392
|
+
function isRecord(value) {
|
|
1393
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1394
|
+
}
|
|
1395
|
+
function expandClaudeMcpConfigVariables(raw, vars) {
|
|
1396
|
+
let expanded = raw;
|
|
1397
|
+
for (const [name, value] of Object.entries(vars)) {
|
|
1398
|
+
expanded = expanded.replaceAll(`\${${name}}`, value);
|
|
1399
|
+
}
|
|
1400
|
+
return expanded;
|
|
1401
|
+
}
|
|
1402
|
+
function readClaudeMcpServers(configPath, vars = {}) {
|
|
1403
|
+
try {
|
|
1404
|
+
const parsed = JSON.parse(
|
|
1405
|
+
expandClaudeMcpConfigVariables(readFileSync(configPath, "utf8"), vars)
|
|
1406
|
+
);
|
|
1407
|
+
if (!isRecord(parsed) || !isRecord(parsed.mcpServers)) return null;
|
|
1408
|
+
return parsed.mcpServers;
|
|
1409
|
+
} catch (err) {
|
|
1410
|
+
logger.warn(
|
|
1411
|
+
`[Claude] failed to read MCP config ${configPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
1412
|
+
);
|
|
1413
|
+
return null;
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
function resolveClaudeConfigDir(ctx, home) {
|
|
1417
|
+
const configured = ctx.config.envVars?.CLAUDE_CONFIG_DIR || process.env.CLAUDE_CONFIG_DIR;
|
|
1418
|
+
return configured && path3.isAbsolute(configured) ? configured : path3.join(home, ".claude");
|
|
1419
|
+
}
|
|
1420
|
+
function collectClaudeMcpConfigFiles(ctx, home) {
|
|
1421
|
+
const files = [];
|
|
1422
|
+
const pushIfFile = (candidate) => {
|
|
1423
|
+
try {
|
|
1424
|
+
if (existsSync2(candidate) && statSync(candidate).isFile()) {
|
|
1425
|
+
files.push({ path: candidate });
|
|
1426
|
+
}
|
|
1427
|
+
} catch {
|
|
1428
|
+
}
|
|
1429
|
+
};
|
|
1430
|
+
pushIfFile(path3.join(home, ".claude.json"));
|
|
1431
|
+
pushIfFile(path3.join(ctx.workingDirectory, ".mcp.json"));
|
|
1432
|
+
const pluginRoot = path3.join(resolveClaudeConfigDir(ctx, home), "plugins");
|
|
1433
|
+
try {
|
|
1434
|
+
for (const entry of readdirSync(pluginRoot)) {
|
|
1435
|
+
const pluginPath = path3.join(pluginRoot, entry);
|
|
1436
|
+
const configPath = path3.join(pluginPath, ".mcp.json");
|
|
1437
|
+
try {
|
|
1438
|
+
if (existsSync2(configPath) && statSync(configPath).isFile()) {
|
|
1439
|
+
files.push({
|
|
1440
|
+
path: configPath,
|
|
1441
|
+
vars: { CLAUDE_PLUGIN_ROOT: pluginPath }
|
|
1442
|
+
});
|
|
1443
|
+
}
|
|
1444
|
+
} catch {
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
} catch {
|
|
1448
|
+
}
|
|
1449
|
+
return files;
|
|
1450
|
+
}
|
|
1451
|
+
function buildClaudeUserMcpServers(ctx, home) {
|
|
1452
|
+
const servers = /* @__PURE__ */ Object.create(null);
|
|
1453
|
+
for (const configFile of collectClaudeMcpConfigFiles(ctx, home)) {
|
|
1454
|
+
const mcpServers = readClaudeMcpServers(configFile.path, configFile.vars);
|
|
1455
|
+
if (!mcpServers) continue;
|
|
1456
|
+
for (const [name, server] of Object.entries(mcpServers)) {
|
|
1457
|
+
if (!isRecord(server)) {
|
|
1458
|
+
logger.warn(`[Claude] ignoring invalid MCP server "${name}" in ${configFile.path}`);
|
|
1459
|
+
continue;
|
|
1460
|
+
}
|
|
1461
|
+
servers[name] = server;
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
return servers;
|
|
1465
|
+
}
|
|
1329
1466
|
var ClaudeDriver = class {
|
|
1330
1467
|
id = "claude";
|
|
1468
|
+
lifecycle = {
|
|
1469
|
+
kind: "persistent",
|
|
1470
|
+
stdin: "gated",
|
|
1471
|
+
inFlightWake: "queue"
|
|
1472
|
+
};
|
|
1473
|
+
communication = {
|
|
1474
|
+
chat: "slock_cli",
|
|
1475
|
+
runtimeControl: "mcp_runtime_actions"
|
|
1476
|
+
};
|
|
1477
|
+
session = {
|
|
1478
|
+
recovery: "resume_or_fresh"
|
|
1479
|
+
};
|
|
1480
|
+
model = {
|
|
1481
|
+
detectedModelsVerifiedAs: "launchable",
|
|
1482
|
+
toLaunchSpec: (modelId) => ({ args: ["--model", modelId] })
|
|
1483
|
+
};
|
|
1331
1484
|
supportsStdinNotification = true;
|
|
1332
1485
|
mcpToolPrefix = "mcp__chat__";
|
|
1486
|
+
usesSlockCliForCommunication = true;
|
|
1333
1487
|
// Claude Code supports same-turn steering, but raw stdin injection at an
|
|
1334
1488
|
// arbitrary busy instant can collide with active signed thinking blocks. The
|
|
1335
1489
|
// daemon therefore gates busy delivery on Claude stream-json boundaries.
|
|
@@ -1364,36 +1518,46 @@ var ClaudeDriver = class {
|
|
|
1364
1518
|
}
|
|
1365
1519
|
return args;
|
|
1366
1520
|
}
|
|
1367
|
-
|
|
1521
|
+
buildRuntimeActionsMcpServer(ctx) {
|
|
1368
1522
|
const isTsSource = ctx.chatBridgePath.endsWith(".ts");
|
|
1369
1523
|
const command = isTsSource ? "npx" : "node";
|
|
1370
1524
|
const bridgeArgs = isTsSource ? ["tsx", ctx.chatBridgePath] : [ctx.chatBridgePath];
|
|
1525
|
+
return {
|
|
1526
|
+
command,
|
|
1527
|
+
args: [
|
|
1528
|
+
...bridgeArgs,
|
|
1529
|
+
"--agent-id",
|
|
1530
|
+
ctx.agentId,
|
|
1531
|
+
"--server-url",
|
|
1532
|
+
ctx.config.serverUrl,
|
|
1533
|
+
"--auth-token",
|
|
1534
|
+
ctx.config.authToken || ctx.daemonApiKey,
|
|
1535
|
+
"--runtime",
|
|
1536
|
+
this.id,
|
|
1537
|
+
...ctx.launchId ? ["--launch-id", ctx.launchId] : [],
|
|
1538
|
+
"--runtime-actions-only"
|
|
1539
|
+
]
|
|
1540
|
+
};
|
|
1541
|
+
}
|
|
1542
|
+
buildRuntimeActionsMcpConfig(ctx, home = os.homedir()) {
|
|
1543
|
+
const userMcpServers = buildClaudeUserMcpServers(ctx, home);
|
|
1544
|
+
if (Object.prototype.hasOwnProperty.call(userMcpServers, SLOCK_RUNTIME_ACTIONS_MCP_SERVER_NAME)) {
|
|
1545
|
+
logger.warn(
|
|
1546
|
+
`[Agent ${ctx.agentId}] Claude user MCP server "${SLOCK_RUNTIME_ACTIONS_MCP_SERVER_NAME}" is reserved by Slock runtime actions and will be ignored`
|
|
1547
|
+
);
|
|
1548
|
+
}
|
|
1371
1549
|
return JSON.stringify({
|
|
1372
1550
|
mcpServers: {
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
args: [
|
|
1376
|
-
...bridgeArgs,
|
|
1377
|
-
"--agent-id",
|
|
1378
|
-
ctx.agentId,
|
|
1379
|
-
"--server-url",
|
|
1380
|
-
ctx.config.serverUrl,
|
|
1381
|
-
"--auth-token",
|
|
1382
|
-
ctx.config.authToken || ctx.daemonApiKey,
|
|
1383
|
-
"--runtime",
|
|
1384
|
-
this.id,
|
|
1385
|
-
...ctx.launchId ? ["--launch-id", ctx.launchId] : [],
|
|
1386
|
-
"--runtime-actions-only"
|
|
1387
|
-
]
|
|
1388
|
-
}
|
|
1551
|
+
...userMcpServers,
|
|
1552
|
+
[SLOCK_RUNTIME_ACTIONS_MCP_SERVER_NAME]: this.buildRuntimeActionsMcpServer(ctx)
|
|
1389
1553
|
}
|
|
1390
1554
|
});
|
|
1391
1555
|
}
|
|
1392
|
-
writeClaudeLaunchFiles(ctx, slockDir) {
|
|
1556
|
+
writeClaudeLaunchFiles(ctx, slockDir, home = os.homedir()) {
|
|
1393
1557
|
const systemPromptPath = path3.join(slockDir, CLAUDE_SYSTEM_PROMPT_FILE);
|
|
1394
1558
|
const mcpConfigPath = path3.join(slockDir, CLAUDE_MCP_CONFIG_FILE);
|
|
1395
1559
|
writeFileSync2(systemPromptPath, ctx.standingPrompt, { mode: 384 });
|
|
1396
|
-
writeFileSync2(mcpConfigPath, this.buildRuntimeActionsMcpConfig(ctx), { mode: 384 });
|
|
1560
|
+
writeFileSync2(mcpConfigPath, this.buildRuntimeActionsMcpConfig(ctx, home), { mode: 384 });
|
|
1397
1561
|
return { systemPromptPath, mcpConfigPath };
|
|
1398
1562
|
}
|
|
1399
1563
|
spawn(ctx) {
|
|
@@ -1541,8 +1705,8 @@ var ClaudeDriver = class {
|
|
|
1541
1705
|
|
|
1542
1706
|
// src/drivers/codex.ts
|
|
1543
1707
|
import { spawn as spawn2, execSync } from "child_process";
|
|
1544
|
-
import { existsSync as
|
|
1545
|
-
import
|
|
1708
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
1709
|
+
import os2 from "os";
|
|
1546
1710
|
import path4 from "path";
|
|
1547
1711
|
function getCodexNotificationErrorMessage(params) {
|
|
1548
1712
|
const topLevelMessage = params?.message;
|
|
@@ -1556,7 +1720,7 @@ function getCodexNotificationErrorMessage(params) {
|
|
|
1556
1720
|
return null;
|
|
1557
1721
|
}
|
|
1558
1722
|
function ensureGitRepoForCodex(workingDirectory, deps = {}) {
|
|
1559
|
-
const existsSyncFn = deps.existsSyncFn ??
|
|
1723
|
+
const existsSyncFn = deps.existsSyncFn ?? existsSync3;
|
|
1560
1724
|
const execSyncFn = deps.execSyncFn ?? execSync;
|
|
1561
1725
|
const gitDir = path4.join(workingDirectory, ".git");
|
|
1562
1726
|
if (existsSyncFn(gitDir)) return;
|
|
@@ -1603,14 +1767,14 @@ function resolveCodexSpawn(commandArgs, deps = {}) {
|
|
|
1603
1767
|
try {
|
|
1604
1768
|
const globalRoot = execSync("npm root -g", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
1605
1769
|
const candidate = path4.join(globalRoot, "@openai", "codex", "bin", "codex.js");
|
|
1606
|
-
if (
|
|
1770
|
+
if (existsSync3(candidate)) codexEntry = candidate;
|
|
1607
1771
|
} catch {
|
|
1608
1772
|
}
|
|
1609
1773
|
if (!codexEntry) {
|
|
1610
1774
|
try {
|
|
1611
1775
|
const cmdPath = execSync("where codex", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim().split(/\r?\n/)[0];
|
|
1612
1776
|
const candidate = path4.join(path4.dirname(cmdPath), "node_modules", "@openai", "codex", "bin", "codex.js");
|
|
1613
|
-
if (
|
|
1777
|
+
if (existsSync3(candidate)) codexEntry = candidate;
|
|
1614
1778
|
} catch {
|
|
1615
1779
|
}
|
|
1616
1780
|
}
|
|
@@ -1631,8 +1795,25 @@ function joinReasoningText(item) {
|
|
|
1631
1795
|
}
|
|
1632
1796
|
var CodexDriver = class {
|
|
1633
1797
|
id = "codex";
|
|
1798
|
+
lifecycle = {
|
|
1799
|
+
kind: "persistent",
|
|
1800
|
+
stdin: "direct",
|
|
1801
|
+
inFlightWake: "steer"
|
|
1802
|
+
};
|
|
1803
|
+
communication = {
|
|
1804
|
+
chat: "slock_cli",
|
|
1805
|
+
runtimeControl: "mcp_runtime_actions"
|
|
1806
|
+
};
|
|
1807
|
+
session = {
|
|
1808
|
+
recovery: "resume_or_fresh"
|
|
1809
|
+
};
|
|
1810
|
+
model = {
|
|
1811
|
+
detectedModelsVerifiedAs: "launchable",
|
|
1812
|
+
toLaunchSpec: (modelId) => ({ params: { model: modelId } })
|
|
1813
|
+
};
|
|
1634
1814
|
supportsStdinNotification = true;
|
|
1635
1815
|
mcpToolPrefix = "mcp_chat_";
|
|
1816
|
+
usesSlockCliForCommunication = true;
|
|
1636
1817
|
busyDeliveryMode = "direct";
|
|
1637
1818
|
supportsNativeStandingPrompt = true;
|
|
1638
1819
|
probe() {
|
|
@@ -2013,12 +2194,12 @@ var CodexDriver = class {
|
|
|
2013
2194
|
return detectCodexModels();
|
|
2014
2195
|
}
|
|
2015
2196
|
};
|
|
2016
|
-
function detectCodexModels(home =
|
|
2197
|
+
function detectCodexModels(home = os2.homedir()) {
|
|
2017
2198
|
const cachePath = path4.join(home, ".codex", "models_cache.json");
|
|
2018
2199
|
const configPath = path4.join(home, ".codex", "config.toml");
|
|
2019
2200
|
let models = [];
|
|
2020
2201
|
try {
|
|
2021
|
-
const raw =
|
|
2202
|
+
const raw = readFileSync2(cachePath, "utf8");
|
|
2022
2203
|
const parsed = JSON.parse(raw);
|
|
2023
2204
|
const entries = Array.isArray(parsed?.models) ? parsed.models : [];
|
|
2024
2205
|
for (const entry of entries) {
|
|
@@ -2027,7 +2208,7 @@ function detectCodexModels(home = os.homedir()) {
|
|
|
2027
2208
|
if (entry?.visibility && entry.visibility !== "public") continue;
|
|
2028
2209
|
if (entry?.supported_in_api === false) continue;
|
|
2029
2210
|
const label = typeof entry?.display_name === "string" && entry.display_name.length > 0 ? entry.display_name : slug;
|
|
2030
|
-
models.push({ id: slug, label });
|
|
2211
|
+
models.push({ id: slug, label, verified: "launchable" });
|
|
2031
2212
|
}
|
|
2032
2213
|
} catch {
|
|
2033
2214
|
return null;
|
|
@@ -2035,7 +2216,7 @@ function detectCodexModels(home = os.homedir()) {
|
|
|
2035
2216
|
if (models.length === 0) return null;
|
|
2036
2217
|
let defaultModel;
|
|
2037
2218
|
try {
|
|
2038
|
-
const raw =
|
|
2219
|
+
const raw = readFileSync2(configPath, "utf8");
|
|
2039
2220
|
const match = raw.match(/^\s*model\s*=\s*"([^"]+)"/m);
|
|
2040
2221
|
if (match) defaultModel = match[1];
|
|
2041
2222
|
} catch {
|
|
@@ -2049,6 +2230,23 @@ import path5 from "path";
|
|
|
2049
2230
|
import { writeFileSync as writeFileSync3 } from "fs";
|
|
2050
2231
|
var CopilotDriver = class {
|
|
2051
2232
|
id = "copilot";
|
|
2233
|
+
lifecycle = {
|
|
2234
|
+
kind: "per_turn",
|
|
2235
|
+
start: "immediate",
|
|
2236
|
+
exit: "natural",
|
|
2237
|
+
inFlightWake: "spawn_new"
|
|
2238
|
+
};
|
|
2239
|
+
communication = {
|
|
2240
|
+
chat: "mcp_chat_bridge",
|
|
2241
|
+
runtimeControl: "mcp_runtime_actions"
|
|
2242
|
+
};
|
|
2243
|
+
session = {
|
|
2244
|
+
recovery: "resume_or_fresh"
|
|
2245
|
+
};
|
|
2246
|
+
model = {
|
|
2247
|
+
detectedModelsVerifiedAs: "launchable",
|
|
2248
|
+
toLaunchSpec: (modelId) => ({ args: ["--model", modelId] })
|
|
2249
|
+
};
|
|
2052
2250
|
supportsStdinNotification = false;
|
|
2053
2251
|
mcpToolPrefix = "";
|
|
2054
2252
|
busyDeliveryMode = "none";
|
|
@@ -2182,17 +2380,34 @@ var CopilotDriver = class {
|
|
|
2182
2380
|
};
|
|
2183
2381
|
|
|
2184
2382
|
// src/drivers/cursor.ts
|
|
2185
|
-
import { spawn as spawn4 } from "child_process";
|
|
2186
|
-
import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync2, existsSync as
|
|
2383
|
+
import { spawn as spawn4, spawnSync } from "child_process";
|
|
2384
|
+
import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync2, existsSync as existsSync4 } from "fs";
|
|
2187
2385
|
import path6 from "path";
|
|
2188
2386
|
var CursorDriver = class {
|
|
2189
2387
|
id = "cursor";
|
|
2388
|
+
lifecycle = {
|
|
2389
|
+
kind: "per_turn",
|
|
2390
|
+
start: "immediate",
|
|
2391
|
+
exit: "natural",
|
|
2392
|
+
inFlightWake: "spawn_new"
|
|
2393
|
+
};
|
|
2394
|
+
communication = {
|
|
2395
|
+
chat: "mcp_chat_bridge",
|
|
2396
|
+
runtimeControl: "mcp_runtime_actions"
|
|
2397
|
+
};
|
|
2398
|
+
session = {
|
|
2399
|
+
recovery: "resume_or_fresh"
|
|
2400
|
+
};
|
|
2401
|
+
model = {
|
|
2402
|
+
detectedModelsVerifiedAs: "launchable",
|
|
2403
|
+
toLaunchSpec: (modelId) => ({ args: ["--model", modelId] })
|
|
2404
|
+
};
|
|
2190
2405
|
supportsStdinNotification = false;
|
|
2191
2406
|
mcpToolPrefix = "mcp__chat__";
|
|
2192
2407
|
busyDeliveryMode = "none";
|
|
2193
2408
|
spawn(ctx) {
|
|
2194
2409
|
const cursorDir = path6.join(ctx.workingDirectory, ".cursor");
|
|
2195
|
-
if (!
|
|
2410
|
+
if (!existsSync4(cursorDir)) {
|
|
2196
2411
|
mkdirSync2(cursorDir, { recursive: true });
|
|
2197
2412
|
}
|
|
2198
2413
|
const isTsSource = ctx.chatBridgePath.endsWith(".ts");
|
|
@@ -2299,49 +2514,89 @@ var CursorDriver = class {
|
|
|
2299
2514
|
messageNotificationStyle: "poll"
|
|
2300
2515
|
});
|
|
2301
2516
|
}
|
|
2517
|
+
async detectModels() {
|
|
2518
|
+
return detectCursorModels();
|
|
2519
|
+
}
|
|
2302
2520
|
};
|
|
2521
|
+
function parseCursorModelsOutput(output) {
|
|
2522
|
+
const stripAnsi = (value) => value.replace(/\u001b\[[0-9;]*m/g, "");
|
|
2523
|
+
const models = [];
|
|
2524
|
+
let defaultModel;
|
|
2525
|
+
for (const rawLine of stripAnsi(output).split(/\r?\n/)) {
|
|
2526
|
+
const line = rawLine.trim();
|
|
2527
|
+
if (!line || /^available models$/i.test(line) || /^tip:/i.test(line)) continue;
|
|
2528
|
+
if (/^no models available/i.test(line) || /^failed to load models:/i.test(line)) continue;
|
|
2529
|
+
let modelLine = line;
|
|
2530
|
+
const markerMatch = modelLine.match(/\s+\(([^)]+)\)$/);
|
|
2531
|
+
const markers = markerMatch?.[1]?.split(",").map((part) => part.trim().toLowerCase()) ?? [];
|
|
2532
|
+
if (markers.length > 0 && markers.every((part) => part === "current" || part === "default")) {
|
|
2533
|
+
const markerStart = markerMatch?.index ?? modelLine.length;
|
|
2534
|
+
modelLine = modelLine.slice(0, markerStart).trim();
|
|
2535
|
+
}
|
|
2536
|
+
const match = modelLine.match(/^(\S+)(?:\s+-\s+(.+))?$/);
|
|
2537
|
+
if (!match) continue;
|
|
2538
|
+
const id = match[1]?.trim();
|
|
2539
|
+
if (!id || id.startsWith("-")) continue;
|
|
2540
|
+
const label = match[2]?.trim() || id;
|
|
2541
|
+
models.push({ id, label, verified: "launchable" });
|
|
2542
|
+
if (markers.includes("default")) defaultModel = id;
|
|
2543
|
+
}
|
|
2544
|
+
if (models.length === 0) return null;
|
|
2545
|
+
return { models, default: defaultModel };
|
|
2546
|
+
}
|
|
2547
|
+
function detectCursorModels(runCommand = runCursorModelsCommand) {
|
|
2548
|
+
const result = runCommand();
|
|
2549
|
+
if (result.error || result.status !== 0) return null;
|
|
2550
|
+
return parseCursorModelsOutput(String(result.stdout || ""));
|
|
2551
|
+
}
|
|
2552
|
+
function runCursorModelsCommand() {
|
|
2553
|
+
return spawnSync("cursor-agent", ["models"], {
|
|
2554
|
+
env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" },
|
|
2555
|
+
encoding: "utf8",
|
|
2556
|
+
timeout: 5e3
|
|
2557
|
+
});
|
|
2558
|
+
}
|
|
2303
2559
|
|
|
2304
2560
|
// src/drivers/gemini.ts
|
|
2305
2561
|
import { spawn as spawn5 } from "child_process";
|
|
2306
|
-
import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync3
|
|
2562
|
+
import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync3 } from "fs";
|
|
2307
2563
|
import path7 from "path";
|
|
2308
|
-
function buildGeminiSpawnEnv(ctx) {
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
// unless we explicitly trust the daemon-owned agent workspace.
|
|
2315
|
-
GEMINI_CLI_TRUST_WORKSPACE: "true",
|
|
2316
|
-
...ctx.config.envVars || {}
|
|
2317
|
-
};
|
|
2564
|
+
function buildGeminiSpawnEnv(ctx, platform = process.platform) {
|
|
2565
|
+
const { spawnEnv } = prepareCliTransport(ctx, { NO_COLOR: "1" }, platform);
|
|
2566
|
+
if (!Object.prototype.hasOwnProperty.call(ctx.config.envVars ?? {}, "GEMINI_CLI_TRUST_WORKSPACE")) {
|
|
2567
|
+
spawnEnv.GEMINI_CLI_TRUST_WORKSPACE = "true";
|
|
2568
|
+
}
|
|
2569
|
+
return spawnEnv;
|
|
2318
2570
|
}
|
|
2319
2571
|
var GeminiDriver = class {
|
|
2320
2572
|
id = "gemini";
|
|
2573
|
+
lifecycle = {
|
|
2574
|
+
kind: "per_turn",
|
|
2575
|
+
start: "immediate",
|
|
2576
|
+
exit: "natural",
|
|
2577
|
+
inFlightWake: "spawn_new"
|
|
2578
|
+
};
|
|
2579
|
+
communication = {
|
|
2580
|
+
chat: "slock_cli",
|
|
2581
|
+
runtimeControl: "mcp_runtime_actions"
|
|
2582
|
+
};
|
|
2583
|
+
session = {
|
|
2584
|
+
recovery: "resume_or_fresh"
|
|
2585
|
+
};
|
|
2586
|
+
model = {
|
|
2587
|
+
detectedModelsVerifiedAs: "suggestion_only",
|
|
2588
|
+
toLaunchSpec: (modelId) => modelId && modelId !== "default" ? { args: ["--model", modelId] } : { args: [] }
|
|
2589
|
+
};
|
|
2321
2590
|
supportsStdinNotification = false;
|
|
2322
2591
|
mcpToolPrefix = "";
|
|
2323
2592
|
busyDeliveryMode = "none";
|
|
2593
|
+
usesSlockCliForCommunication = true;
|
|
2324
2594
|
sessionId = null;
|
|
2325
2595
|
sessionAnnounced = false;
|
|
2326
2596
|
spawn(ctx) {
|
|
2327
2597
|
this.sessionId = ctx.config.sessionId || null;
|
|
2328
2598
|
this.sessionAnnounced = false;
|
|
2329
|
-
|
|
2330
|
-
if (!existsSync4(geminiDir)) {
|
|
2331
|
-
mkdirSync3(geminiDir, { recursive: true });
|
|
2332
|
-
}
|
|
2333
|
-
const isTsSource = ctx.chatBridgePath.endsWith(".ts");
|
|
2334
|
-
const mcpCommand = isTsSource ? "npx" : "node";
|
|
2335
|
-
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];
|
|
2336
|
-
const settingsPath = path7.join(geminiDir, "settings.json");
|
|
2337
|
-
writeFileSync5(settingsPath, JSON.stringify({
|
|
2338
|
-
mcpServers: {
|
|
2339
|
-
chat: {
|
|
2340
|
-
command: mcpCommand,
|
|
2341
|
-
args: mcpArgs
|
|
2342
|
-
}
|
|
2343
|
-
}
|
|
2344
|
-
}), "utf8");
|
|
2599
|
+
this.writeGeminiSettings(ctx);
|
|
2345
2600
|
const args = [
|
|
2346
2601
|
"--output-format",
|
|
2347
2602
|
"stream-json",
|
|
@@ -2415,23 +2670,56 @@ var GeminiDriver = class {
|
|
|
2415
2670
|
return null;
|
|
2416
2671
|
}
|
|
2417
2672
|
buildSystemPrompt(config, _agentId) {
|
|
2418
|
-
return
|
|
2673
|
+
return buildCliTransportSystemPrompt(config, {
|
|
2419
2674
|
toolPrefix: "",
|
|
2420
2675
|
extraCriticalRules: [
|
|
2421
|
-
"-
|
|
2676
|
+
"- Runtime Profile migration completion is the only exception to CLI-only operation: when a migration notice tells you to acknowledge with `runtime_profile_migration_done`, call the `runtime_profile_migration_done` tool with the exact `migration_key`; do not use `slock` CLI or reply in chat as the acknowledgment."
|
|
2677
|
+
],
|
|
2678
|
+
postStartupNotes: [
|
|
2679
|
+
"**Gemini runtime note:** Slock launches you as a per-turn process. Complete the current wake using `slock` CLI commands, then stop; the daemon will restart you when new messages arrive."
|
|
2422
2680
|
],
|
|
2423
|
-
postStartupNotes: [],
|
|
2424
2681
|
includeStdinNotificationSection: false,
|
|
2425
2682
|
messageNotificationStyle: "poll"
|
|
2426
2683
|
});
|
|
2427
2684
|
}
|
|
2685
|
+
writeGeminiSettings(ctx) {
|
|
2686
|
+
const geminiDir = path7.join(ctx.workingDirectory, ".gemini");
|
|
2687
|
+
mkdirSync3(geminiDir, { recursive: true });
|
|
2688
|
+
const settingsPath = path7.join(geminiDir, "settings.json");
|
|
2689
|
+
writeFileSync5(settingsPath, JSON.stringify(this.buildRuntimeActionsMcpSettings(ctx)), "utf8");
|
|
2690
|
+
}
|
|
2691
|
+
buildRuntimeActionsMcpSettings(ctx) {
|
|
2692
|
+
const isTsSource = ctx.chatBridgePath.endsWith(".ts");
|
|
2693
|
+
const command = isTsSource ? "npx" : "node";
|
|
2694
|
+
const bridgeArgs = isTsSource ? ["tsx", ctx.chatBridgePath] : [ctx.chatBridgePath];
|
|
2695
|
+
return {
|
|
2696
|
+
mcpServers: {
|
|
2697
|
+
chat: {
|
|
2698
|
+
command,
|
|
2699
|
+
args: [
|
|
2700
|
+
...bridgeArgs,
|
|
2701
|
+
"--agent-id",
|
|
2702
|
+
ctx.agentId,
|
|
2703
|
+
"--server-url",
|
|
2704
|
+
ctx.config.serverUrl,
|
|
2705
|
+
"--auth-token",
|
|
2706
|
+
ctx.config.authToken || ctx.daemonApiKey,
|
|
2707
|
+
"--runtime",
|
|
2708
|
+
this.id,
|
|
2709
|
+
...ctx.launchId ? ["--launch-id", ctx.launchId] : [],
|
|
2710
|
+
"--runtime-actions-only"
|
|
2711
|
+
]
|
|
2712
|
+
}
|
|
2713
|
+
}
|
|
2714
|
+
};
|
|
2715
|
+
}
|
|
2428
2716
|
};
|
|
2429
2717
|
|
|
2430
2718
|
// src/drivers/kimi.ts
|
|
2431
2719
|
import { randomUUID } from "crypto";
|
|
2432
2720
|
import { spawn as spawn6 } from "child_process";
|
|
2433
|
-
import { existsSync as existsSync5, readFileSync as
|
|
2434
|
-
import
|
|
2721
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync6 } from "fs";
|
|
2722
|
+
import os3 from "os";
|
|
2435
2723
|
import path8 from "path";
|
|
2436
2724
|
var KIMI_WIRE_PROTOCOL_VERSION = "1.3";
|
|
2437
2725
|
var KIMI_SYSTEM_PROMPT_FILE = ".slock-kimi-system.md";
|
|
@@ -2447,6 +2735,22 @@ function parseToolArguments(raw) {
|
|
|
2447
2735
|
}
|
|
2448
2736
|
var KimiDriver = class {
|
|
2449
2737
|
id = "kimi";
|
|
2738
|
+
lifecycle = {
|
|
2739
|
+
kind: "persistent",
|
|
2740
|
+
stdin: "direct",
|
|
2741
|
+
inFlightWake: "steer"
|
|
2742
|
+
};
|
|
2743
|
+
communication = {
|
|
2744
|
+
chat: "mcp_chat_bridge",
|
|
2745
|
+
runtimeControl: "mcp_runtime_actions"
|
|
2746
|
+
};
|
|
2747
|
+
session = {
|
|
2748
|
+
recovery: "resume_or_fresh"
|
|
2749
|
+
};
|
|
2750
|
+
model = {
|
|
2751
|
+
detectedModelsVerifiedAs: "launchable",
|
|
2752
|
+
toLaunchSpec: (modelId) => ({ args: ["--model", modelId] })
|
|
2753
|
+
};
|
|
2450
2754
|
supportsStdinNotification = true;
|
|
2451
2755
|
mcpToolPrefix = "";
|
|
2452
2756
|
busyDeliveryMode = "direct";
|
|
@@ -2631,11 +2935,11 @@ var KimiDriver = class {
|
|
|
2631
2935
|
return detectKimiModels();
|
|
2632
2936
|
}
|
|
2633
2937
|
};
|
|
2634
|
-
function detectKimiModels(home =
|
|
2938
|
+
function detectKimiModels(home = os3.homedir()) {
|
|
2635
2939
|
const configPath = path8.join(home, ".kimi", "config.toml");
|
|
2636
2940
|
let raw;
|
|
2637
2941
|
try {
|
|
2638
|
-
raw =
|
|
2942
|
+
raw = readFileSync3(configPath, "utf8");
|
|
2639
2943
|
} catch {
|
|
2640
2944
|
return null;
|
|
2641
2945
|
}
|
|
@@ -2647,7 +2951,7 @@ function detectKimiModels(home = os2.homedir()) {
|
|
|
2647
2951
|
let key = match[1].trim();
|
|
2648
2952
|
if (key.startsWith('"') && key.endsWith('"')) key = key.slice(1, -1);
|
|
2649
2953
|
if (!key) continue;
|
|
2650
|
-
models.push({ id: key, label: key });
|
|
2954
|
+
models.push({ id: key, label: key, verified: "launchable" });
|
|
2651
2955
|
}
|
|
2652
2956
|
void sectionRe;
|
|
2653
2957
|
if (models.length === 0) return null;
|
|
@@ -2658,9 +2962,9 @@ function detectKimiModels(home = os2.homedir()) {
|
|
|
2658
2962
|
}
|
|
2659
2963
|
|
|
2660
2964
|
// src/drivers/opencode.ts
|
|
2661
|
-
import { spawn as spawn7 } from "child_process";
|
|
2662
|
-
import { readFileSync as
|
|
2663
|
-
import
|
|
2965
|
+
import { spawn as spawn7, spawnSync as spawnSync2 } from "child_process";
|
|
2966
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
2967
|
+
import os4 from "os";
|
|
2664
2968
|
import path9 from "path";
|
|
2665
2969
|
var CHAT_MCP_SERVER_NAME = "chat";
|
|
2666
2970
|
var CHAT_MCP_TOOL_PREFIX = `${CHAT_MCP_SERVER_NAME}_`;
|
|
@@ -2700,10 +3004,10 @@ function parseUserOpenCodeConfig(ctx) {
|
|
|
2700
3004
|
const raw = ctx.config.envVars?.OPENCODE_CONFIG_CONTENT;
|
|
2701
3005
|
return parseOpenCodeConfigContent(raw);
|
|
2702
3006
|
}
|
|
2703
|
-
function readLocalOpenCodeConfig(home =
|
|
3007
|
+
function readLocalOpenCodeConfig(home = os4.homedir()) {
|
|
2704
3008
|
const configPath = path9.join(home, ".config", "opencode", "opencode.json");
|
|
2705
3009
|
try {
|
|
2706
|
-
return parseOpenCodeConfigContent(
|
|
3010
|
+
return parseOpenCodeConfigContent(readFileSync4(configPath, "utf8"));
|
|
2707
3011
|
} catch {
|
|
2708
3012
|
}
|
|
2709
3013
|
return {};
|
|
@@ -2749,7 +3053,7 @@ function mergeOpenCodeConfigs(localConfig, envConfig) {
|
|
|
2749
3053
|
}
|
|
2750
3054
|
};
|
|
2751
3055
|
}
|
|
2752
|
-
function buildOpenCodeConfig(ctx, home =
|
|
3056
|
+
function buildOpenCodeConfig(ctx, home = os4.homedir()) {
|
|
2753
3057
|
const userConfig = mergeOpenCodeConfigs(readLocalOpenCodeConfig(home), parseUserOpenCodeConfig(ctx));
|
|
2754
3058
|
const userAgents = recordField(userConfig.agent);
|
|
2755
3059
|
const userSlockAgent = recordField(userAgents[SLOCK_AGENT_NAME]);
|
|
@@ -2774,7 +3078,7 @@ function buildOpenCodeConfig(ctx, home = os3.homedir()) {
|
|
|
2774
3078
|
}
|
|
2775
3079
|
};
|
|
2776
3080
|
}
|
|
2777
|
-
function buildOpenCodeLaunchOptions(ctx, home =
|
|
3081
|
+
function buildOpenCodeLaunchOptions(ctx, home = os4.homedir()) {
|
|
2778
3082
|
const slock = prepareCliTransport(ctx, { NO_COLOR: "1" });
|
|
2779
3083
|
const config = buildOpenCodeConfig(ctx, home);
|
|
2780
3084
|
const env = {
|
|
@@ -2801,21 +3105,42 @@ function buildOpenCodeLaunchOptions(ctx, home = os3.homedir()) {
|
|
|
2801
3105
|
args.push("--", turnPrompt);
|
|
2802
3106
|
return { args, env, config };
|
|
2803
3107
|
}
|
|
2804
|
-
function
|
|
2805
|
-
const
|
|
2806
|
-
const
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
3108
|
+
function parseOpenCodeModelsOutput(output) {
|
|
3109
|
+
const stripAnsi = (value) => value.replace(/\u001b\[[0-9;]*m/g, "");
|
|
3110
|
+
const models = [];
|
|
3111
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3112
|
+
for (const rawLine of stripAnsi(output).split(/\r?\n/)) {
|
|
3113
|
+
const line = rawLine.trim();
|
|
3114
|
+
if (!line || line.startsWith("{") || line.startsWith("}") || line.startsWith('"')) continue;
|
|
3115
|
+
if (/^opencode models\b/i.test(line) || /^list all available models$/i.test(line)) continue;
|
|
3116
|
+
if (!line.includes("/") || /\s/.test(line) || line.startsWith("-")) continue;
|
|
3117
|
+
if (seen.has(line)) continue;
|
|
3118
|
+
seen.add(line);
|
|
3119
|
+
models.push({
|
|
3120
|
+
id: line,
|
|
3121
|
+
label: line,
|
|
3122
|
+
verified: "launchable"
|
|
3123
|
+
});
|
|
2816
3124
|
}
|
|
2817
3125
|
return models.length > 0 ? { models } : null;
|
|
2818
3126
|
}
|
|
3127
|
+
function detectOpenCodeModels(home = os4.homedir(), runCommand = runOpenCodeModelsCommand) {
|
|
3128
|
+
const commandResult = runCommand(home);
|
|
3129
|
+
if (commandResult.error || commandResult.status !== 0) return null;
|
|
3130
|
+
return parseOpenCodeModelsOutput(commandResult.stdout);
|
|
3131
|
+
}
|
|
3132
|
+
function runOpenCodeModelsCommand(home) {
|
|
3133
|
+
const result = spawnSync2("opencode", ["models"], {
|
|
3134
|
+
env: { ...process.env, HOME: home, FORCE_COLOR: "0", NO_COLOR: "1" },
|
|
3135
|
+
encoding: "utf8",
|
|
3136
|
+
timeout: 5e3
|
|
3137
|
+
});
|
|
3138
|
+
return {
|
|
3139
|
+
status: result.status,
|
|
3140
|
+
stdout: String(result.stdout || ""),
|
|
3141
|
+
error: result.error
|
|
3142
|
+
};
|
|
3143
|
+
}
|
|
2819
3144
|
function isSystemFirstMessageTask(message) {
|
|
2820
3145
|
return message.sender_id === "system" && message.channel_type === "channel" && message.channel_name === "all" && message.content.trimStart().startsWith(FIRST_MESSAGE_TASK_PREFIX);
|
|
2821
3146
|
}
|
|
@@ -2834,6 +3159,38 @@ function buildOpenCodeSystemPrompt(config) {
|
|
|
2834
3159
|
}
|
|
2835
3160
|
var OpenCodeDriver = class {
|
|
2836
3161
|
id = "opencode";
|
|
3162
|
+
lifecycle = {
|
|
3163
|
+
kind: "per_turn",
|
|
3164
|
+
start: "defer_until_concrete_message",
|
|
3165
|
+
exit: "terminate_on_turn_end",
|
|
3166
|
+
inFlightWake: "coalesce_into_pending"
|
|
3167
|
+
};
|
|
3168
|
+
communication = {
|
|
3169
|
+
chat: "slock_cli",
|
|
3170
|
+
runtimeControl: "mcp_runtime_actions"
|
|
3171
|
+
};
|
|
3172
|
+
session = {
|
|
3173
|
+
recovery: "resume_or_fresh"
|
|
3174
|
+
};
|
|
3175
|
+
model = {
|
|
3176
|
+
detectedModelsVerifiedAs: "launchable",
|
|
3177
|
+
toLaunchSpec: (modelId, ctx, opts) => {
|
|
3178
|
+
if (!ctx) return { args: ["--model", modelId] };
|
|
3179
|
+
const launchCtx = {
|
|
3180
|
+
...ctx,
|
|
3181
|
+
config: {
|
|
3182
|
+
...ctx.config,
|
|
3183
|
+
model: modelId
|
|
3184
|
+
}
|
|
3185
|
+
};
|
|
3186
|
+
const launch = buildOpenCodeLaunchOptions(launchCtx, opts?.home);
|
|
3187
|
+
return {
|
|
3188
|
+
args: launch.args,
|
|
3189
|
+
env: launch.env,
|
|
3190
|
+
config: launch.config
|
|
3191
|
+
};
|
|
3192
|
+
}
|
|
3193
|
+
};
|
|
2837
3194
|
supportsStdinNotification = false;
|
|
2838
3195
|
mcpToolPrefix = CHAT_MCP_TOOL_PREFIX;
|
|
2839
3196
|
busyDeliveryMode = "none";
|
|
@@ -3059,7 +3416,53 @@ async function deleteWorkspaceDirectory(dataDir, directoryName) {
|
|
|
3059
3416
|
}
|
|
3060
3417
|
|
|
3061
3418
|
// src/agentProcessManager.ts
|
|
3062
|
-
var DATA_DIR = path11.join(
|
|
3419
|
+
var DATA_DIR = path11.join(os5.homedir(), ".slock", "agents");
|
|
3420
|
+
var DEFAULT_MAX_CONCURRENT_AGENT_STARTS = 5;
|
|
3421
|
+
var DEFAULT_AGENT_START_INTERVAL_MS = 500;
|
|
3422
|
+
var WORKSPACE_TEXT_FILE_MAX_BYTES = 1048576;
|
|
3423
|
+
var WORKSPACE_IMAGE_PREVIEW_MAX_BYTES = 5 * 1024 * 1024;
|
|
3424
|
+
var WORKSPACE_TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
3425
|
+
".md",
|
|
3426
|
+
".txt",
|
|
3427
|
+
".json",
|
|
3428
|
+
".js",
|
|
3429
|
+
".ts",
|
|
3430
|
+
".jsx",
|
|
3431
|
+
".tsx",
|
|
3432
|
+
".yaml",
|
|
3433
|
+
".yml",
|
|
3434
|
+
".toml",
|
|
3435
|
+
".log",
|
|
3436
|
+
".csv",
|
|
3437
|
+
".xml",
|
|
3438
|
+
".html",
|
|
3439
|
+
".css",
|
|
3440
|
+
".sh",
|
|
3441
|
+
".py"
|
|
3442
|
+
]);
|
|
3443
|
+
var WORKSPACE_IMAGE_MIME_BY_EXTENSION = {
|
|
3444
|
+
".apng": "image/apng",
|
|
3445
|
+
".avif": "image/avif",
|
|
3446
|
+
".gif": "image/gif",
|
|
3447
|
+
".jpg": "image/jpeg",
|
|
3448
|
+
".jpeg": "image/jpeg",
|
|
3449
|
+
".png": "image/png",
|
|
3450
|
+
".webp": "image/webp"
|
|
3451
|
+
};
|
|
3452
|
+
function readPositiveIntegerEnv(name, fallback) {
|
|
3453
|
+
const raw = process.env[name];
|
|
3454
|
+
if (!raw) return fallback;
|
|
3455
|
+
const parsed = Number(raw);
|
|
3456
|
+
if (!Number.isFinite(parsed) || parsed < 1) return fallback;
|
|
3457
|
+
return Math.floor(parsed);
|
|
3458
|
+
}
|
|
3459
|
+
function readNonNegativeIntegerEnv(name, fallback) {
|
|
3460
|
+
const raw = process.env[name];
|
|
3461
|
+
if (!raw) return fallback;
|
|
3462
|
+
const parsed = Number(raw);
|
|
3463
|
+
if (!Number.isFinite(parsed) || parsed < 0) return fallback;
|
|
3464
|
+
return Math.floor(parsed);
|
|
3465
|
+
}
|
|
3063
3466
|
function toLocalTime(iso) {
|
|
3064
3467
|
const d = new Date(iso);
|
|
3065
3468
|
if (isNaN(d.getTime())) return iso;
|
|
@@ -3085,6 +3488,7 @@ function formatMessageTarget(message) {
|
|
|
3085
3488
|
function getMessageShortId(messageId) {
|
|
3086
3489
|
return messageId.startsWith("thread-") ? messageId.slice(7) : messageId.slice(0, 8);
|
|
3087
3490
|
}
|
|
3491
|
+
var RESPONSE_TARGET_HINT = "Reply in the channel or create/reply in a thread as appropriate; use each message's `target` and `msg` fields to choose the exact target.";
|
|
3088
3492
|
function findSessionJsonl(root, predicate) {
|
|
3089
3493
|
let visited = 0;
|
|
3090
3494
|
const maxEntries = 1e4;
|
|
@@ -3093,7 +3497,7 @@ function findSessionJsonl(root, predicate) {
|
|
|
3093
3497
|
if (depth < 0 || visited >= maxEntries) return null;
|
|
3094
3498
|
let entries;
|
|
3095
3499
|
try {
|
|
3096
|
-
entries =
|
|
3500
|
+
entries = readdirSync2(dir, { withFileTypes: true }).sort((a, b) => b.name.localeCompare(a.name));
|
|
3097
3501
|
} catch {
|
|
3098
3502
|
return null;
|
|
3099
3503
|
}
|
|
@@ -3139,11 +3543,11 @@ function writeRuntimeSessionHandoff(runtime, sessionId, fallbackDir) {
|
|
|
3139
3543
|
return null;
|
|
3140
3544
|
}
|
|
3141
3545
|
}
|
|
3142
|
-
function resolveRuntimeSessionRef(runtime, sessionId, homeDir =
|
|
3546
|
+
function resolveRuntimeSessionRef(runtime, sessionId, homeDir = os5.homedir(), fallbackDir) {
|
|
3143
3547
|
const directPath = path11.isAbsolute(sessionId) ? sessionId : null;
|
|
3144
3548
|
if (directPath) {
|
|
3145
3549
|
try {
|
|
3146
|
-
if (
|
|
3550
|
+
if (statSync2(directPath).isFile()) {
|
|
3147
3551
|
return { label: sessionId, path: directPath, runtime, reachable: true };
|
|
3148
3552
|
}
|
|
3149
3553
|
} catch {
|
|
@@ -3239,6 +3643,16 @@ function formatRuntimeProfileControlPrompt(messages) {
|
|
|
3239
3643
|
return null;
|
|
3240
3644
|
}
|
|
3241
3645
|
const body = controls.map(({ message }) => message.content).join("\n\n---\n\n");
|
|
3646
|
+
const hasMigration = controls.some(({ notification }) => notification?.kind === "migration");
|
|
3647
|
+
if (!hasMigration) {
|
|
3648
|
+
return [
|
|
3649
|
+
"Runtime Profile daemon release notice.",
|
|
3650
|
+
"",
|
|
3651
|
+
"Read the notice below before continuing. No chat reply or runtime control action is required for this notice \u2014 resume normal inbox processing afterward.",
|
|
3652
|
+
"",
|
|
3653
|
+
body
|
|
3654
|
+
].join("\n");
|
|
3655
|
+
}
|
|
3242
3656
|
return [
|
|
3243
3657
|
"Runtime Profile control notice.",
|
|
3244
3658
|
"",
|
|
@@ -3744,6 +4158,15 @@ function classifyTerminalFailure(ap) {
|
|
|
3744
4158
|
}
|
|
3745
4159
|
return null;
|
|
3746
4160
|
}
|
|
4161
|
+
function hasDirectStdinRecoveryEvidence(ap) {
|
|
4162
|
+
const candidates = [
|
|
4163
|
+
ap.lastRuntimeError,
|
|
4164
|
+
...ap.recentStderr
|
|
4165
|
+
].filter((value) => !!value);
|
|
4166
|
+
return candidates.some(
|
|
4167
|
+
(text) => /write_stdin failed|stdin is closed|closed for this session|session.*closed/i.test(text)
|
|
4168
|
+
);
|
|
4169
|
+
}
|
|
3747
4170
|
function isMissingResumeSession(ap) {
|
|
3748
4171
|
if (!ap.sessionId) return false;
|
|
3749
4172
|
const candidates = [
|
|
@@ -3783,6 +4206,14 @@ var AgentProcessManager = class _AgentProcessManager {
|
|
|
3783
4206
|
agents = /* @__PURE__ */ new Map();
|
|
3784
4207
|
agentsStarting = /* @__PURE__ */ new Set();
|
|
3785
4208
|
// Prevent concurrent starts of same agent
|
|
4209
|
+
queuedAgentStarts = /* @__PURE__ */ new Map();
|
|
4210
|
+
agentStartQueue = [];
|
|
4211
|
+
activeAgentStartCount = 0;
|
|
4212
|
+
agentStartPumpTimer = null;
|
|
4213
|
+
lastAgentStartAt = 0;
|
|
4214
|
+
lastAgentStartAgentId = null;
|
|
4215
|
+
maxConcurrentAgentStarts;
|
|
4216
|
+
agentStartIntervalMs;
|
|
3786
4217
|
startingInboxes = /* @__PURE__ */ new Map();
|
|
3787
4218
|
/** Cached configs for agents whose process exited normally — enables auto-restart on next message */
|
|
3788
4219
|
idleAgentConfigs = /* @__PURE__ */ new Map();
|
|
@@ -3796,6 +4227,7 @@ var AgentProcessManager = class _AgentProcessManager {
|
|
|
3796
4227
|
driverResolver;
|
|
3797
4228
|
defaultAgentEnvVarsProvider;
|
|
3798
4229
|
tracer;
|
|
4230
|
+
deliveryTraceContexts = /* @__PURE__ */ new WeakMap();
|
|
3799
4231
|
constructor(chatBridgePath, sendToServer, daemonApiKey, opts) {
|
|
3800
4232
|
this.chatBridgePath = chatBridgePath;
|
|
3801
4233
|
this.slockCliPath = opts.slockCliPath ?? "";
|
|
@@ -3803,59 +4235,286 @@ var AgentProcessManager = class _AgentProcessManager {
|
|
|
3803
4235
|
this.daemonApiKey = daemonApiKey;
|
|
3804
4236
|
this.serverUrl = opts.serverUrl;
|
|
3805
4237
|
this.dataDir = opts.dataDir || DATA_DIR;
|
|
3806
|
-
this.runtimeSessionHomeDir = opts.runtimeSessionHomeDir ||
|
|
4238
|
+
this.runtimeSessionHomeDir = opts.runtimeSessionHomeDir || os5.homedir();
|
|
3807
4239
|
this.driverResolver = opts.driverResolver || getDriver;
|
|
3808
4240
|
this.defaultAgentEnvVarsProvider = opts.defaultAgentEnvVarsProvider || null;
|
|
3809
4241
|
this.tracer = opts.tracer ?? noopTracer;
|
|
4242
|
+
this.maxConcurrentAgentStarts = Math.max(
|
|
4243
|
+
1,
|
|
4244
|
+
Math.floor(
|
|
4245
|
+
opts.runtimeStartScheduler?.maxConcurrentStarts ?? readPositiveIntegerEnv("SLOCK_DAEMON_MAX_CONCURRENT_AGENT_STARTS", DEFAULT_MAX_CONCURRENT_AGENT_STARTS)
|
|
4246
|
+
)
|
|
4247
|
+
);
|
|
4248
|
+
this.agentStartIntervalMs = Math.max(
|
|
4249
|
+
0,
|
|
4250
|
+
Math.floor(
|
|
4251
|
+
opts.runtimeStartScheduler?.minStartIntervalMs ?? readNonNegativeIntegerEnv("SLOCK_DAEMON_AGENT_START_INTERVAL_MS", DEFAULT_AGENT_START_INTERVAL_MS)
|
|
4252
|
+
)
|
|
4253
|
+
);
|
|
4254
|
+
}
|
|
4255
|
+
setTracer(tracer) {
|
|
4256
|
+
this.tracer = tracer;
|
|
4257
|
+
}
|
|
4258
|
+
recordDaemonTrace(name, attrs, status = "ok", parentTraceparent) {
|
|
4259
|
+
const span = this.tracer.startSpan(name, {
|
|
4260
|
+
parent: parseTraceparent(parentTraceparent),
|
|
4261
|
+
surface: "daemon",
|
|
4262
|
+
kind: "internal",
|
|
4263
|
+
attrs
|
|
4264
|
+
});
|
|
4265
|
+
span.end(status);
|
|
4266
|
+
}
|
|
4267
|
+
startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId) {
|
|
4268
|
+
return {
|
|
4269
|
+
agentId,
|
|
4270
|
+
launchId,
|
|
4271
|
+
runtime: config.runtime,
|
|
4272
|
+
model: config.model,
|
|
4273
|
+
session_id_present: Boolean(config.sessionId),
|
|
4274
|
+
launch_id_present: Boolean(launchId),
|
|
4275
|
+
wake_message_present: Boolean(wakeMessage),
|
|
4276
|
+
unread_channels_count: unreadSummary ? Object.keys(unreadSummary).length : 0,
|
|
4277
|
+
resume_prompt_present: Boolean(resumePrompt),
|
|
4278
|
+
queue_depth: this.agentStartQueue.length,
|
|
4279
|
+
active_starts: this.activeAgentStartCount,
|
|
4280
|
+
max_concurrent_starts: this.maxConcurrentAgentStarts,
|
|
4281
|
+
min_start_interval_ms: this.agentStartIntervalMs
|
|
4282
|
+
};
|
|
4283
|
+
}
|
|
4284
|
+
getDeliveryTraceContext(message) {
|
|
4285
|
+
return this.deliveryTraceContexts.get(message) ?? {};
|
|
4286
|
+
}
|
|
4287
|
+
deliveryTraceAttrs(agentId, message, attrs = {}) {
|
|
4288
|
+
const context = this.getDeliveryTraceContext(message);
|
|
4289
|
+
const deliveryCorrelationId = context.deliveryId ?? message.message_id;
|
|
4290
|
+
return {
|
|
4291
|
+
agentId,
|
|
4292
|
+
deliveryId: context.deliveryId,
|
|
4293
|
+
delivery_correlation_id: deliveryCorrelationId,
|
|
4294
|
+
channel_type: message.channel_type,
|
|
4295
|
+
sender_type: message.sender_type,
|
|
4296
|
+
messageId: message.message_id,
|
|
4297
|
+
message_id_present: Boolean(message.message_id),
|
|
4298
|
+
...attrs
|
|
4299
|
+
};
|
|
3810
4300
|
}
|
|
3811
4301
|
async startAgent(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId) {
|
|
4302
|
+
this.recordDaemonTrace("daemon.agent.start.requested", this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId));
|
|
3812
4303
|
if (this.agents.has(agentId)) {
|
|
4304
|
+
this.recordDaemonTrace("daemon.agent.start.ignored", {
|
|
4305
|
+
...this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId),
|
|
4306
|
+
reason: "already_running"
|
|
4307
|
+
});
|
|
3813
4308
|
logger.info(`[Agent ${agentId}] Start ignored (already running)`);
|
|
3814
4309
|
return;
|
|
3815
4310
|
}
|
|
3816
4311
|
if (this.agentsStarting.has(agentId)) {
|
|
4312
|
+
this.recordDaemonTrace("daemon.agent.start.ignored", {
|
|
4313
|
+
...this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId),
|
|
4314
|
+
reason: "already_starting"
|
|
4315
|
+
});
|
|
3817
4316
|
logger.info(`[Agent ${agentId}] Start ignored (startup in progress)`);
|
|
3818
4317
|
return;
|
|
3819
4318
|
}
|
|
3820
|
-
this.
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
4319
|
+
if (this.queuedAgentStarts.has(agentId)) {
|
|
4320
|
+
this.recordDaemonTrace("daemon.agent.start.ignored", {
|
|
4321
|
+
...this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId),
|
|
4322
|
+
reason: "already_queued"
|
|
4323
|
+
});
|
|
4324
|
+
logger.info(`[Agent ${agentId}] Start ignored (startup already queued)`);
|
|
4325
|
+
return;
|
|
4326
|
+
}
|
|
4327
|
+
return new Promise((resolve, reject) => {
|
|
4328
|
+
const item = {
|
|
4329
|
+
agentId,
|
|
4330
|
+
config,
|
|
4331
|
+
wakeMessage,
|
|
4332
|
+
unreadSummary,
|
|
4333
|
+
resumePrompt,
|
|
4334
|
+
launchId,
|
|
4335
|
+
resolve,
|
|
4336
|
+
reject
|
|
4337
|
+
};
|
|
4338
|
+
this.agentStartQueue.push(item);
|
|
4339
|
+
this.queuedAgentStarts.set(agentId, item);
|
|
4340
|
+
this.recordDaemonTrace("daemon.agent.start.queued", this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId));
|
|
4341
|
+
logger.info(
|
|
4342
|
+
`[Agent ${agentId}] Start queued (queue=${this.agentStartQueue.length}, active=${this.activeAgentStartCount}, max=${this.maxConcurrentAgentStarts}, interval=${this.agentStartIntervalMs}ms)`
|
|
4343
|
+
);
|
|
4344
|
+
this.pumpAgentStartQueue();
|
|
4345
|
+
});
|
|
4346
|
+
}
|
|
4347
|
+
pumpAgentStartQueue() {
|
|
4348
|
+
if (this.agentStartPumpTimer) return;
|
|
4349
|
+
if (this.agentStartQueue.length === 0) return;
|
|
4350
|
+
if (this.activeAgentStartCount >= this.maxConcurrentAgentStarts) return;
|
|
4351
|
+
const next = this.agentStartQueue[0];
|
|
4352
|
+
const shouldRateLimit = next ? next.agentId !== this.lastAgentStartAgentId : true;
|
|
4353
|
+
const elapsed = Date.now() - this.lastAgentStartAt;
|
|
4354
|
+
const waitMs = shouldRateLimit ? Math.max(0, this.agentStartIntervalMs - elapsed) : 0;
|
|
4355
|
+
if (waitMs > 0) {
|
|
4356
|
+
this.recordDaemonTrace("daemon.agent.start.rate_limited", {
|
|
4357
|
+
...this.startQueueTraceAttrs(next.agentId, next.config, next.wakeMessage, next.unreadSummary, next.resumePrompt, next.launchId),
|
|
4358
|
+
wait_ms: waitMs
|
|
4359
|
+
});
|
|
4360
|
+
this.agentStartPumpTimer = setTimeout(() => {
|
|
4361
|
+
this.agentStartPumpTimer = null;
|
|
4362
|
+
this.pumpAgentStartQueue();
|
|
4363
|
+
}, waitMs);
|
|
4364
|
+
return;
|
|
4365
|
+
}
|
|
4366
|
+
const item = this.agentStartQueue.shift();
|
|
4367
|
+
if (!item) return;
|
|
4368
|
+
if (this.queuedAgentStarts.get(item.agentId) !== item) {
|
|
4369
|
+
this.recordDaemonTrace("daemon.agent.start.skipped", {
|
|
4370
|
+
...this.startQueueTraceAttrs(item.agentId, item.config, item.wakeMessage, item.unreadSummary, item.resumePrompt, item.launchId),
|
|
4371
|
+
reason: "stale_queue_item"
|
|
4372
|
+
});
|
|
4373
|
+
this.pumpAgentStartQueue();
|
|
4374
|
+
return;
|
|
4375
|
+
}
|
|
4376
|
+
this.queuedAgentStarts.delete(item.agentId);
|
|
4377
|
+
if (this.agents.has(item.agentId) || this.agentsStarting.has(item.agentId)) {
|
|
4378
|
+
this.recordDaemonTrace("daemon.agent.start.skipped", {
|
|
4379
|
+
...this.startQueueTraceAttrs(item.agentId, item.config, item.wakeMessage, item.unreadSummary, item.resumePrompt, item.launchId),
|
|
4380
|
+
reason: "already_running_or_starting"
|
|
4381
|
+
});
|
|
4382
|
+
logger.info(`[Agent ${item.agentId}] Queued start skipped (already running or starting)`);
|
|
4383
|
+
item.resolve();
|
|
4384
|
+
this.pumpAgentStartQueue();
|
|
4385
|
+
return;
|
|
4386
|
+
}
|
|
4387
|
+
this.activeAgentStartCount++;
|
|
4388
|
+
this.lastAgentStartAt = Date.now();
|
|
4389
|
+
this.lastAgentStartAgentId = item.agentId;
|
|
4390
|
+
logger.info(
|
|
4391
|
+
`[Agent ${item.agentId}] Dequeued start (remaining=${this.agentStartQueue.length}, active=${this.activeAgentStartCount})`
|
|
4392
|
+
);
|
|
4393
|
+
this.recordDaemonTrace("daemon.agent.start.dequeued", this.startQueueTraceAttrs(item.agentId, item.config, item.wakeMessage, item.unreadSummary, item.resumePrompt, item.launchId));
|
|
4394
|
+
this.startAgentNow(
|
|
4395
|
+
item.agentId,
|
|
4396
|
+
item.config,
|
|
4397
|
+
item.wakeMessage,
|
|
4398
|
+
item.unreadSummary,
|
|
4399
|
+
item.resumePrompt,
|
|
4400
|
+
item.launchId
|
|
4401
|
+
).then(() => {
|
|
4402
|
+
this.releaseAgentStartSlot(item.agentId, "spawn attempted");
|
|
4403
|
+
item.resolve();
|
|
4404
|
+
}, (err) => {
|
|
4405
|
+
this.releaseAgentStartSlot(item.agentId, "start failed");
|
|
4406
|
+
item.reject(err);
|
|
4407
|
+
});
|
|
4408
|
+
}
|
|
4409
|
+
releaseAgentStartSlot(agentId, reason) {
|
|
4410
|
+
if (this.activeAgentStartCount <= 0) return;
|
|
4411
|
+
this.activeAgentStartCount = Math.max(0, this.activeAgentStartCount - 1);
|
|
4412
|
+
this.recordDaemonTrace("daemon.agent.start.slot_released", {
|
|
4413
|
+
agentId,
|
|
4414
|
+
reason,
|
|
4415
|
+
active_starts: this.activeAgentStartCount,
|
|
4416
|
+
queue_depth: this.agentStartQueue.length,
|
|
4417
|
+
max_concurrent_starts: this.maxConcurrentAgentStarts
|
|
4418
|
+
});
|
|
4419
|
+
logger.info(
|
|
4420
|
+
`[Agent ${agentId}] Start slot released (${reason}) (active=${this.activeAgentStartCount}, queue=${this.agentStartQueue.length})`
|
|
4421
|
+
);
|
|
4422
|
+
this.pumpAgentStartQueue();
|
|
4423
|
+
}
|
|
4424
|
+
cancelQueuedAgentStart(agentId, reason) {
|
|
4425
|
+
const item = this.queuedAgentStarts.get(agentId);
|
|
4426
|
+
if (!item) return false;
|
|
4427
|
+
this.queuedAgentStarts.delete(agentId);
|
|
4428
|
+
this.agentStartQueue = this.agentStartQueue.filter((candidate) => candidate !== item);
|
|
4429
|
+
this.startingInboxes.delete(agentId);
|
|
4430
|
+
if (this.agentStartQueue.length === 0 && this.agentStartPumpTimer) {
|
|
4431
|
+
clearTimeout(this.agentStartPumpTimer);
|
|
4432
|
+
this.agentStartPumpTimer = null;
|
|
4433
|
+
}
|
|
4434
|
+
this.recordDaemonTrace("daemon.agent.start.cancelled", {
|
|
4435
|
+
...this.startQueueTraceAttrs(agentId, item.config, item.wakeMessage, item.unreadSummary, item.resumePrompt, item.launchId),
|
|
4436
|
+
reason
|
|
4437
|
+
}, "cancelled");
|
|
4438
|
+
logger.info(`[Agent ${agentId}] Queued start cancelled (${reason})`);
|
|
4439
|
+
item.resolve();
|
|
4440
|
+
return true;
|
|
4441
|
+
}
|
|
4442
|
+
cancelAllQueuedAgentStarts(reason) {
|
|
4443
|
+
for (const item of this.agentStartQueue) {
|
|
4444
|
+
if (this.queuedAgentStarts.get(item.agentId) === item) {
|
|
4445
|
+
this.recordDaemonTrace("daemon.agent.start.cancelled", {
|
|
4446
|
+
...this.startQueueTraceAttrs(item.agentId, item.config, item.wakeMessage, item.unreadSummary, item.resumePrompt, item.launchId),
|
|
4447
|
+
reason
|
|
4448
|
+
}, "cancelled");
|
|
4449
|
+
logger.info(`[Agent ${item.agentId}] Queued start cancelled (${reason})`);
|
|
4450
|
+
item.resolve();
|
|
4451
|
+
}
|
|
4452
|
+
}
|
|
4453
|
+
this.agentStartQueue = [];
|
|
4454
|
+
this.queuedAgentStarts.clear();
|
|
4455
|
+
this.startingInboxes.clear();
|
|
4456
|
+
if (this.agentStartPumpTimer) {
|
|
4457
|
+
clearTimeout(this.agentStartPumpTimer);
|
|
4458
|
+
this.agentStartPumpTimer = null;
|
|
4459
|
+
}
|
|
4460
|
+
}
|
|
4461
|
+
async startAgentNow(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId) {
|
|
4462
|
+
if (this.agents.has(agentId)) {
|
|
4463
|
+
this.recordDaemonTrace("daemon.agent.spawn.skipped", {
|
|
4464
|
+
...this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId),
|
|
4465
|
+
reason: "already_running"
|
|
4466
|
+
});
|
|
4467
|
+
logger.info(`[Agent ${agentId}] Start ignored (already running)`);
|
|
4468
|
+
return;
|
|
4469
|
+
}
|
|
4470
|
+
if (this.agentsStarting.has(agentId)) {
|
|
4471
|
+
this.recordDaemonTrace("daemon.agent.spawn.skipped", {
|
|
4472
|
+
...this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId),
|
|
4473
|
+
reason: "already_starting"
|
|
4474
|
+
});
|
|
4475
|
+
logger.info(`[Agent ${agentId}] Start ignored (startup in progress)`);
|
|
4476
|
+
return;
|
|
4477
|
+
}
|
|
4478
|
+
this.agentsStarting.add(agentId);
|
|
4479
|
+
this.recordDaemonTrace("daemon.agent.spawn.started", this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId));
|
|
4480
|
+
try {
|
|
4481
|
+
const driver = this.driverResolver(config.runtime || "claude");
|
|
4482
|
+
const agentDataDir = path11.join(this.dataDir, agentId);
|
|
4483
|
+
await mkdir(agentDataDir, { recursive: true });
|
|
4484
|
+
const runtimeConfig = withLocalRuntimeContext(config, agentId, agentDataDir);
|
|
4485
|
+
const memoryMdPath = path11.join(agentDataDir, "MEMORY.md");
|
|
4486
|
+
try {
|
|
4487
|
+
await access(memoryMdPath);
|
|
4488
|
+
} catch {
|
|
4489
|
+
const initialMemoryMd = buildInitialMemoryMd(runtimeConfig);
|
|
4490
|
+
await writeFile(memoryMdPath, initialMemoryMd);
|
|
4491
|
+
}
|
|
4492
|
+
const notesDir = path11.join(agentDataDir, "notes");
|
|
4493
|
+
await mkdir(notesDir, { recursive: true });
|
|
4494
|
+
if (getOnboardingSeedMode(config) === FIRST_CINDY_SEED_MODE) {
|
|
4495
|
+
const seedFiles = buildOnboardingSeedFiles();
|
|
4496
|
+
for (const { relativePath, content } of seedFiles) {
|
|
4497
|
+
const fullPath = path11.join(agentDataDir, relativePath);
|
|
4498
|
+
try {
|
|
4499
|
+
await access(fullPath);
|
|
4500
|
+
} catch {
|
|
4501
|
+
await mkdir(path11.dirname(fullPath), { recursive: true });
|
|
4502
|
+
await writeFile(fullPath, content);
|
|
4503
|
+
}
|
|
4504
|
+
}
|
|
4505
|
+
}
|
|
4506
|
+
const isResume = !!runtimeConfig.sessionId;
|
|
4507
|
+
const standingPrompt = driver.buildSystemPrompt(runtimeConfig, agentId);
|
|
4508
|
+
let prompt;
|
|
4509
|
+
if (runtimeConfig.runtimeProfileControl && !wakeMessage) {
|
|
4510
|
+
prompt = driver.supportsNativeStandingPrompt ? NATIVE_STANDING_PROMPT_STARTUP_INPUT : formatRuntimeProfileControlStartupInput(runtimeConfig.runtimeProfileControl, driver);
|
|
4511
|
+
} else if (isResume && resumePrompt) {
|
|
4512
|
+
prompt = resumePrompt;
|
|
4513
|
+
prompt += getBusyDeliveryNote(driver);
|
|
4514
|
+
} else if (wakeMessage) {
|
|
4515
|
+
const runtimeProfileControlPrompt = formatRuntimeProfileControlPrompt([wakeMessage]);
|
|
4516
|
+
const channelLabel = formatChannelLabel(wakeMessage);
|
|
4517
|
+
prompt = runtimeProfileControlPrompt ?? `New message received:
|
|
3859
4518
|
|
|
3860
4519
|
${formatIncomingMessage(wakeMessage, driver)}`;
|
|
3861
4520
|
if (!runtimeProfileControlPrompt && unreadSummary && Object.keys(unreadSummary).length > 0) {
|
|
@@ -3877,6 +4536,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up, or respond to t
|
|
|
3877
4536
|
prompt += `
|
|
3878
4537
|
|
|
3879
4538
|
Respond as appropriate \u2014 ${dynamicReplyInstruction(driver)}, or take action as needed. Complete ALL your work before stopping.
|
|
4539
|
+
${RESPONSE_TARGET_HINT}
|
|
3880
4540
|
|
|
3881
4541
|
IMPORTANT: If the message requires multi-step work (e.g. research, code changes, testing), complete ALL steps before stopping. Sending a progress update does NOT mean your task is done \u2014 only stop when you have NO more work to do. ${getMessageDeliveryText(driver)}`;
|
|
3882
4542
|
prompt += getBusyDeliveryNote(driver);
|
|
@@ -3909,6 +4569,11 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
|
|
|
3909
4569
|
});
|
|
3910
4570
|
this.sendAgentStatus(agentId, "active", launchId || null);
|
|
3911
4571
|
this.broadcastActivity(agentId, "online", "Process idle");
|
|
4572
|
+
this.recordDaemonTrace("daemon.agent.spawn.deferred", {
|
|
4573
|
+
...this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId),
|
|
4574
|
+
pending_messages_count: pendingMessages.length,
|
|
4575
|
+
reason: "defer_until_concrete_message"
|
|
4576
|
+
});
|
|
3912
4577
|
logger.info(`[Agent ${agentId}] Deferred ${driver.id} spawn until first concrete message`);
|
|
3913
4578
|
for (const message of pendingMessages) {
|
|
3914
4579
|
this.deliverMessage(agentId, message);
|
|
@@ -3926,6 +4591,12 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
|
|
|
3926
4591
|
daemonApiKey: this.daemonApiKey,
|
|
3927
4592
|
launchId: launchId || null
|
|
3928
4593
|
});
|
|
4594
|
+
this.recordDaemonTrace("daemon.agent.spawn.created", {
|
|
4595
|
+
...this.startQueueTraceAttrs(agentId, effectiveConfig, wakeMessage, unreadSummary, resumePrompt, launchId),
|
|
4596
|
+
detached: false,
|
|
4597
|
+
new_session: false,
|
|
4598
|
+
process_pid_present: typeof proc.pid === "number"
|
|
4599
|
+
});
|
|
3929
4600
|
const agentProcess = {
|
|
3930
4601
|
process: proc,
|
|
3931
4602
|
driver,
|
|
@@ -3947,6 +4618,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
|
|
|
3947
4618
|
runtimeTraceSpan: null,
|
|
3948
4619
|
lastActivity: "",
|
|
3949
4620
|
lastActivityDetail: "",
|
|
4621
|
+
activityClientSeq: 0,
|
|
3950
4622
|
recentStdout: [],
|
|
3951
4623
|
recentStderr: [],
|
|
3952
4624
|
lastRuntimeError: null,
|
|
@@ -3959,7 +4631,12 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
|
|
|
3959
4631
|
};
|
|
3960
4632
|
this.startingInboxes.delete(agentId);
|
|
3961
4633
|
this.agents.set(agentId, agentProcess);
|
|
3962
|
-
this.
|
|
4634
|
+
this.idleAgentConfigs.set(agentId, {
|
|
4635
|
+
config: { ...effectiveConfig, sessionId: effectiveConfig.sessionId || null },
|
|
4636
|
+
sessionId: effectiveConfig.sessionId || null,
|
|
4637
|
+
launchId: launchId || null
|
|
4638
|
+
});
|
|
4639
|
+
this.startRuntimeTrace(agentId, agentProcess, "spawn", wakeMessage ? [wakeMessage] : void 0);
|
|
3963
4640
|
this.agentsStarting.delete(agentId);
|
|
3964
4641
|
if (config.runtimeProfileControl) {
|
|
3965
4642
|
this.ackInjectedRuntimeProfileControl(agentId, config.runtimeProfileControl, agentProcess.launchId);
|
|
@@ -3998,6 +4675,13 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
|
|
|
3998
4675
|
proc.on("error", (err) => {
|
|
3999
4676
|
const current = this.agents.get(agentId);
|
|
4000
4677
|
if (current) current.spawnError = err.message;
|
|
4678
|
+
this.recordDaemonTrace("daemon.agent.process.error", {
|
|
4679
|
+
agentId,
|
|
4680
|
+
launchId: current?.launchId || void 0,
|
|
4681
|
+
runtime: config.runtime,
|
|
4682
|
+
model: config.model,
|
|
4683
|
+
error_class: err.name || typeof err
|
|
4684
|
+
}, "error");
|
|
4001
4685
|
logger.error(`[Agent ${agentId}] Process error: ${err.message}`);
|
|
4002
4686
|
});
|
|
4003
4687
|
proc.on("exit", (code, signal) => {
|
|
@@ -4006,6 +4690,18 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
|
|
|
4006
4690
|
current.exitCode = code;
|
|
4007
4691
|
current.exitSignal = signal;
|
|
4008
4692
|
}
|
|
4693
|
+
this.recordDaemonTrace("daemon.agent.process.exited", {
|
|
4694
|
+
agentId,
|
|
4695
|
+
launchId: current?.launchId || void 0,
|
|
4696
|
+
runtime: config.runtime,
|
|
4697
|
+
model: config.model,
|
|
4698
|
+
exit_code: code,
|
|
4699
|
+
exit_signal: signal,
|
|
4700
|
+
clean_exit: code === 0,
|
|
4701
|
+
runtime_trace_active: Boolean(current?.runtimeTraceSpan),
|
|
4702
|
+
inbox_count: current?.inbox.length ?? 0,
|
|
4703
|
+
pending_notification_count: current?.pendingNotificationCount ?? 0
|
|
4704
|
+
});
|
|
4009
4705
|
logger.info(`[Agent ${agentId}] Process exited with code ${code}${signal ? ` (signal ${signal})` : ""}`);
|
|
4010
4706
|
});
|
|
4011
4707
|
proc.on("close", (code, signal) => {
|
|
@@ -4069,7 +4765,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
|
|
|
4069
4765
|
}
|
|
4070
4766
|
if (processEndedCleanly) {
|
|
4071
4767
|
let queuedWakeMessage;
|
|
4072
|
-
if (!ap.driver.supportsStdinNotification) {
|
|
4768
|
+
if (!ap.driver.supportsStdinNotification || ap.expectedTerminationReason === "stalled_recovery") {
|
|
4073
4769
|
while (ap.inbox.length > 0) {
|
|
4074
4770
|
const candidate = ap.inbox.shift();
|
|
4075
4771
|
if (this.shouldDeferWakeMessage(agentId, ap.driver, candidate)) continue;
|
|
@@ -4175,7 +4871,68 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
|
|
|
4175
4871
|
}
|
|
4176
4872
|
return leftKeys.every((key) => left?.[key] === right?.[key]);
|
|
4177
4873
|
}
|
|
4874
|
+
enqueueRuntimeProfileNotification(agentId, ap, message, kind, key) {
|
|
4875
|
+
ap.inbox.push(message);
|
|
4876
|
+
if (ap.driver.supportsStdinNotification && ap.sessionId) {
|
|
4877
|
+
ap.pendingNotificationCount++;
|
|
4878
|
+
if (ap.driver.busyDeliveryMode === "gated") {
|
|
4879
|
+
this.recordGatedSteeringEvent(agentId, ap, "buffer", {
|
|
4880
|
+
reason: "runtime_profile",
|
|
4881
|
+
kind,
|
|
4882
|
+
pendingMessages: ap.inbox.length
|
|
4883
|
+
});
|
|
4884
|
+
} else if (!ap.notificationTimer) {
|
|
4885
|
+
ap.notificationTimer = setTimeout(() => {
|
|
4886
|
+
this.sendStdinNotification(agentId);
|
|
4887
|
+
}, 3e3);
|
|
4888
|
+
}
|
|
4889
|
+
}
|
|
4890
|
+
this.recordDaemonTrace("daemon.agent.runtime_profile.routed", {
|
|
4891
|
+
agentId,
|
|
4892
|
+
kind,
|
|
4893
|
+
key_present: Boolean(key),
|
|
4894
|
+
outcome: ap.sessionId ? "queued_busy" : "queued_before_session",
|
|
4895
|
+
runtime: ap.config.runtime,
|
|
4896
|
+
session_id_present: Boolean(ap.sessionId),
|
|
4897
|
+
launchId: ap.launchId || void 0,
|
|
4898
|
+
inbox_count: ap.inbox.length,
|
|
4899
|
+
pending_notification_count: ap.pendingNotificationCount,
|
|
4900
|
+
busy_delivery_mode: ap.driver.busyDeliveryMode,
|
|
4901
|
+
supports_stdin_notification: ap.driver.supportsStdinNotification
|
|
4902
|
+
});
|
|
4903
|
+
logger.info(
|
|
4904
|
+
`[Agent ${agentId}] Queued runtime profile ${kind} ${key} for ${ap.sessionId ? "busy" : "pre-session"} ${ap.driver.id} delivery`
|
|
4905
|
+
);
|
|
4906
|
+
}
|
|
4907
|
+
queueRuntimeProfileNotificationDuringStart(agentId, message, kind, key) {
|
|
4908
|
+
const pending = this.startingInboxes.get(agentId) || [];
|
|
4909
|
+
pending.push(message);
|
|
4910
|
+
this.startingInboxes.set(agentId, pending);
|
|
4911
|
+
const queuedStart = this.queuedAgentStarts.get(agentId);
|
|
4912
|
+
this.recordDaemonTrace("daemon.agent.runtime_profile.routed", {
|
|
4913
|
+
agentId,
|
|
4914
|
+
kind,
|
|
4915
|
+
key_present: Boolean(key),
|
|
4916
|
+
outcome: "queued_during_start",
|
|
4917
|
+
startup_pending: true,
|
|
4918
|
+
starting_inbox_count: pending.length,
|
|
4919
|
+
launchId: queuedStart?.launchId
|
|
4920
|
+
});
|
|
4921
|
+
logger.info(`[Agent ${agentId}] Queued runtime profile ${kind} ${key} during startup`);
|
|
4922
|
+
}
|
|
4923
|
+
splitRuntimeProfileControlBatch(messages) {
|
|
4924
|
+
const controlMessages = messages.filter((message) => runtimeProfileNotificationFromMessage(message));
|
|
4925
|
+
if (controlMessages.length === 0 || controlMessages.length === messages.length) {
|
|
4926
|
+
return { nextMessages: messages, deferredMessages: [] };
|
|
4927
|
+
}
|
|
4928
|
+
const deferredMessages = messages.filter((message) => !runtimeProfileNotificationFromMessage(message));
|
|
4929
|
+
return { nextMessages: controlMessages, deferredMessages };
|
|
4930
|
+
}
|
|
4931
|
+
containsOrdinaryInboxMessage(messages) {
|
|
4932
|
+
return messages.some((message) => !runtimeProfileNotificationFromMessage(message));
|
|
4933
|
+
}
|
|
4178
4934
|
async stopAgent(agentId, { wait = false, silent = false } = {}) {
|
|
4935
|
+
this.cancelQueuedAgentStart(agentId, "stop requested");
|
|
4179
4936
|
this.idleAgentConfigs.delete(agentId);
|
|
4180
4937
|
const ap = this.agents.get(agentId);
|
|
4181
4938
|
if (!ap) {
|
|
@@ -4222,51 +4979,155 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
|
|
|
4222
4979
|
});
|
|
4223
4980
|
}
|
|
4224
4981
|
}
|
|
4225
|
-
deliverMessage(agentId, message) {
|
|
4982
|
+
deliverMessage(agentId, message, traceContext = {}) {
|
|
4983
|
+
if (traceContext.deliveryId) {
|
|
4984
|
+
this.deliveryTraceContexts.set(message, traceContext);
|
|
4985
|
+
}
|
|
4226
4986
|
const ap = this.agents.get(agentId);
|
|
4227
4987
|
if (!ap) {
|
|
4228
|
-
if (this.agentsStarting.has(agentId)) {
|
|
4988
|
+
if (this.agentsStarting.has(agentId) || this.queuedAgentStarts.has(agentId)) {
|
|
4989
|
+
const queuedStart = this.queuedAgentStarts.get(agentId);
|
|
4229
4990
|
const pending = this.startingInboxes.get(agentId) || [];
|
|
4230
4991
|
pending.push(message);
|
|
4231
4992
|
this.startingInboxes.set(agentId, pending);
|
|
4232
|
-
|
|
4993
|
+
this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
|
|
4994
|
+
outcome: "queued_during_start",
|
|
4995
|
+
accepted: true,
|
|
4996
|
+
process_present: false,
|
|
4997
|
+
startup_pending: true,
|
|
4998
|
+
starting_inbox_count: pending.length,
|
|
4999
|
+
launchId: queuedStart?.launchId
|
|
5000
|
+
}));
|
|
5001
|
+
return true;
|
|
4233
5002
|
}
|
|
4234
5003
|
const cached = this.idleAgentConfigs.get(agentId);
|
|
4235
5004
|
if (cached) {
|
|
4236
5005
|
const driver = this.driverResolver(cached.config.runtime || "claude");
|
|
4237
5006
|
if (this.shouldDeferWakeMessage(agentId, driver, message)) {
|
|
4238
|
-
|
|
5007
|
+
this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
|
|
5008
|
+
outcome: "deferred_wake_message",
|
|
5009
|
+
accepted: true,
|
|
5010
|
+
process_present: false,
|
|
5011
|
+
cached_idle_config_present: true,
|
|
5012
|
+
runtime: cached.config.runtime,
|
|
5013
|
+
session_id_present: Boolean(cached.sessionId),
|
|
5014
|
+
launchId: cached.launchId || void 0
|
|
5015
|
+
}));
|
|
5016
|
+
return true;
|
|
4239
5017
|
}
|
|
4240
5018
|
logger.info(`[Agent ${agentId}] Starting from idle state for new message`);
|
|
4241
5019
|
this.idleAgentConfigs.delete(agentId);
|
|
5020
|
+
this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
|
|
5021
|
+
outcome: "auto_restart_from_idle",
|
|
5022
|
+
accepted: true,
|
|
5023
|
+
process_present: false,
|
|
5024
|
+
cached_idle_config_present: true,
|
|
5025
|
+
runtime: cached.config.runtime,
|
|
5026
|
+
session_id_present: Boolean(cached.sessionId),
|
|
5027
|
+
launchId: cached.launchId || void 0
|
|
5028
|
+
}));
|
|
4242
5029
|
this.startAgent(agentId, cached.config, message, void 0, void 0, cached.launchId || void 0).catch((err) => {
|
|
4243
5030
|
logger.error(`[Agent ${agentId}] Failed to auto-restart`, err);
|
|
4244
5031
|
});
|
|
5032
|
+
return true;
|
|
4245
5033
|
}
|
|
4246
|
-
|
|
5034
|
+
logger.warn(`[Agent ${agentId}] Delivery received but no running process or cached idle config exists`);
|
|
5035
|
+
this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
|
|
5036
|
+
outcome: "rejected_no_process",
|
|
5037
|
+
accepted: false,
|
|
5038
|
+
process_present: false,
|
|
5039
|
+
cached_idle_config_present: false
|
|
5040
|
+
}), "error");
|
|
5041
|
+
this.sendAgentStatus(agentId, "inactive", null);
|
|
5042
|
+
this.broadcastActivity(agentId, "offline", "Process unavailable; restart required");
|
|
5043
|
+
return false;
|
|
4247
5044
|
}
|
|
4248
5045
|
if (this.shouldDeferWakeMessage(agentId, ap.driver, message)) {
|
|
4249
|
-
|
|
5046
|
+
this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
|
|
5047
|
+
outcome: "deferred_wake_message",
|
|
5048
|
+
accepted: true,
|
|
5049
|
+
process_present: true,
|
|
5050
|
+
runtime: ap.config.runtime,
|
|
5051
|
+
session_id_present: Boolean(ap.sessionId),
|
|
5052
|
+
launchId: ap.launchId || void 0,
|
|
5053
|
+
is_idle: ap.isIdle,
|
|
5054
|
+
inbox_count: ap.inbox.length
|
|
5055
|
+
}));
|
|
5056
|
+
return true;
|
|
4250
5057
|
}
|
|
4251
5058
|
if (ap.isIdle && ap.driver.supportsStdinNotification && ap.sessionId) {
|
|
4252
5059
|
const nextMessages = ap.inbox.splice(0, ap.inbox.length);
|
|
4253
5060
|
nextMessages.push(message);
|
|
4254
5061
|
ap.isIdle = false;
|
|
4255
|
-
this.startRuntimeTrace(agentId, ap, "stdin-idle-delivery");
|
|
5062
|
+
this.startRuntimeTrace(agentId, ap, "stdin-idle-delivery", nextMessages);
|
|
4256
5063
|
this.broadcastActivity(agentId, "working", "Message received");
|
|
4257
|
-
this.deliverMessagesViaStdin(agentId, ap, nextMessages, "idle");
|
|
4258
|
-
|
|
5064
|
+
const stdinAccepted = this.deliverMessagesViaStdin(agentId, ap, nextMessages, "idle");
|
|
5065
|
+
this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
|
|
5066
|
+
outcome: "stdin_idle_delivery",
|
|
5067
|
+
accepted: true,
|
|
5068
|
+
process_present: true,
|
|
5069
|
+
runtime: ap.config.runtime,
|
|
5070
|
+
session_id_present: true,
|
|
5071
|
+
launchId: ap.launchId || void 0,
|
|
5072
|
+
stdin_delivery_accepted: stdinAccepted,
|
|
5073
|
+
delivered_messages_count: nextMessages.length
|
|
5074
|
+
}));
|
|
5075
|
+
return true;
|
|
4259
5076
|
}
|
|
4260
5077
|
ap.inbox.push(message);
|
|
4261
|
-
if (
|
|
4262
|
-
|
|
5078
|
+
if (this.recoverStaleProcessForQueuedMessageIfNeeded(agentId, ap)) {
|
|
5079
|
+
this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
|
|
5080
|
+
outcome: "queued_stalled_recovery",
|
|
5081
|
+
accepted: true,
|
|
5082
|
+
process_present: true,
|
|
5083
|
+
runtime: ap.config.runtime,
|
|
5084
|
+
session_id_present: Boolean(ap.sessionId),
|
|
5085
|
+
launchId: ap.launchId || void 0,
|
|
5086
|
+
inbox_count: ap.inbox.length
|
|
5087
|
+
}));
|
|
5088
|
+
return true;
|
|
5089
|
+
}
|
|
5090
|
+
if (!ap.driver.supportsStdinNotification) {
|
|
5091
|
+
this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
|
|
5092
|
+
outcome: "queued_busy_non_stdin",
|
|
5093
|
+
accepted: true,
|
|
5094
|
+
process_present: true,
|
|
5095
|
+
runtime: ap.config.runtime,
|
|
5096
|
+
session_id_present: Boolean(ap.sessionId),
|
|
5097
|
+
launchId: ap.launchId || void 0,
|
|
5098
|
+
inbox_count: ap.inbox.length
|
|
5099
|
+
}));
|
|
5100
|
+
return true;
|
|
5101
|
+
}
|
|
5102
|
+
if (!ap.sessionId) {
|
|
5103
|
+
this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
|
|
5104
|
+
outcome: "queued_before_session",
|
|
5105
|
+
accepted: true,
|
|
5106
|
+
process_present: true,
|
|
5107
|
+
runtime: ap.config.runtime,
|
|
5108
|
+
session_id_present: false,
|
|
5109
|
+
launchId: ap.launchId || void 0,
|
|
5110
|
+
inbox_count: ap.inbox.length
|
|
5111
|
+
}));
|
|
5112
|
+
return true;
|
|
5113
|
+
}
|
|
4263
5114
|
if (ap.driver.busyDeliveryMode === "gated") {
|
|
4264
5115
|
ap.pendingNotificationCount++;
|
|
4265
5116
|
this.recordGatedSteeringEvent(agentId, ap, "buffer", {
|
|
4266
5117
|
reason: "busy_message",
|
|
4267
5118
|
pendingMessages: ap.inbox.length
|
|
4268
5119
|
});
|
|
4269
|
-
|
|
5120
|
+
this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
|
|
5121
|
+
outcome: "queued_busy_gated",
|
|
5122
|
+
accepted: true,
|
|
5123
|
+
process_present: true,
|
|
5124
|
+
runtime: ap.config.runtime,
|
|
5125
|
+
session_id_present: true,
|
|
5126
|
+
launchId: ap.launchId || void 0,
|
|
5127
|
+
inbox_count: ap.inbox.length,
|
|
5128
|
+
pending_notification_count: ap.pendingNotificationCount
|
|
5129
|
+
}));
|
|
5130
|
+
return true;
|
|
4270
5131
|
}
|
|
4271
5132
|
ap.pendingNotificationCount++;
|
|
4272
5133
|
if (!ap.notificationTimer) {
|
|
@@ -4274,6 +5135,17 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
|
|
|
4274
5135
|
this.sendStdinNotification(agentId);
|
|
4275
5136
|
}, 3e3);
|
|
4276
5137
|
}
|
|
5138
|
+
this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
|
|
5139
|
+
outcome: "queued_busy_notification",
|
|
5140
|
+
accepted: true,
|
|
5141
|
+
process_present: true,
|
|
5142
|
+
runtime: ap.config.runtime,
|
|
5143
|
+
session_id_present: true,
|
|
5144
|
+
inbox_count: ap.inbox.length,
|
|
5145
|
+
pending_notification_count: ap.pendingNotificationCount,
|
|
5146
|
+
notification_timer_present: Boolean(ap.notificationTimer)
|
|
5147
|
+
}));
|
|
5148
|
+
return true;
|
|
4277
5149
|
}
|
|
4278
5150
|
async resetWorkspace(agentId) {
|
|
4279
5151
|
const agentDataDir = path11.join(this.dataDir, agentId);
|
|
@@ -4285,6 +5157,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
|
|
|
4285
5157
|
}
|
|
4286
5158
|
}
|
|
4287
5159
|
async stopAll() {
|
|
5160
|
+
this.cancelAllQueuedAgentStarts("daemon shutdown");
|
|
4288
5161
|
this.idleAgentConfigs.clear();
|
|
4289
5162
|
const ids = [...this.agents.keys()];
|
|
4290
5163
|
await Promise.all(ids.map((id) => this.stopAgent(id, { wait: true, silent: true })));
|
|
@@ -4306,6 +5179,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
|
|
|
4306
5179
|
getIdleAgentSessionIds() {
|
|
4307
5180
|
const result = [];
|
|
4308
5181
|
for (const [agentId, { sessionId, launchId }] of this.idleAgentConfigs) {
|
|
5182
|
+
if (this.agents.has(agentId)) continue;
|
|
4309
5183
|
if (sessionId) result.push({ agentId, sessionId, launchId });
|
|
4310
5184
|
}
|
|
4311
5185
|
return result;
|
|
@@ -4357,7 +5231,17 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
|
|
|
4357
5231
|
}
|
|
4358
5232
|
return reports;
|
|
4359
5233
|
}
|
|
4360
|
-
deliverRuntimeProfileNotification(agentId, key, kind, content) {
|
|
5234
|
+
deliverRuntimeProfileNotification(agentId, key, kind, content, traceparent) {
|
|
5235
|
+
const span = this.tracer.startSpan("daemon.runtime_profile.control.inject", {
|
|
5236
|
+
parent: parseTraceparent(traceparent),
|
|
5237
|
+
surface: "daemon",
|
|
5238
|
+
kind: "consumer",
|
|
5239
|
+
attrs: {
|
|
5240
|
+
agentId,
|
|
5241
|
+
control_kind: kind,
|
|
5242
|
+
key_present: Boolean(key)
|
|
5243
|
+
}
|
|
5244
|
+
});
|
|
4361
5245
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4362
5246
|
const message = {
|
|
4363
5247
|
channel_id: "system",
|
|
@@ -4368,18 +5252,65 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
|
|
|
4368
5252
|
sender_type: "system",
|
|
4369
5253
|
content,
|
|
4370
5254
|
timestamp: now,
|
|
4371
|
-
message_id: `${kind === "migration" ? RUNTIME_PROFILE_MIGRATION_MESSAGE_PREFIX : RUNTIME_PROFILE_DAEMON_NOTICE_MESSAGE_PREFIX}${key}
|
|
5255
|
+
message_id: `${kind === "migration" ? RUNTIME_PROFILE_MIGRATION_MESSAGE_PREFIX : RUNTIME_PROFILE_DAEMON_NOTICE_MESSAGE_PREFIX}${key}`,
|
|
5256
|
+
traceparent: formatTraceparent(span.context)
|
|
4372
5257
|
};
|
|
4373
5258
|
const ap = this.agents.get(agentId);
|
|
5259
|
+
if (ap && !(ap.sessionId && ap.driver.supportsStdinNotification && ap.isIdle) && !(ap.sessionId && ap.driver.busyDeliveryMode === "direct")) {
|
|
5260
|
+
this.enqueueRuntimeProfileNotification(agentId, ap, message, kind, key);
|
|
5261
|
+
span.end("ok", {
|
|
5262
|
+
attrs: {
|
|
5263
|
+
outcome: ap.sessionId ? "queued_busy" : "queued_before_session",
|
|
5264
|
+
runtime: ap.config.runtime,
|
|
5265
|
+
launchId: ap.launchId || void 0,
|
|
5266
|
+
session_id_present: Boolean(ap.sessionId),
|
|
5267
|
+
supports_stdin_notification: ap.driver.supportsStdinNotification,
|
|
5268
|
+
busy_delivery_mode: ap.driver.busyDeliveryMode
|
|
5269
|
+
}
|
|
5270
|
+
});
|
|
5271
|
+
return true;
|
|
5272
|
+
}
|
|
4374
5273
|
if (ap?.sessionId && ap.driver.supportsStdinNotification && ap.isIdle) {
|
|
4375
5274
|
ap.isIdle = false;
|
|
4376
5275
|
this.startRuntimeTrace(agentId, ap, "runtime-profile");
|
|
4377
|
-
this.deliverMessagesViaStdin(agentId, ap, [message], "idle");
|
|
4378
|
-
|
|
5276
|
+
const written = this.deliverMessagesViaStdin(agentId, ap, [message], "idle");
|
|
5277
|
+
span.end(written ? "ok" : "error", {
|
|
5278
|
+
attrs: {
|
|
5279
|
+
outcome: written ? "stdin_idle" : "stdin_failed",
|
|
5280
|
+
runtime: ap.config.runtime,
|
|
5281
|
+
launchId: ap.launchId || void 0,
|
|
5282
|
+
session_id_present: true,
|
|
5283
|
+
supports_stdin_notification: true,
|
|
5284
|
+
busy_delivery_mode: ap.driver.busyDeliveryMode
|
|
5285
|
+
}
|
|
5286
|
+
});
|
|
5287
|
+
return written;
|
|
4379
5288
|
}
|
|
4380
5289
|
if (ap?.sessionId && ap.driver.busyDeliveryMode === "direct") {
|
|
4381
|
-
this.deliverMessagesViaStdin(agentId, ap, [message], "busy");
|
|
4382
|
-
|
|
5290
|
+
const written = this.deliverMessagesViaStdin(agentId, ap, [message], "busy");
|
|
5291
|
+
span.end(written ? "ok" : "error", {
|
|
5292
|
+
attrs: {
|
|
5293
|
+
outcome: written ? "stdin_busy" : "stdin_failed",
|
|
5294
|
+
runtime: ap.config.runtime,
|
|
5295
|
+
launchId: ap.launchId || void 0,
|
|
5296
|
+
session_id_present: true,
|
|
5297
|
+
supports_stdin_notification: ap.driver.supportsStdinNotification,
|
|
5298
|
+
busy_delivery_mode: ap.driver.busyDeliveryMode
|
|
5299
|
+
}
|
|
5300
|
+
});
|
|
5301
|
+
return written;
|
|
5302
|
+
}
|
|
5303
|
+
if (this.agentsStarting.has(agentId) || this.queuedAgentStarts.has(agentId)) {
|
|
5304
|
+
const queuedStart = this.queuedAgentStarts.get(agentId);
|
|
5305
|
+
this.queueRuntimeProfileNotificationDuringStart(agentId, message, kind, key);
|
|
5306
|
+
span.end("ok", {
|
|
5307
|
+
attrs: {
|
|
5308
|
+
outcome: "queued_during_start",
|
|
5309
|
+
startup_pending: true,
|
|
5310
|
+
launchId: queuedStart?.launchId
|
|
5311
|
+
}
|
|
5312
|
+
});
|
|
5313
|
+
return true;
|
|
4383
5314
|
}
|
|
4384
5315
|
const cached = this.idleAgentConfigs.get(agentId);
|
|
4385
5316
|
if (cached) {
|
|
@@ -4389,9 +5320,19 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
|
|
|
4389
5320
|
logger.error(`[Agent ${agentId}] Failed to auto-restart for runtime profile notification`, err);
|
|
4390
5321
|
this.idleAgentConfigs.set(agentId, cached);
|
|
4391
5322
|
});
|
|
4392
|
-
|
|
5323
|
+
span.end("ok", {
|
|
5324
|
+
attrs: {
|
|
5325
|
+
outcome: "restart_queued",
|
|
5326
|
+
runtime: cached.config.runtime,
|
|
5327
|
+
launchId: cached.launchId || void 0,
|
|
5328
|
+
session_id_present: Boolean(cached.sessionId)
|
|
5329
|
+
}
|
|
5330
|
+
});
|
|
5331
|
+
return true;
|
|
4393
5332
|
}
|
|
4394
5333
|
logger.warn(`[Agent ${agentId}] Runtime profile ${kind} ${key} has no runtime injection path yet; leaving unacked for retry`);
|
|
5334
|
+
span.end("ok", { attrs: { outcome: "no_path" } });
|
|
5335
|
+
return false;
|
|
4395
5336
|
}
|
|
4396
5337
|
ackInjectedRuntimeProfileMessages(agentId, messages, launchId) {
|
|
4397
5338
|
for (const message of messages) {
|
|
@@ -4404,19 +5345,32 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
|
|
|
4404
5345
|
type: "agent:runtime_profile:migration:ack",
|
|
4405
5346
|
agentId,
|
|
4406
5347
|
migrationKey: notification.key,
|
|
4407
|
-
launchId: launchId || void 0
|
|
5348
|
+
launchId: launchId || void 0,
|
|
5349
|
+
traceparent: message.traceparent
|
|
4408
5350
|
});
|
|
4409
5351
|
} else {
|
|
4410
5352
|
this.sendToServer({
|
|
4411
5353
|
type: "agent:runtime_profile:daemon_release_notice:ack",
|
|
4412
5354
|
agentId,
|
|
4413
5355
|
noticeKey: notification.key,
|
|
4414
|
-
launchId: launchId || void 0
|
|
5356
|
+
launchId: launchId || void 0,
|
|
5357
|
+
traceparent: message.traceparent
|
|
4415
5358
|
});
|
|
4416
5359
|
}
|
|
4417
5360
|
}
|
|
4418
5361
|
}
|
|
4419
5362
|
ackInjectedRuntimeProfileControl(agentId, control, launchId) {
|
|
5363
|
+
const span = this.tracer.startSpan("daemon.runtime_profile.control.inject", {
|
|
5364
|
+
surface: "daemon",
|
|
5365
|
+
kind: "internal",
|
|
5366
|
+
attrs: {
|
|
5367
|
+
agentId,
|
|
5368
|
+
control_kind: control.kind,
|
|
5369
|
+
key_present: Boolean(control.key),
|
|
5370
|
+
launchId: launchId || void 0,
|
|
5371
|
+
source: "agent_config"
|
|
5372
|
+
}
|
|
5373
|
+
});
|
|
4420
5374
|
const title = runtimeProfileNotificationTitle(control.kind);
|
|
4421
5375
|
this.broadcastActivity(agentId, "working", title, [{ kind: "system", title, text: control.message }], launchId);
|
|
4422
5376
|
if (control.kind === "migration") {
|
|
@@ -4424,24 +5378,41 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
|
|
|
4424
5378
|
type: "agent:runtime_profile:migration:ack",
|
|
4425
5379
|
agentId,
|
|
4426
5380
|
migrationKey: control.key,
|
|
4427
|
-
launchId: launchId || void 0
|
|
5381
|
+
launchId: launchId || void 0,
|
|
5382
|
+
traceparent: formatTraceparent(span.context)
|
|
4428
5383
|
});
|
|
4429
5384
|
} else {
|
|
4430
5385
|
this.sendToServer({
|
|
4431
5386
|
type: "agent:runtime_profile:daemon_release_notice:ack",
|
|
4432
5387
|
agentId,
|
|
4433
5388
|
noticeKey: control.key,
|
|
4434
|
-
launchId: launchId || void 0
|
|
5389
|
+
launchId: launchId || void 0,
|
|
5390
|
+
traceparent: formatTraceparent(span.context)
|
|
4435
5391
|
});
|
|
4436
5392
|
}
|
|
5393
|
+
span.end("ok", { attrs: { outcome: "agent_config_ack_sent" } });
|
|
4437
5394
|
}
|
|
4438
5395
|
sendRuntimeProfileWireReport(report) {
|
|
5396
|
+
const span = this.tracer.startSpan("daemon.runtime_profile.report.sent", {
|
|
5397
|
+
surface: "daemon",
|
|
5398
|
+
kind: "producer",
|
|
5399
|
+
attrs: {
|
|
5400
|
+
agentId: report.agentId,
|
|
5401
|
+
launchId: report.launchId || void 0,
|
|
5402
|
+
runtime: report.facts.runtime,
|
|
5403
|
+
model_present: Boolean(report.facts.model),
|
|
5404
|
+
session_ref_present: Boolean(report.facts.sessionRef),
|
|
5405
|
+
workspace_ref_present: Boolean(report.facts.workspaceRef || report.facts.workspacePathRef)
|
|
5406
|
+
}
|
|
5407
|
+
});
|
|
4439
5408
|
this.sendToServer({
|
|
4440
5409
|
type: "agent:runtime_profile",
|
|
4441
5410
|
agentId: report.agentId,
|
|
4442
5411
|
facts: report.facts,
|
|
4443
|
-
launchId: report.launchId || void 0
|
|
5412
|
+
launchId: report.launchId || void 0,
|
|
5413
|
+
traceparent: formatTraceparent(span.context)
|
|
4444
5414
|
});
|
|
5415
|
+
span.end("ok");
|
|
4445
5416
|
}
|
|
4446
5417
|
sendRuntimeProfileReportFor(agentId, config, sessionId, launchId) {
|
|
4447
5418
|
this.sendRuntimeProfileWireReport(this.buildRuntimeProfileReport(agentId, config, sessionId, launchId));
|
|
@@ -4484,32 +5455,21 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
|
|
|
4484
5455
|
}
|
|
4485
5456
|
const info = await stat2(resolved);
|
|
4486
5457
|
if (info.isDirectory()) throw new Error("Cannot read a directory");
|
|
4487
|
-
const TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
4488
|
-
".md",
|
|
4489
|
-
".txt",
|
|
4490
|
-
".json",
|
|
4491
|
-
".js",
|
|
4492
|
-
".ts",
|
|
4493
|
-
".jsx",
|
|
4494
|
-
".tsx",
|
|
4495
|
-
".yaml",
|
|
4496
|
-
".yml",
|
|
4497
|
-
".toml",
|
|
4498
|
-
".log",
|
|
4499
|
-
".csv",
|
|
4500
|
-
".xml",
|
|
4501
|
-
".html",
|
|
4502
|
-
".css",
|
|
4503
|
-
".sh",
|
|
4504
|
-
".py"
|
|
4505
|
-
]);
|
|
4506
5458
|
const ext = path11.extname(resolved).toLowerCase();
|
|
4507
|
-
if (
|
|
4508
|
-
|
|
5459
|
+
if (WORKSPACE_TEXT_EXTENSIONS.has(ext) || ext === "") {
|
|
5460
|
+
if (info.size > WORKSPACE_TEXT_FILE_MAX_BYTES) throw new Error("File too large");
|
|
5461
|
+
const content = await readFile(resolved, "utf-8");
|
|
5462
|
+
return { content, binary: false, size: info.size, encoding: "utf-8" };
|
|
5463
|
+
}
|
|
5464
|
+
const imageMimeType = WORKSPACE_IMAGE_MIME_BY_EXTENSION[ext];
|
|
5465
|
+
if (imageMimeType) {
|
|
5466
|
+
if (info.size > WORKSPACE_IMAGE_PREVIEW_MAX_BYTES) {
|
|
5467
|
+
return { content: null, binary: true, size: info.size, mimeType: imageMimeType };
|
|
5468
|
+
}
|
|
5469
|
+
const content = await readFile(resolved, "base64");
|
|
5470
|
+
return { content, binary: true, size: info.size, mimeType: imageMimeType, encoding: "base64" };
|
|
4509
5471
|
}
|
|
4510
|
-
|
|
4511
|
-
const content = await readFile(resolved, "utf-8");
|
|
4512
|
-
return { content, binary: false };
|
|
5472
|
+
return { content: null, binary: true, size: info.size };
|
|
4513
5473
|
}
|
|
4514
5474
|
// Skill scanning
|
|
4515
5475
|
// Per-runtime skill search paths (relative to home dir for global, workspace dir for workspace).
|
|
@@ -4529,7 +5489,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
|
|
|
4529
5489
|
async listSkills(agentId, runtimeHint) {
|
|
4530
5490
|
const agent = this.agents.get(agentId);
|
|
4531
5491
|
const runtime = runtimeHint || agent?.config.runtime || "claude";
|
|
4532
|
-
const home =
|
|
5492
|
+
const home = os5.homedir();
|
|
4533
5493
|
const workspaceDir = path11.join(this.dataDir, agentId);
|
|
4534
5494
|
const paths = _AgentProcessManager.SKILL_PATHS[runtime] || _AgentProcessManager.SKILL_PATHS.claude;
|
|
4535
5495
|
const globalResults = await Promise.all(
|
|
@@ -4619,13 +5579,15 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
|
|
|
4619
5579
|
if (!hasToolStart) {
|
|
4620
5580
|
entries.push({ kind: "status", activity, detail });
|
|
4621
5581
|
}
|
|
5582
|
+
if (ap) ap.activityClientSeq += 1;
|
|
4622
5583
|
this.sendToServer({
|
|
4623
5584
|
type: "agent:activity",
|
|
4624
5585
|
agentId,
|
|
4625
5586
|
activity,
|
|
4626
5587
|
detail,
|
|
4627
5588
|
entries,
|
|
4628
|
-
launchId: launchIdOverride || ap?.launchId || void 0
|
|
5589
|
+
launchId: launchIdOverride || ap?.launchId || void 0,
|
|
5590
|
+
clientSeq: ap?.activityClientSeq
|
|
4629
5591
|
});
|
|
4630
5592
|
if (ap) {
|
|
4631
5593
|
ap.lastActivity = activity;
|
|
@@ -4637,12 +5599,14 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
|
|
|
4637
5599
|
this.recordRuntimeTraceEvent(agentId, ap, "activity.heartbeat.sent", {
|
|
4638
5600
|
activity: ap.lastActivity
|
|
4639
5601
|
});
|
|
5602
|
+
ap.activityClientSeq += 1;
|
|
4640
5603
|
this.sendToServer({
|
|
4641
5604
|
type: "agent:activity",
|
|
4642
5605
|
agentId,
|
|
4643
5606
|
activity: ap.lastActivity,
|
|
4644
5607
|
detail: ap.lastActivityDetail,
|
|
4645
|
-
launchId: launchIdOverride || ap.launchId || void 0
|
|
5608
|
+
launchId: launchIdOverride || ap.launchId || void 0,
|
|
5609
|
+
clientSeq: ap.activityClientSeq
|
|
4646
5610
|
});
|
|
4647
5611
|
}, ACTIVITY_HEARTBEAT_MS);
|
|
4648
5612
|
}
|
|
@@ -4654,6 +5618,42 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
|
|
|
4654
5618
|
}
|
|
4655
5619
|
}
|
|
4656
5620
|
}
|
|
5621
|
+
/**
|
|
5622
|
+
* Respond to a server-issued `agent:activity_probe`. Echoes the
|
|
5623
|
+
* agent's current `lastActivity` back through the existing
|
|
5624
|
+
* `agent:activity` upstream channel with the matching `probeId`.
|
|
5625
|
+
*
|
|
5626
|
+
* Why this exists: the server's stale-activity sweep used to
|
|
5627
|
+
* synthesize `online` whenever a transient state went 90s without
|
|
5628
|
+
* an update. That invented state without consulting ground truth
|
|
5629
|
+
* and produced "agent shows green/idle but is actually working" UI
|
|
5630
|
+
* staleness (#engineering:72283cf7 task #340 RCA).
|
|
5631
|
+
*
|
|
5632
|
+
* The new flow: server sends `agent:activity_probe` for stale
|
|
5633
|
+
* agents, daemon replies here with the *real* current activity, and
|
|
5634
|
+
* the server only falls back to synth-online if the probe times out
|
|
5635
|
+
* (5s). The body is intentionally minimal — no entries, no
|
|
5636
|
+
* heartbeat side-effects, no state mutation. We just echo what we
|
|
5637
|
+
* already know.
|
|
5638
|
+
*
|
|
5639
|
+
* If the agent is no longer running locally (`ap` undefined), we
|
|
5640
|
+
* report `offline` so the server stops believing the agent is busy.
|
|
5641
|
+
*/
|
|
5642
|
+
respondToActivityProbe(agentId, probeId) {
|
|
5643
|
+
const ap = this.agents.get(agentId);
|
|
5644
|
+
const activity = ap?.lastActivity || "offline";
|
|
5645
|
+
const detail = ap?.lastActivityDetail || (ap ? "" : "Agent not running");
|
|
5646
|
+
if (ap) ap.activityClientSeq += 1;
|
|
5647
|
+
this.sendToServer({
|
|
5648
|
+
type: "agent:activity",
|
|
5649
|
+
agentId,
|
|
5650
|
+
activity,
|
|
5651
|
+
detail,
|
|
5652
|
+
launchId: ap?.launchId || void 0,
|
|
5653
|
+
probeId,
|
|
5654
|
+
clientSeq: ap?.activityClientSeq
|
|
5655
|
+
});
|
|
5656
|
+
}
|
|
4657
5657
|
flushPendingTrajectory(agentId) {
|
|
4658
5658
|
const ap = this.agents.get(agentId);
|
|
4659
5659
|
const pending = ap?.pendingTrajectory;
|
|
@@ -4719,7 +5719,30 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
|
|
|
4719
5719
|
this.clearCompactionWatchdog(ap);
|
|
4720
5720
|
this.broadcastActivity(agentId, "working", detail, [{ kind: "compaction_finished" }]);
|
|
4721
5721
|
}
|
|
4722
|
-
|
|
5722
|
+
messagesTraceAttrs(messages) {
|
|
5723
|
+
if (!messages || messages.length === 0) return {};
|
|
5724
|
+
const first = messages[0];
|
|
5725
|
+
const context = this.getDeliveryTraceContext(first);
|
|
5726
|
+
const notification = runtimeProfileNotificationFromMessage(first);
|
|
5727
|
+
if (notification) {
|
|
5728
|
+
return {
|
|
5729
|
+
messages_count: messages.length,
|
|
5730
|
+
message_id_present: Boolean(first.message_id),
|
|
5731
|
+
deliveryId: context.deliveryId,
|
|
5732
|
+
delivery_correlation_id: context.deliveryId,
|
|
5733
|
+
control_kind: notification.kind,
|
|
5734
|
+
key_present: Boolean(notification.key)
|
|
5735
|
+
};
|
|
5736
|
+
}
|
|
5737
|
+
return {
|
|
5738
|
+
messages_count: messages.length,
|
|
5739
|
+
messageId: first.message_id,
|
|
5740
|
+
message_id_present: Boolean(first.message_id),
|
|
5741
|
+
deliveryId: context.deliveryId,
|
|
5742
|
+
delivery_correlation_id: context.deliveryId ?? first.message_id
|
|
5743
|
+
};
|
|
5744
|
+
}
|
|
5745
|
+
startRuntimeTrace(agentId, ap, reason, messages) {
|
|
4723
5746
|
if (ap.runtimeTraceSpan) return ap.runtimeTraceSpan;
|
|
4724
5747
|
const span = this.tracer.startSpan("daemon.runtime.turn", {
|
|
4725
5748
|
surface: "daemon",
|
|
@@ -4729,10 +5752,11 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
|
|
|
4729
5752
|
runtime: ap.config.runtime,
|
|
4730
5753
|
model: ap.config.model,
|
|
4731
5754
|
reason,
|
|
4732
|
-
hasSession: Boolean(ap.sessionId)
|
|
5755
|
+
hasSession: Boolean(ap.sessionId),
|
|
5756
|
+
...this.messagesTraceAttrs(messages)
|
|
4733
5757
|
}
|
|
4734
5758
|
});
|
|
4735
|
-
span.addEvent("daemon.turn.started", { reason });
|
|
5759
|
+
span.addEvent("daemon.turn.started", { reason, ...this.messagesTraceAttrs(messages) });
|
|
4736
5760
|
ap.runtimeTraceSpan = span;
|
|
4737
5761
|
return span;
|
|
4738
5762
|
}
|
|
@@ -4844,6 +5868,48 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
|
|
|
4844
5868
|
this.broadcastActivity(agentId, "error", `Runtime stalled: no runtime events for ${staleForMinutes}m`);
|
|
4845
5869
|
return true;
|
|
4846
5870
|
}
|
|
5871
|
+
recoverStaleProcessForQueuedMessageIfNeeded(agentId, ap) {
|
|
5872
|
+
if (ap.inbox.length === 0) return false;
|
|
5873
|
+
if (ap.expectedTerminationReason === "stalled_recovery") {
|
|
5874
|
+
return true;
|
|
5875
|
+
}
|
|
5876
|
+
const directStdinRuntime = ap.driver.supportsStdinNotification && ap.driver.busyDeliveryMode === "direct";
|
|
5877
|
+
const canRestartDirectStdinProcess = directStdinRuntime && Boolean(ap.sessionId) && (ap.gatedSteering.outstandingToolUses === 0 || hasDirectStdinRecoveryEvidence(ap));
|
|
5878
|
+
const canRestartStalledProcess = !ap.driver.supportsStdinNotification || canRestartDirectStdinProcess;
|
|
5879
|
+
if (!canRestartStalledProcess) return false;
|
|
5880
|
+
const staleForMs = Date.now() - ap.lastRuntimeEventAt;
|
|
5881
|
+
if (staleForMs < RUNTIME_PROGRESS_STALE_MS && !ap.runtimeProgressStaleSince) return false;
|
|
5882
|
+
const staleForMinutes = Math.max(1, Math.floor(staleForMs / 6e4));
|
|
5883
|
+
ap.runtimeProgressStaleSince ??= Date.now();
|
|
5884
|
+
this.recordRuntimeTraceEvent(agentId, ap, "runtime.progress.stalled", {
|
|
5885
|
+
ageMs: staleForMs,
|
|
5886
|
+
staleForMinutes,
|
|
5887
|
+
lastActivity: ap.lastActivity,
|
|
5888
|
+
pendingMessages: ap.inbox.length,
|
|
5889
|
+
recovery: "terminate_for_queued_message"
|
|
5890
|
+
});
|
|
5891
|
+
this.endRuntimeTrace(ap, "error", {
|
|
5892
|
+
outcome: "runtime-stalled",
|
|
5893
|
+
ageMs: staleForMs,
|
|
5894
|
+
lastActivity: ap.lastActivity,
|
|
5895
|
+
pendingMessages: ap.inbox.length,
|
|
5896
|
+
recovery: "terminate_for_queued_message"
|
|
5897
|
+
});
|
|
5898
|
+
ap.expectedTerminationReason = "stalled_recovery";
|
|
5899
|
+
const runtimeLabel = ap.driver.id === "opencode" ? "OpenCode" : ap.driver.id;
|
|
5900
|
+
logger.warn(
|
|
5901
|
+
`[Agent ${agentId}] ${runtimeLabel} process stalled for ${staleForMinutes}m with ${ap.inbox.length} queued message(s); terminating for restart`
|
|
5902
|
+
);
|
|
5903
|
+
this.broadcastActivity(agentId, "working", `Restarting stalled ${runtimeLabel} runtime for queued message`);
|
|
5904
|
+
try {
|
|
5905
|
+
ap.process.kill("SIGTERM");
|
|
5906
|
+
} catch (err) {
|
|
5907
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
5908
|
+
logger.warn(`[Agent ${agentId}] Failed to terminate stalled ${runtimeLabel} process: ${reason}`);
|
|
5909
|
+
return false;
|
|
5910
|
+
}
|
|
5911
|
+
return true;
|
|
5912
|
+
}
|
|
4847
5913
|
/** Handle a single ParsedEvent from any runtime driver */
|
|
4848
5914
|
handleParsedEvent(agentId, event, driver) {
|
|
4849
5915
|
const ap = this.agents.get(agentId);
|
|
@@ -4967,7 +6033,11 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
|
|
|
4967
6033
|
}
|
|
4968
6034
|
} else {
|
|
4969
6035
|
ap.isIdle = true;
|
|
4970
|
-
|
|
6036
|
+
if (ap.lastRuntimeError) {
|
|
6037
|
+
this.broadcastActivity(agentId, "error", ap.lastRuntimeError);
|
|
6038
|
+
} else {
|
|
6039
|
+
this.broadcastActivity(agentId, "online", "Idle");
|
|
6040
|
+
}
|
|
4971
6041
|
}
|
|
4972
6042
|
this.endRuntimeTrace(ap, "ok", { outcome: "turn-completed" });
|
|
4973
6043
|
if (ap.driver.terminateProcessOnTurnEnd) {
|
|
@@ -5006,6 +6076,15 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
|
|
|
5006
6076
|
}
|
|
5007
6077
|
this.recordRuntimeTraceEvent(agentId, ap, "runtime.error", { message: event.message });
|
|
5008
6078
|
this.endRuntimeTrace(ap, "error", { outcome: "runtime-error", errorMessage: event.message });
|
|
6079
|
+
if (ap.driver.supportsStdinNotification && classifyTerminalFailure(ap)) {
|
|
6080
|
+
ap.isIdle = true;
|
|
6081
|
+
ap.pendingNotificationCount = 0;
|
|
6082
|
+
if (ap.notificationTimer) {
|
|
6083
|
+
clearTimeout(ap.notificationTimer);
|
|
6084
|
+
ap.notificationTimer = null;
|
|
6085
|
+
}
|
|
6086
|
+
logger.info(`[Agent ${agentId}] Marked ${ap.driver.id} wakeable after terminal runtime error`);
|
|
6087
|
+
}
|
|
5009
6088
|
}
|
|
5010
6089
|
this.broadcastActivity(agentId, "error", event.message, [
|
|
5011
6090
|
{ kind: "text", text: `Error: ${event.message}` }
|
|
@@ -5052,20 +6131,73 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
|
|
|
5052
6131
|
const encoded = ap.driver.encodeStdinMessage(notification, ap.sessionId, { mode: "busy" });
|
|
5053
6132
|
if (encoded) {
|
|
5054
6133
|
ap.process.stdin?.write(encoded + "\n");
|
|
6134
|
+
this.recordDaemonTrace("daemon.agent.stdin_notification", {
|
|
6135
|
+
agentId,
|
|
6136
|
+
runtime: ap.config.runtime,
|
|
6137
|
+
model: ap.config.model,
|
|
6138
|
+
launchId: ap.launchId || void 0,
|
|
6139
|
+
outcome: "written",
|
|
6140
|
+
mode: "busy",
|
|
6141
|
+
pending_notification_count: count,
|
|
6142
|
+
session_id_present: true
|
|
6143
|
+
});
|
|
6144
|
+
} else {
|
|
6145
|
+
this.recordDaemonTrace("daemon.agent.stdin_notification", {
|
|
6146
|
+
agentId,
|
|
6147
|
+
runtime: ap.config.runtime,
|
|
6148
|
+
model: ap.config.model,
|
|
6149
|
+
launchId: ap.launchId || void 0,
|
|
6150
|
+
outcome: "encode_failed",
|
|
6151
|
+
mode: "busy",
|
|
6152
|
+
pending_notification_count: count,
|
|
6153
|
+
session_id_present: true
|
|
6154
|
+
}, "error");
|
|
5055
6155
|
}
|
|
5056
6156
|
}
|
|
5057
6157
|
/** Deliver a message to an agent via stdin, formatting it the same way as the MCP bridge */
|
|
5058
6158
|
deliverMessagesViaStdin(agentId, ap, messages, mode) {
|
|
5059
6159
|
if (messages.length === 0) return true;
|
|
6160
|
+
const split = this.splitRuntimeProfileControlBatch(messages);
|
|
6161
|
+
if (split.deferredMessages.length > 0) {
|
|
6162
|
+
ap.inbox.unshift(...split.deferredMessages);
|
|
6163
|
+
ap.pendingNotificationCount += split.deferredMessages.length;
|
|
6164
|
+
messages = split.nextMessages;
|
|
6165
|
+
this.recordDaemonTrace("daemon.agent.runtime_profile.split_batch", {
|
|
6166
|
+
agentId,
|
|
6167
|
+
launchId: ap.launchId || void 0,
|
|
6168
|
+
runtime: ap.config.runtime,
|
|
6169
|
+
mode,
|
|
6170
|
+
delivered_control_messages_count: messages.length,
|
|
6171
|
+
deferred_messages_count: split.deferredMessages.length,
|
|
6172
|
+
inbox_count: ap.inbox.length,
|
|
6173
|
+
pending_notification_count: ap.pendingNotificationCount
|
|
6174
|
+
});
|
|
6175
|
+
}
|
|
6176
|
+
const traceAttrs = {
|
|
6177
|
+
agentId,
|
|
6178
|
+
launchId: ap.launchId || void 0,
|
|
6179
|
+
runtime: ap.config.runtime,
|
|
6180
|
+
model: ap.config.model,
|
|
6181
|
+
mode,
|
|
6182
|
+
messages_count: messages.length,
|
|
6183
|
+
session_id_present: Boolean(ap.sessionId),
|
|
6184
|
+
inbox_count: ap.inbox.length,
|
|
6185
|
+
pending_notification_count: ap.pendingNotificationCount,
|
|
6186
|
+
busy_delivery_mode: ap.driver.busyDeliveryMode,
|
|
6187
|
+
supports_stdin_notification: ap.driver.supportsStdinNotification,
|
|
6188
|
+
...this.messagesTraceAttrs(messages)
|
|
6189
|
+
};
|
|
5060
6190
|
const prompt = formatRuntimeProfileControlPrompt(messages) ?? (messages.length === 1 ? `New message received:
|
|
5061
6191
|
|
|
5062
6192
|
${formatIncomingMessage(messages[0], ap.driver)}
|
|
5063
6193
|
|
|
5064
|
-
Respond as appropriate. Complete all your work before stopping
|
|
6194
|
+
Respond as appropriate. Complete all your work before stopping.
|
|
6195
|
+
${RESPONSE_TARGET_HINT}` : `New messages received:
|
|
5065
6196
|
|
|
5066
6197
|
${messages.map((message) => formatIncomingMessage(message, ap.driver)).join("\n")}
|
|
5067
6198
|
|
|
5068
|
-
Respond as appropriate. Complete all your work before stopping
|
|
6199
|
+
Respond as appropriate. Complete all your work before stopping.
|
|
6200
|
+
${RESPONSE_TARGET_HINT}`);
|
|
5069
6201
|
const encoded = ap.driver.encodeStdinMessage(prompt, ap.sessionId, { mode });
|
|
5070
6202
|
if (!encoded) {
|
|
5071
6203
|
ap.inbox.unshift(...messages);
|
|
@@ -5075,14 +6207,27 @@ Respond as appropriate. Complete all your work before stopping.`);
|
|
|
5075
6207
|
logger.warn(
|
|
5076
6208
|
`[Agent ${agentId}] Failed to encode ${mode} stdin delivery; re-queued ${messages.length === 1 ? "message" : `${messages.length} messages`}`
|
|
5077
6209
|
);
|
|
6210
|
+
this.recordDaemonTrace("daemon.agent.stdin_delivery", {
|
|
6211
|
+
...traceAttrs,
|
|
6212
|
+
outcome: "encode_failed",
|
|
6213
|
+
requeued_messages_count: messages.length
|
|
6214
|
+
}, "error");
|
|
5078
6215
|
return false;
|
|
5079
6216
|
}
|
|
5080
6217
|
const senders = [...new Set(messages.map((message) => `@${message.sender_name}`))].join(", ");
|
|
5081
6218
|
logger.info(
|
|
5082
6219
|
`[Agent ${agentId}] Delivering ${mode} ${messages.length === 1 ? "message" : `${messages.length} messages`} via stdin from ${senders}`
|
|
5083
6220
|
);
|
|
6221
|
+
if (this.containsOrdinaryInboxMessage(messages)) {
|
|
6222
|
+
ap.lastRuntimeError = null;
|
|
6223
|
+
}
|
|
5084
6224
|
ap.process.stdin?.write(encoded + "\n");
|
|
5085
6225
|
this.ackInjectedRuntimeProfileMessages(agentId, messages, ap.launchId);
|
|
6226
|
+
this.recordDaemonTrace("daemon.agent.stdin_delivery", {
|
|
6227
|
+
...traceAttrs,
|
|
6228
|
+
outcome: "written",
|
|
6229
|
+
stdin_write_attempted: true
|
|
6230
|
+
});
|
|
5086
6231
|
return true;
|
|
5087
6232
|
}
|
|
5088
6233
|
/** List ONE level of a directory — directories returned without children (lazy-loaded on demand) */
|
|
@@ -5127,6 +6272,16 @@ var systemClock = {
|
|
|
5127
6272
|
clearTimeout: (timer) => clearTimeout(timer)
|
|
5128
6273
|
};
|
|
5129
6274
|
var INBOUND_WATCHDOG_MS = 7e4;
|
|
6275
|
+
function durationMsBucket(ms) {
|
|
6276
|
+
if (ms == null || !Number.isFinite(ms) || ms < 0) return "unknown";
|
|
6277
|
+
if (ms === 0) return "0";
|
|
6278
|
+
if (ms <= 1e3) return "1s";
|
|
6279
|
+
if (ms <= 1e4) return "1s-10s";
|
|
6280
|
+
if (ms <= 3e4) return "10s-30s";
|
|
6281
|
+
if (ms <= 6e4) return "30s-60s";
|
|
6282
|
+
if (ms <= 12e4) return "60s-120s";
|
|
6283
|
+
return "120s+";
|
|
6284
|
+
}
|
|
5130
6285
|
var DaemonConnection = class {
|
|
5131
6286
|
ws = null;
|
|
5132
6287
|
options;
|
|
@@ -5138,6 +6293,8 @@ var DaemonConnection = class {
|
|
|
5138
6293
|
shouldConnect = true;
|
|
5139
6294
|
reconnectAttempt = 0;
|
|
5140
6295
|
lastDroppedSendLogAt = 0;
|
|
6296
|
+
lastInboundAt = null;
|
|
6297
|
+
lastInboundMessageKind = null;
|
|
5141
6298
|
constructor(options) {
|
|
5142
6299
|
this.options = options;
|
|
5143
6300
|
this.clock = options.clock ?? systemClock;
|
|
@@ -5172,6 +6329,10 @@ var DaemonConnection = class {
|
|
|
5172
6329
|
this.lastDroppedSendLogAt = now;
|
|
5173
6330
|
logger.warn(`[Daemon] Dropping outbound message while disconnected: ${msg.type}`);
|
|
5174
6331
|
}
|
|
6332
|
+
this.trace("daemon.connection.outbound_dropped", {
|
|
6333
|
+
message_type: msg.type,
|
|
6334
|
+
ws_ready_state: this.ws?.readyState ?? null
|
|
6335
|
+
});
|
|
5175
6336
|
}
|
|
5176
6337
|
get connected() {
|
|
5177
6338
|
return this.ws?.readyState === WebSocket.OPEN;
|
|
@@ -5185,25 +6346,49 @@ var DaemonConnection = class {
|
|
|
5185
6346
|
if (wsOptions?.agent) {
|
|
5186
6347
|
logger.info("[Daemon] Using configured proxy for WebSocket connection");
|
|
5187
6348
|
}
|
|
6349
|
+
this.trace("daemon.connection.connecting", {
|
|
6350
|
+
reconnect_attempt: this.reconnectAttempt,
|
|
6351
|
+
server_url_present: Boolean(this.options.serverUrl),
|
|
6352
|
+
proxy_present: Boolean(wsOptions?.agent)
|
|
6353
|
+
});
|
|
5188
6354
|
const ws = this.options.wsFactory ? this.options.wsFactory(wsUrl, wsOptions) : new WebSocket(wsUrl, wsOptions);
|
|
5189
6355
|
this.ws = ws;
|
|
5190
6356
|
ws.on("open", () => {
|
|
5191
6357
|
if (this.ws !== ws) return;
|
|
5192
6358
|
if (!this.shouldConnect) return;
|
|
5193
6359
|
logger.info("[Daemon] Connected to server");
|
|
6360
|
+
const priorReconnectAttempt = this.reconnectAttempt;
|
|
5194
6361
|
this.reconnectAttempt = 0;
|
|
5195
6362
|
this.reconnectDelay = this.options.minReconnectDelayMs ?? 1e3;
|
|
6363
|
+
this.markInbound("websocket_open");
|
|
5196
6364
|
this.resetWatchdog();
|
|
6365
|
+
this.trace("daemon.connection.connected", {
|
|
6366
|
+
reconnect_attempt: priorReconnectAttempt,
|
|
6367
|
+
inbound_watchdog_ms: this.options.inboundWatchdogMs ?? INBOUND_WATCHDOG_MS
|
|
6368
|
+
});
|
|
5197
6369
|
this.options.onConnect();
|
|
5198
6370
|
});
|
|
5199
6371
|
ws.on("message", (data) => {
|
|
5200
6372
|
if (this.ws !== ws) return;
|
|
5201
|
-
|
|
6373
|
+
let messageKind = "unknown";
|
|
5202
6374
|
try {
|
|
5203
6375
|
const msg = JSON.parse(data.toString());
|
|
6376
|
+
messageKind = msg.type;
|
|
6377
|
+
this.markInbound(messageKind);
|
|
6378
|
+
this.resetWatchdog();
|
|
6379
|
+
this.trace("daemon.connection.inbound_received", {
|
|
6380
|
+
message_type: messageKind,
|
|
6381
|
+
last_inbound_age_ms_bucket: "0"
|
|
6382
|
+
});
|
|
5204
6383
|
this.options.onMessage(msg);
|
|
5205
6384
|
} catch (err) {
|
|
6385
|
+
this.markInbound("invalid_json");
|
|
6386
|
+
this.resetWatchdog();
|
|
5206
6387
|
logger.error("[Daemon] Invalid message from server", err);
|
|
6388
|
+
this.trace("daemon.connection.invalid_message", {
|
|
6389
|
+
error_class: err instanceof Error ? err.name : typeof err,
|
|
6390
|
+
last_inbound_message_kind: "invalid_json"
|
|
6391
|
+
}, "error");
|
|
5207
6392
|
}
|
|
5208
6393
|
});
|
|
5209
6394
|
ws.on("close", (code, reasonBuffer) => {
|
|
@@ -5214,12 +6399,23 @@ var DaemonConnection = class {
|
|
|
5214
6399
|
logger.warn(
|
|
5215
6400
|
`[Daemon] Disconnected from server (code=${code}, reason=${JSON.stringify(reason)}, reconnecting=${this.shouldConnect})`
|
|
5216
6401
|
);
|
|
6402
|
+
this.trace("daemon.connection.disconnected", {
|
|
6403
|
+
close_code: code,
|
|
6404
|
+
close_reason_present: Boolean(reason),
|
|
6405
|
+
reconnecting: this.shouldConnect,
|
|
6406
|
+
reconnect_attempt: this.reconnectAttempt,
|
|
6407
|
+
last_inbound_message_kind: this.lastInboundMessageKind,
|
|
6408
|
+
last_inbound_age_ms_bucket: this.lastInboundAgeBucket()
|
|
6409
|
+
}, this.shouldConnect ? "cancelled" : "ok");
|
|
5217
6410
|
this.options.onDisconnect();
|
|
5218
6411
|
this.scheduleReconnect();
|
|
5219
6412
|
});
|
|
5220
6413
|
ws.on("error", (err) => {
|
|
5221
6414
|
if (this.ws !== ws) return;
|
|
5222
6415
|
logger.error(`[Daemon] WebSocket error: ${err.message}`);
|
|
6416
|
+
this.trace("daemon.connection.error", {
|
|
6417
|
+
error_class: err.name || "Error"
|
|
6418
|
+
}, "error");
|
|
5223
6419
|
});
|
|
5224
6420
|
}
|
|
5225
6421
|
scheduleReconnect() {
|
|
@@ -5227,6 +6423,10 @@ var DaemonConnection = class {
|
|
|
5227
6423
|
if (this.reconnectTimer) return;
|
|
5228
6424
|
this.reconnectAttempt += 1;
|
|
5229
6425
|
logger.info(`[Daemon] Reconnecting to server in ${this.reconnectDelay}ms (attempt ${this.reconnectAttempt})`);
|
|
6426
|
+
this.trace("daemon.connection.reconnect_scheduled", {
|
|
6427
|
+
reconnect_attempt: this.reconnectAttempt,
|
|
6428
|
+
delay_ms: this.reconnectDelay
|
|
6429
|
+
});
|
|
5230
6430
|
this.reconnectTimer = this.clock.setTimeout(() => {
|
|
5231
6431
|
this.reconnectTimer = null;
|
|
5232
6432
|
this.doConnect();
|
|
@@ -5238,6 +6438,13 @@ var DaemonConnection = class {
|
|
|
5238
6438
|
const ms = this.options.inboundWatchdogMs ?? INBOUND_WATCHDOG_MS;
|
|
5239
6439
|
this.watchdogTimer = this.clock.setTimeout(() => {
|
|
5240
6440
|
logger.warn(`[Daemon] No inbound traffic for ${ms / 1e3}s \u2014 forcing reconnect`);
|
|
6441
|
+
this.trace("daemon.connection.watchdog_timeout", {
|
|
6442
|
+
inbound_watchdog_ms: ms,
|
|
6443
|
+
last_inbound_message_kind: this.lastInboundMessageKind,
|
|
6444
|
+
last_inbound_age_ms_bucket: this.lastInboundAgeBucket(),
|
|
6445
|
+
ws_ready_state: this.ws?.readyState ?? null,
|
|
6446
|
+
reconnecting: this.shouldConnect
|
|
6447
|
+
}, "error");
|
|
5241
6448
|
try {
|
|
5242
6449
|
this.ws?.terminate();
|
|
5243
6450
|
} catch {
|
|
@@ -5250,6 +6457,16 @@ var DaemonConnection = class {
|
|
|
5250
6457
|
this.watchdogTimer = null;
|
|
5251
6458
|
}
|
|
5252
6459
|
}
|
|
6460
|
+
markInbound(messageKind) {
|
|
6461
|
+
this.lastInboundAt = this.clock.now();
|
|
6462
|
+
this.lastInboundMessageKind = messageKind;
|
|
6463
|
+
}
|
|
6464
|
+
lastInboundAgeBucket() {
|
|
6465
|
+
return durationMsBucket(this.lastInboundAt == null ? null : this.clock.now() - this.lastInboundAt);
|
|
6466
|
+
}
|
|
6467
|
+
trace(name, attrs, status = "ok") {
|
|
6468
|
+
this.options.onTraceEvent?.(name, attrs, status);
|
|
6469
|
+
}
|
|
5253
6470
|
};
|
|
5254
6471
|
|
|
5255
6472
|
// src/reminderCache.ts
|
|
@@ -5333,10 +6550,10 @@ var ReminderCache = class {
|
|
|
5333
6550
|
|
|
5334
6551
|
// src/machineLock.ts
|
|
5335
6552
|
import { createHash, randomUUID as randomUUID2 } from "crypto";
|
|
5336
|
-
import { mkdirSync as mkdirSync5, readFileSync as
|
|
5337
|
-
import
|
|
6553
|
+
import { mkdirSync as mkdirSync5, readFileSync as readFileSync5, rmSync as rmSync2, statSync as statSync3, writeFileSync as writeFileSync8 } from "fs";
|
|
6554
|
+
import os6 from "os";
|
|
5338
6555
|
import path12 from "path";
|
|
5339
|
-
var DEFAULT_MACHINE_STATE_ROOT = path12.join(
|
|
6556
|
+
var DEFAULT_MACHINE_STATE_ROOT = path12.join(os6.homedir(), ".slock", "machines");
|
|
5340
6557
|
var INCOMPLETE_LOCK_STALE_MS = 3e4;
|
|
5341
6558
|
var DaemonMachineLockConflictError = class extends Error {
|
|
5342
6559
|
code = "DAEMON_MACHINE_LOCK_HELD";
|
|
@@ -5359,14 +6576,14 @@ function ownerPath(lockDir) {
|
|
|
5359
6576
|
}
|
|
5360
6577
|
function readOwner(lockDir) {
|
|
5361
6578
|
try {
|
|
5362
|
-
return JSON.parse(
|
|
6579
|
+
return JSON.parse(readFileSync5(ownerPath(lockDir), "utf8"));
|
|
5363
6580
|
} catch {
|
|
5364
6581
|
return null;
|
|
5365
6582
|
}
|
|
5366
6583
|
}
|
|
5367
6584
|
function lockAgeMs(lockDir) {
|
|
5368
6585
|
try {
|
|
5369
|
-
return Date.now() -
|
|
6586
|
+
return Date.now() - statSync3(lockDir).mtimeMs;
|
|
5370
6587
|
} catch {
|
|
5371
6588
|
return null;
|
|
5372
6589
|
}
|
|
@@ -5395,7 +6612,7 @@ function acquireDaemonMachineLock(options) {
|
|
|
5395
6612
|
const owner = {
|
|
5396
6613
|
pid: process.pid,
|
|
5397
6614
|
token,
|
|
5398
|
-
hostname:
|
|
6615
|
+
hostname: os6.hostname(),
|
|
5399
6616
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5400
6617
|
serverUrl: options.serverUrl,
|
|
5401
6618
|
apiKeyFingerprint: fingerprint.slice(0, 16)
|
|
@@ -5437,6 +6654,418 @@ function acquireDaemonMachineLock(options) {
|
|
|
5437
6654
|
throw new DaemonMachineLockConflictError(lockDir, readOwner(lockDir));
|
|
5438
6655
|
}
|
|
5439
6656
|
|
|
6657
|
+
// src/localTraceSink.ts
|
|
6658
|
+
import { appendFileSync, mkdirSync as mkdirSync6, readdirSync as readdirSync3, rmSync as rmSync3, statSync as statSync4, writeFileSync as writeFileSync9 } from "fs";
|
|
6659
|
+
import path13 from "path";
|
|
6660
|
+
var DEFAULT_MAX_FILE_BYTES = 5 * 1024 * 1024;
|
|
6661
|
+
var DEFAULT_MAX_FILES = 8;
|
|
6662
|
+
var DIAGNOSTIC_ID_ATTRS = /* @__PURE__ */ new Set([
|
|
6663
|
+
"serverId",
|
|
6664
|
+
"machineId",
|
|
6665
|
+
"agentId",
|
|
6666
|
+
"messageId",
|
|
6667
|
+
"launchId",
|
|
6668
|
+
"uploadId",
|
|
6669
|
+
"bundleId",
|
|
6670
|
+
"deliveryId",
|
|
6671
|
+
"deliveryCorrelationId",
|
|
6672
|
+
"delivery_correlation_id"
|
|
6673
|
+
]);
|
|
6674
|
+
var LocalRotatingTraceSink = class {
|
|
6675
|
+
traceDir;
|
|
6676
|
+
maxFileBytes;
|
|
6677
|
+
maxFiles;
|
|
6678
|
+
currentFile = null;
|
|
6679
|
+
currentSize = 0;
|
|
6680
|
+
sequence = 0;
|
|
6681
|
+
constructor(options) {
|
|
6682
|
+
this.traceDir = path13.join(options.machineDir, "traces");
|
|
6683
|
+
this.maxFileBytes = Math.max(1024, Math.floor(options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES));
|
|
6684
|
+
this.maxFiles = Math.max(1, Math.floor(options.maxFiles ?? DEFAULT_MAX_FILES));
|
|
6685
|
+
}
|
|
6686
|
+
record(span) {
|
|
6687
|
+
try {
|
|
6688
|
+
const line = `${JSON.stringify(toLocalTraceRecord(span))}
|
|
6689
|
+
`;
|
|
6690
|
+
this.ensureFile(Buffer.byteLength(line));
|
|
6691
|
+
appendFileSync(this.currentFile, line, { encoding: "utf8" });
|
|
6692
|
+
this.currentSize += Buffer.byteLength(line);
|
|
6693
|
+
} catch {
|
|
6694
|
+
}
|
|
6695
|
+
}
|
|
6696
|
+
getCurrentFile() {
|
|
6697
|
+
return this.currentFile;
|
|
6698
|
+
}
|
|
6699
|
+
ensureFile(nextBytes) {
|
|
6700
|
+
mkdirSync6(this.traceDir, { recursive: true, mode: 448 });
|
|
6701
|
+
if (!this.currentFile || this.currentSize + nextBytes > this.maxFileBytes) {
|
|
6702
|
+
this.currentFile = path13.join(
|
|
6703
|
+
this.traceDir,
|
|
6704
|
+
`daemon-trace-${safeTimestamp(Date.now())}-${process.pid}-${String(this.sequence++).padStart(4, "0")}.jsonl`
|
|
6705
|
+
);
|
|
6706
|
+
writeFileSync9(this.currentFile, "", { flag: "a", mode: 384 });
|
|
6707
|
+
this.currentSize = statSync4(this.currentFile).size;
|
|
6708
|
+
this.pruneOldFiles();
|
|
6709
|
+
}
|
|
6710
|
+
}
|
|
6711
|
+
pruneOldFiles() {
|
|
6712
|
+
const files = readdirSync3(this.traceDir).filter((name) => name.startsWith("daemon-trace-") && name.endsWith(".jsonl")).sort();
|
|
6713
|
+
const excess = files.length - this.maxFiles;
|
|
6714
|
+
if (excess <= 0) return;
|
|
6715
|
+
for (const file of files.slice(0, excess)) {
|
|
6716
|
+
rmSync3(path13.join(this.traceDir, file), { force: true });
|
|
6717
|
+
}
|
|
6718
|
+
}
|
|
6719
|
+
};
|
|
6720
|
+
function safeTimestamp(timeMs) {
|
|
6721
|
+
return new Date(timeMs).toISOString().replace(/[:.]/g, "-");
|
|
6722
|
+
}
|
|
6723
|
+
function toLocalTraceRecord(span) {
|
|
6724
|
+
return {
|
|
6725
|
+
type: "span",
|
|
6726
|
+
schema_version: 1,
|
|
6727
|
+
trace_id: span.context.traceId,
|
|
6728
|
+
span_id: span.context.spanId,
|
|
6729
|
+
parent_span_id: span.context.parentSpanId,
|
|
6730
|
+
name: span.name,
|
|
6731
|
+
surface: span.surface,
|
|
6732
|
+
kind: span.kind,
|
|
6733
|
+
status: span.status,
|
|
6734
|
+
start_time: new Date(span.startTimeMs).toISOString(),
|
|
6735
|
+
end_time: new Date(span.endTimeMs).toISOString(),
|
|
6736
|
+
duration_ms: span.durationMs,
|
|
6737
|
+
attrs: sanitizeAttrs(span.attrs),
|
|
6738
|
+
events: span.events.map(sanitizeEvent)
|
|
6739
|
+
};
|
|
6740
|
+
}
|
|
6741
|
+
function sanitizeEvent(event) {
|
|
6742
|
+
return {
|
|
6743
|
+
name: event.name,
|
|
6744
|
+
time: new Date(event.timeMs).toISOString(),
|
|
6745
|
+
attrs: sanitizeAttrs(event.attrs)
|
|
6746
|
+
};
|
|
6747
|
+
}
|
|
6748
|
+
function sanitizeAttrs(attrs) {
|
|
6749
|
+
if (!attrs) return void 0;
|
|
6750
|
+
const sanitized = {};
|
|
6751
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
6752
|
+
if (isDiagnosticIdAttr(key)) {
|
|
6753
|
+
if (value === null || value === void 0 || value === "") continue;
|
|
6754
|
+
sanitized[key] = sanitizeValue(value);
|
|
6755
|
+
continue;
|
|
6756
|
+
}
|
|
6757
|
+
if (shouldDropAttr(key)) continue;
|
|
6758
|
+
sanitized[key] = sanitizeValue(value);
|
|
6759
|
+
}
|
|
6760
|
+
return sanitized;
|
|
6761
|
+
}
|
|
6762
|
+
function sanitizeValue(value) {
|
|
6763
|
+
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
6764
|
+
return value;
|
|
6765
|
+
}
|
|
6766
|
+
if (Array.isArray(value)) {
|
|
6767
|
+
return { items_count: value.length };
|
|
6768
|
+
}
|
|
6769
|
+
if (typeof value === "object") {
|
|
6770
|
+
return { object_present: true };
|
|
6771
|
+
}
|
|
6772
|
+
return String(value);
|
|
6773
|
+
}
|
|
6774
|
+
function shouldDropAttr(key) {
|
|
6775
|
+
const normalized = key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
|
|
6776
|
+
if (/(^|_)(api_key|auth_token|token|secret|password|cookie|credential)(_|$)/i.test(normalized)) {
|
|
6777
|
+
return true;
|
|
6778
|
+
}
|
|
6779
|
+
if (/(^|_)(count|present|kind|mode|source|outcome|reason|class|status|bucket|ms|code|truncated)$/.test(normalized)) {
|
|
6780
|
+
return false;
|
|
6781
|
+
}
|
|
6782
|
+
if (/(^|_)id$/.test(normalized)) {
|
|
6783
|
+
return true;
|
|
6784
|
+
}
|
|
6785
|
+
return /(^|_)(prompt|content|text|message|body|request|response|command|argv|env|cwd|path|file|error|tool_args|tool_input|tool_output|stdout|stderr)(_|$)/i.test(normalized);
|
|
6786
|
+
}
|
|
6787
|
+
function isDiagnosticIdAttr(key) {
|
|
6788
|
+
return DIAGNOSTIC_ID_ATTRS.has(key);
|
|
6789
|
+
}
|
|
6790
|
+
|
|
6791
|
+
// src/traceBundleUpload.ts
|
|
6792
|
+
import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
|
|
6793
|
+
import { gzipSync } from "zlib";
|
|
6794
|
+
import { mkdir as mkdir2, readFile as readFile2, readdir as readdir3, stat as stat3, writeFile as writeFile2 } from "fs/promises";
|
|
6795
|
+
import path14 from "path";
|
|
6796
|
+
|
|
6797
|
+
// src/directUploadCapability.ts
|
|
6798
|
+
function joinUrl(base, path16) {
|
|
6799
|
+
return `${base.replace(/\/+$/, "")}${path16}`;
|
|
6800
|
+
}
|
|
6801
|
+
function jsonHeaders(apiKey) {
|
|
6802
|
+
return {
|
|
6803
|
+
"Content-Type": "application/json",
|
|
6804
|
+
...apiKey ? { Authorization: `Bearer ${apiKey}` } : {}
|
|
6805
|
+
};
|
|
6806
|
+
}
|
|
6807
|
+
async function requestDaemonScopeAttestation({
|
|
6808
|
+
serverUrl,
|
|
6809
|
+
apiKey,
|
|
6810
|
+
scope,
|
|
6811
|
+
metadata,
|
|
6812
|
+
fetchImpl = fetch,
|
|
6813
|
+
timeoutMs = DEFAULT_CHAT_BRIDGE_TOOL_TIMEOUT_MS
|
|
6814
|
+
}) {
|
|
6815
|
+
const { response, data } = await executeJsonRequest(
|
|
6816
|
+
joinUrl(serverUrl, "/internal/machine/scope-attestation"),
|
|
6817
|
+
{
|
|
6818
|
+
method: "POST",
|
|
6819
|
+
headers: jsonHeaders(apiKey),
|
|
6820
|
+
body: JSON.stringify({
|
|
6821
|
+
scope,
|
|
6822
|
+
...metadata ? { metadata } : {}
|
|
6823
|
+
})
|
|
6824
|
+
},
|
|
6825
|
+
{
|
|
6826
|
+
toolName: "daemon_direct_upload.scope_attestation",
|
|
6827
|
+
target: scope,
|
|
6828
|
+
timeoutMs,
|
|
6829
|
+
fetchImpl
|
|
6830
|
+
}
|
|
6831
|
+
);
|
|
6832
|
+
if (!response.ok) {
|
|
6833
|
+
throw new Error(`Failed to request daemon scope attestation (${response.status})`);
|
|
6834
|
+
}
|
|
6835
|
+
return data;
|
|
6836
|
+
}
|
|
6837
|
+
async function createDirectUploadSession({
|
|
6838
|
+
serverUrl,
|
|
6839
|
+
apiKey,
|
|
6840
|
+
workerUrl,
|
|
6841
|
+
scope,
|
|
6842
|
+
createPath = "/api/uploads",
|
|
6843
|
+
body,
|
|
6844
|
+
attestationMetadata,
|
|
6845
|
+
fetchImpl = fetch,
|
|
6846
|
+
timeoutMs = DEFAULT_CHAT_BRIDGE_TOOL_TIMEOUT_MS
|
|
6847
|
+
}) {
|
|
6848
|
+
const capability = await requestDaemonScopeAttestation({
|
|
6849
|
+
serverUrl,
|
|
6850
|
+
apiKey,
|
|
6851
|
+
scope,
|
|
6852
|
+
metadata: attestationMetadata,
|
|
6853
|
+
fetchImpl,
|
|
6854
|
+
timeoutMs
|
|
6855
|
+
});
|
|
6856
|
+
const { response, data } = await executeJsonRequest(
|
|
6857
|
+
joinUrl(workerUrl, createPath),
|
|
6858
|
+
{
|
|
6859
|
+
method: "POST",
|
|
6860
|
+
headers: jsonHeaders(),
|
|
6861
|
+
body: JSON.stringify({
|
|
6862
|
+
...body,
|
|
6863
|
+
attestation: capability.attestation
|
|
6864
|
+
})
|
|
6865
|
+
},
|
|
6866
|
+
{
|
|
6867
|
+
toolName: "daemon_direct_upload.create",
|
|
6868
|
+
target: capability.audience,
|
|
6869
|
+
timeoutMs,
|
|
6870
|
+
fetchImpl
|
|
6871
|
+
}
|
|
6872
|
+
);
|
|
6873
|
+
if (!response.ok) {
|
|
6874
|
+
throw new Error(`Failed to create direct upload session (${response.status})`);
|
|
6875
|
+
}
|
|
6876
|
+
return { capability, response: data };
|
|
6877
|
+
}
|
|
6878
|
+
async function uploadWithSignedCapability({
|
|
6879
|
+
serverUrl,
|
|
6880
|
+
apiKey,
|
|
6881
|
+
workerUrl,
|
|
6882
|
+
scope,
|
|
6883
|
+
createPath = "/api/uploads",
|
|
6884
|
+
createBody,
|
|
6885
|
+
attestationMetadata,
|
|
6886
|
+
uploadBody,
|
|
6887
|
+
fetchImpl = fetch,
|
|
6888
|
+
timeoutMs = DEFAULT_CHAT_BRIDGE_TOOL_TIMEOUT_MS
|
|
6889
|
+
}) {
|
|
6890
|
+
const { capability, response: session } = await createDirectUploadSession({
|
|
6891
|
+
serverUrl,
|
|
6892
|
+
apiKey,
|
|
6893
|
+
workerUrl,
|
|
6894
|
+
scope,
|
|
6895
|
+
createPath,
|
|
6896
|
+
body: createBody,
|
|
6897
|
+
attestationMetadata,
|
|
6898
|
+
fetchImpl,
|
|
6899
|
+
timeoutMs
|
|
6900
|
+
});
|
|
6901
|
+
const { response: uploadResponse } = await executeResponseRequest(
|
|
6902
|
+
session.upload.url,
|
|
6903
|
+
{
|
|
6904
|
+
method: session.upload.method,
|
|
6905
|
+
headers: session.upload.headers ?? {},
|
|
6906
|
+
body: uploadBody
|
|
6907
|
+
},
|
|
6908
|
+
{
|
|
6909
|
+
toolName: "daemon_direct_upload.put",
|
|
6910
|
+
target: capability.audience,
|
|
6911
|
+
timeoutMs,
|
|
6912
|
+
fetchImpl
|
|
6913
|
+
}
|
|
6914
|
+
);
|
|
6915
|
+
if (!uploadResponse.ok) {
|
|
6916
|
+
throw new Error(`Failed to upload with signed capability (${uploadResponse.status})`);
|
|
6917
|
+
}
|
|
6918
|
+
return { capability, session, uploadResponse };
|
|
6919
|
+
}
|
|
6920
|
+
|
|
6921
|
+
// src/traceBundleUpload.ts
|
|
6922
|
+
var TRACE_UPLOAD_SCOPE = "daemon-trace-bundle:create";
|
|
6923
|
+
var DEFAULT_UPLOAD_INTERVAL_MS = 5 * 60 * 1e3;
|
|
6924
|
+
var DEFAULT_MIN_FILE_AGE_MS = 60 * 1e3;
|
|
6925
|
+
var DEFAULT_MAX_FILES_PER_RUN = 4;
|
|
6926
|
+
var DaemonTraceBundleUploader = class {
|
|
6927
|
+
options;
|
|
6928
|
+
timer = null;
|
|
6929
|
+
constructor(options) {
|
|
6930
|
+
this.options = options;
|
|
6931
|
+
}
|
|
6932
|
+
start() {
|
|
6933
|
+
if (this.timer) return;
|
|
6934
|
+
void this.uploadOnce();
|
|
6935
|
+
this.timer = setInterval(() => {
|
|
6936
|
+
void this.uploadOnce();
|
|
6937
|
+
}, this.options.intervalMs ?? readPositiveIntegerEnv2("SLOCK_DAEMON_TRACE_UPLOAD_INTERVAL_MS", DEFAULT_UPLOAD_INTERVAL_MS));
|
|
6938
|
+
}
|
|
6939
|
+
stop() {
|
|
6940
|
+
if (!this.timer) return;
|
|
6941
|
+
clearInterval(this.timer);
|
|
6942
|
+
this.timer = null;
|
|
6943
|
+
}
|
|
6944
|
+
async uploadOnce() {
|
|
6945
|
+
const files = await this.findUploadCandidates();
|
|
6946
|
+
let uploaded = 0;
|
|
6947
|
+
for (const file of files.slice(0, this.options.maxFilesPerRun ?? DEFAULT_MAX_FILES_PER_RUN)) {
|
|
6948
|
+
if (await this.uploadFile(file)) uploaded += 1;
|
|
6949
|
+
}
|
|
6950
|
+
return { attempted: files.length, uploaded };
|
|
6951
|
+
}
|
|
6952
|
+
async findUploadCandidates() {
|
|
6953
|
+
const traceDir = path14.join(this.options.machineDir, "traces");
|
|
6954
|
+
let names;
|
|
6955
|
+
try {
|
|
6956
|
+
names = await readdir3(traceDir);
|
|
6957
|
+
} catch {
|
|
6958
|
+
return [];
|
|
6959
|
+
}
|
|
6960
|
+
const now = Date.now();
|
|
6961
|
+
const minAgeMs = this.options.minFileAgeMs ?? readPositiveIntegerEnv2("SLOCK_DAEMON_TRACE_UPLOAD_MIN_FILE_AGE_MS", DEFAULT_MIN_FILE_AGE_MS);
|
|
6962
|
+
const currentFile = this.options.currentFileProvider?.();
|
|
6963
|
+
const candidates = [];
|
|
6964
|
+
for (const name of names.filter((entry) => entry.startsWith("daemon-trace-") && entry.endsWith(".jsonl")).sort()) {
|
|
6965
|
+
const file = path14.join(traceDir, name);
|
|
6966
|
+
if (currentFile && path14.resolve(file) === path14.resolve(currentFile)) continue;
|
|
6967
|
+
if (await this.isUploaded(file)) continue;
|
|
6968
|
+
try {
|
|
6969
|
+
const info = await stat3(file);
|
|
6970
|
+
if (!info.isFile() || info.size <= 0) continue;
|
|
6971
|
+
if (now - info.mtimeMs < minAgeMs) continue;
|
|
6972
|
+
candidates.push(file);
|
|
6973
|
+
} catch {
|
|
6974
|
+
}
|
|
6975
|
+
}
|
|
6976
|
+
return candidates;
|
|
6977
|
+
}
|
|
6978
|
+
async uploadFile(file) {
|
|
6979
|
+
const span = this.options.tracer?.startSpan("daemon.bundle.upload", {
|
|
6980
|
+
surface: "daemon",
|
|
6981
|
+
kind: "producer",
|
|
6982
|
+
attrs: {
|
|
6983
|
+
file_present: true,
|
|
6984
|
+
worker_url_present: Boolean(this.options.workerUrl)
|
|
6985
|
+
}
|
|
6986
|
+
});
|
|
6987
|
+
try {
|
|
6988
|
+
const raw = await readFile2(file);
|
|
6989
|
+
if (raw.byteLength === 0) {
|
|
6990
|
+
span?.end("cancelled", { attrs: { outcome: "empty" } });
|
|
6991
|
+
return false;
|
|
6992
|
+
}
|
|
6993
|
+
const gzipped = gzipSync(raw);
|
|
6994
|
+
const bundleSha256 = sha256Hex(gzipped);
|
|
6995
|
+
const bundleId = randomUUID3();
|
|
6996
|
+
await uploadWithSignedCapability({
|
|
6997
|
+
serverUrl: this.options.serverUrl,
|
|
6998
|
+
apiKey: this.options.apiKey,
|
|
6999
|
+
workerUrl: this.options.workerUrl,
|
|
7000
|
+
scope: TRACE_UPLOAD_SCOPE,
|
|
7001
|
+
createPath: "/api/trace-bundles",
|
|
7002
|
+
attestationMetadata: {
|
|
7003
|
+
bundleId,
|
|
7004
|
+
bundleSha256,
|
|
7005
|
+
bundleSizeBytes: gzipped.byteLength
|
|
7006
|
+
},
|
|
7007
|
+
createBody: {
|
|
7008
|
+
bundleSha256,
|
|
7009
|
+
bundleSizeBytes: gzipped.byteLength
|
|
7010
|
+
},
|
|
7011
|
+
uploadBody: new Blob([new Uint8Array(gzipped)], { type: "application/x-ndjson" }),
|
|
7012
|
+
fetchImpl: this.options.fetchImpl
|
|
7013
|
+
});
|
|
7014
|
+
await this.markUploaded(file, {
|
|
7015
|
+
bundleId,
|
|
7016
|
+
bundleSha256,
|
|
7017
|
+
bundleSizeBytes: gzipped.byteLength
|
|
7018
|
+
});
|
|
7019
|
+
span?.end("ok", {
|
|
7020
|
+
attrs: {
|
|
7021
|
+
bundleId,
|
|
7022
|
+
bundle_size_bytes: gzipped.byteLength
|
|
7023
|
+
}
|
|
7024
|
+
});
|
|
7025
|
+
return true;
|
|
7026
|
+
} catch (err) {
|
|
7027
|
+
span?.end("error", {
|
|
7028
|
+
attrs: {
|
|
7029
|
+
error_class: err instanceof Error ? err.name : "Error",
|
|
7030
|
+
error_message_present: err instanceof Error && Boolean(err.message)
|
|
7031
|
+
}
|
|
7032
|
+
});
|
|
7033
|
+
return false;
|
|
7034
|
+
}
|
|
7035
|
+
}
|
|
7036
|
+
uploadStatePath(file) {
|
|
7037
|
+
const stateDir = path14.join(this.options.machineDir, "trace-uploads");
|
|
7038
|
+
return path14.join(stateDir, `${path14.basename(file)}.uploaded.json`);
|
|
7039
|
+
}
|
|
7040
|
+
async isUploaded(file) {
|
|
7041
|
+
try {
|
|
7042
|
+
await stat3(this.uploadStatePath(file));
|
|
7043
|
+
return true;
|
|
7044
|
+
} catch {
|
|
7045
|
+
return false;
|
|
7046
|
+
}
|
|
7047
|
+
}
|
|
7048
|
+
async markUploaded(file, metadata) {
|
|
7049
|
+
const stateFile = this.uploadStatePath(file);
|
|
7050
|
+
await mkdir2(path14.dirname(stateFile), { recursive: true, mode: 448 });
|
|
7051
|
+
await writeFile2(stateFile, `${JSON.stringify({
|
|
7052
|
+
file: path14.basename(file),
|
|
7053
|
+
uploadedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7054
|
+
...metadata
|
|
7055
|
+
}, null, 2)}
|
|
7056
|
+
`, { mode: 384 });
|
|
7057
|
+
}
|
|
7058
|
+
};
|
|
7059
|
+
function sha256Hex(body) {
|
|
7060
|
+
return createHash2("sha256").update(body).digest("hex");
|
|
7061
|
+
}
|
|
7062
|
+
function readPositiveIntegerEnv2(name, fallback) {
|
|
7063
|
+
const value = process.env[name];
|
|
7064
|
+
if (!value) return fallback;
|
|
7065
|
+
const parsed = Number(value);
|
|
7066
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
7067
|
+
}
|
|
7068
|
+
|
|
5440
7069
|
// src/core.ts
|
|
5441
7070
|
var DAEMON_CLI_USAGE = "Usage: slock-daemon --server-url <url> --api-key <key>";
|
|
5442
7071
|
function parseDaemonCliArgs(args) {
|
|
@@ -5458,59 +7087,110 @@ function readDaemonVersion(moduleUrl = import.meta.url) {
|
|
|
5458
7087
|
}
|
|
5459
7088
|
}
|
|
5460
7089
|
function resolveChatBridgePath(moduleUrl = import.meta.url) {
|
|
5461
|
-
const dirname =
|
|
5462
|
-
const jsPath =
|
|
7090
|
+
const dirname = path15.dirname(fileURLToPath(moduleUrl));
|
|
7091
|
+
const jsPath = path15.resolve(dirname, "chat-bridge.js");
|
|
5463
7092
|
try {
|
|
5464
7093
|
accessSync(jsPath);
|
|
5465
7094
|
return jsPath;
|
|
5466
7095
|
} catch {
|
|
5467
|
-
return
|
|
7096
|
+
return path15.resolve(dirname, "chat-bridge.ts");
|
|
5468
7097
|
}
|
|
5469
7098
|
}
|
|
5470
7099
|
function resolveSlockCliPath(moduleUrl = import.meta.url) {
|
|
5471
|
-
const thisDir =
|
|
5472
|
-
const bundledDistPath =
|
|
7100
|
+
const thisDir = path15.dirname(fileURLToPath(moduleUrl));
|
|
7101
|
+
const bundledDistPath = path15.resolve(thisDir, "cli", "index.js");
|
|
5473
7102
|
try {
|
|
5474
7103
|
accessSync(bundledDistPath);
|
|
5475
7104
|
return bundledDistPath;
|
|
5476
7105
|
} catch {
|
|
5477
|
-
const workspaceDistPath =
|
|
7106
|
+
const workspaceDistPath = path15.resolve(thisDir, "..", "..", "cli", "dist", "index.js");
|
|
5478
7107
|
accessSync(workspaceDistPath);
|
|
5479
7108
|
return workspaceDistPath;
|
|
5480
7109
|
}
|
|
5481
7110
|
}
|
|
5482
|
-
function detectRuntimes() {
|
|
7111
|
+
function detectRuntimes(tracer = noopTracer) {
|
|
5483
7112
|
const ids = [];
|
|
5484
7113
|
const versions = {};
|
|
7114
|
+
const span = tracer.startSpan("daemon.runtime.detect", {
|
|
7115
|
+
surface: "daemon",
|
|
7116
|
+
kind: "internal",
|
|
7117
|
+
attrs: {
|
|
7118
|
+
known_runtime_count: RUNTIMES.length
|
|
7119
|
+
}
|
|
7120
|
+
});
|
|
5485
7121
|
for (const runtime of RUNTIMES) {
|
|
5486
7122
|
const driver = getDriver(runtime.id);
|
|
7123
|
+
let probeErrorPresent = false;
|
|
5487
7124
|
try {
|
|
5488
7125
|
if (driver.probe) {
|
|
5489
7126
|
const probe = driver.probe();
|
|
5490
7127
|
if (!probe.available) {
|
|
5491
7128
|
if (probe.version) versions[runtime.id] = probe.version;
|
|
7129
|
+
span.addEvent("daemon.runtime.detect.checked", {
|
|
7130
|
+
runtime: runtime.id,
|
|
7131
|
+
outcome: "unavailable",
|
|
7132
|
+
version_present: Boolean(probe.version),
|
|
7133
|
+
binary_path_present: false
|
|
7134
|
+
});
|
|
5492
7135
|
continue;
|
|
5493
7136
|
}
|
|
5494
7137
|
ids.push(runtime.id);
|
|
5495
7138
|
if (probe.version) versions[runtime.id] = probe.version;
|
|
7139
|
+
span.addEvent("daemon.runtime.detect.checked", {
|
|
7140
|
+
runtime: runtime.id,
|
|
7141
|
+
outcome: "available",
|
|
7142
|
+
version_present: Boolean(probe.version),
|
|
7143
|
+
binary_path_present: false
|
|
7144
|
+
});
|
|
5496
7145
|
continue;
|
|
5497
7146
|
}
|
|
5498
7147
|
} catch {
|
|
7148
|
+
probeErrorPresent = true;
|
|
5499
7149
|
}
|
|
5500
7150
|
const detectionBinaries = [runtime.binary];
|
|
7151
|
+
let detectedByPath = false;
|
|
5501
7152
|
for (const binary of detectionBinaries) {
|
|
5502
7153
|
const resolved = resolveCommandOnPath(binary);
|
|
5503
7154
|
if (!resolved) continue;
|
|
5504
7155
|
ids.push(runtime.id);
|
|
7156
|
+
detectedByPath = true;
|
|
5505
7157
|
const version = readCommandVersion(binary);
|
|
5506
7158
|
if (version) {
|
|
5507
7159
|
versions[runtime.id] = version;
|
|
5508
7160
|
}
|
|
7161
|
+
span.addEvent("daemon.runtime.detect.checked", {
|
|
7162
|
+
runtime: runtime.id,
|
|
7163
|
+
outcome: "available",
|
|
7164
|
+
version_present: Boolean(version),
|
|
7165
|
+
binary_path_present: true,
|
|
7166
|
+
probe_error_present: probeErrorPresent
|
|
7167
|
+
});
|
|
5509
7168
|
break;
|
|
5510
7169
|
}
|
|
7170
|
+
if (!detectedByPath) {
|
|
7171
|
+
span.addEvent("daemon.runtime.detect.checked", {
|
|
7172
|
+
runtime: runtime.id,
|
|
7173
|
+
outcome: "unavailable",
|
|
7174
|
+
version_present: false,
|
|
7175
|
+
binary_path_present: false,
|
|
7176
|
+
probe_error_present: probeErrorPresent
|
|
7177
|
+
});
|
|
7178
|
+
}
|
|
5511
7179
|
}
|
|
7180
|
+
span.end("ok", {
|
|
7181
|
+
attrs: {
|
|
7182
|
+
detected_runtime_count: ids.length
|
|
7183
|
+
}
|
|
7184
|
+
});
|
|
5512
7185
|
return { ids, versions };
|
|
5513
7186
|
}
|
|
7187
|
+
function readPositiveIntegerEnv3(name, fallback) {
|
|
7188
|
+
const raw = process.env[name];
|
|
7189
|
+
if (!raw) return fallback;
|
|
7190
|
+
const parsed = Number(raw);
|
|
7191
|
+
if (!Number.isFinite(parsed) || parsed < 1) return fallback;
|
|
7192
|
+
return Math.floor(parsed);
|
|
7193
|
+
}
|
|
5514
7194
|
function formatChannelTarget(msg) {
|
|
5515
7195
|
return msg.message.channel_type === "dm" ? `dm:@${msg.message.channel_name}` : `#${msg.message.channel_name}`;
|
|
5516
7196
|
}
|
|
@@ -5534,6 +7214,8 @@ function summarizeIncomingMessage(msg) {
|
|
|
5534
7214
|
return `(agent=${msg.agentId}, path=${msg.path})`;
|
|
5535
7215
|
case "agent:skills:list":
|
|
5536
7216
|
return `(agent=${msg.agentId}, runtime=${msg.runtime || "auto"})`;
|
|
7217
|
+
case "agent:activity_probe":
|
|
7218
|
+
return `(agent=${msg.agentId}, probe=${msg.probeId.slice(0, 8)}, purpose=${msg.purpose})`;
|
|
5537
7219
|
case "machine:workspace:delete":
|
|
5538
7220
|
return `(directory=${msg.directoryName})`;
|
|
5539
7221
|
case "machine:runtime_models:detect":
|
|
@@ -5558,14 +7240,18 @@ var DaemonCore = class {
|
|
|
5558
7240
|
connection;
|
|
5559
7241
|
reminderCache;
|
|
5560
7242
|
tracer;
|
|
7243
|
+
injectedTracer;
|
|
5561
7244
|
machineLock = null;
|
|
7245
|
+
localTraceSink = null;
|
|
7246
|
+
traceBundleUploader = null;
|
|
5562
7247
|
constructor(options) {
|
|
5563
7248
|
this.options = options;
|
|
5564
7249
|
this.daemonVersion = options.daemonVersion ?? readDaemonVersion();
|
|
5565
7250
|
this.chatBridgePath = options.chatBridgePath ?? resolveChatBridgePath();
|
|
5566
7251
|
this.slockCliPath = options.slockCliPath ?? resolveSlockCliPath();
|
|
5567
|
-
this.
|
|
7252
|
+
this.injectedTracer = Boolean(options.tracer);
|
|
5568
7253
|
this.tracer = options.tracer ?? noopTracer;
|
|
7254
|
+
this.runtimeDetector = options.runtimeDetector ?? (() => detectRuntimes(this.tracer));
|
|
5569
7255
|
this.reminderCache = new ReminderCache({
|
|
5570
7256
|
clock: options.reminderClock,
|
|
5571
7257
|
onFire: (job) => this.onReminderFire(job)
|
|
@@ -5575,7 +7261,8 @@ var DaemonCore = class {
|
|
|
5575
7261
|
dataDir: options.dataDir,
|
|
5576
7262
|
serverUrl: options.serverUrl,
|
|
5577
7263
|
defaultAgentEnvVarsProvider: options.defaultAgentEnvVarsProvider,
|
|
5578
|
-
slockCliPath: this.slockCliPath
|
|
7264
|
+
slockCliPath: this.slockCliPath,
|
|
7265
|
+
tracer: this.tracer
|
|
5579
7266
|
};
|
|
5580
7267
|
this.agentManager = options.agentManagerFactory ? options.agentManagerFactory(this.chatBridgePath, (msg) => connection.send(msg), options.apiKey, agentManagerOptions) : new AgentProcessManager(this.chatBridgePath, (msg) => connection.send(msg), options.apiKey, agentManagerOptions);
|
|
5581
7268
|
const connectionFactory = options.connectionFactory ?? ((connOptions) => new DaemonConnection(connOptions));
|
|
@@ -5585,15 +7272,48 @@ var DaemonCore = class {
|
|
|
5585
7272
|
...options.connectionOptions,
|
|
5586
7273
|
onMessage: (msg) => this.handleMessage(msg),
|
|
5587
7274
|
onConnect: () => this.handleConnect(),
|
|
5588
|
-
onDisconnect: () => this.handleDisconnect()
|
|
7275
|
+
onDisconnect: () => this.handleDisconnect(),
|
|
7276
|
+
onTraceEvent: (name, attrs, status) => this.recordDaemonTrace(name, attrs, status)
|
|
5589
7277
|
});
|
|
5590
7278
|
this.connection = connection;
|
|
5591
7279
|
}
|
|
5592
7280
|
resolveMachineStateRoot() {
|
|
5593
7281
|
if (this.options.machineStateDir) return this.options.machineStateDir;
|
|
5594
|
-
if (this.options.dataDir) return
|
|
7282
|
+
if (this.options.dataDir) return path15.join(path15.dirname(this.options.dataDir), "machines");
|
|
5595
7283
|
return DEFAULT_MACHINE_STATE_ROOT;
|
|
5596
7284
|
}
|
|
7285
|
+
shouldEnableLocalTrace() {
|
|
7286
|
+
if (this.injectedTracer) return false;
|
|
7287
|
+
if (!this.options.localTrace) return false;
|
|
7288
|
+
return process.env.SLOCK_DAEMON_LOCAL_TRACE !== "0";
|
|
7289
|
+
}
|
|
7290
|
+
installLocalTraceSink(machineDir) {
|
|
7291
|
+
if (!this.shouldEnableLocalTrace()) return;
|
|
7292
|
+
this.localTraceSink = new LocalRotatingTraceSink({
|
|
7293
|
+
machineDir,
|
|
7294
|
+
maxFileBytes: this.options.localTraceMaxFileBytes ?? readPositiveIntegerEnv3("SLOCK_DAEMON_TRACE_MAX_FILE_BYTES", 5 * 1024 * 1024),
|
|
7295
|
+
maxFiles: this.options.localTraceMaxFiles ?? readPositiveIntegerEnv3("SLOCK_DAEMON_TRACE_MAX_FILES", 8)
|
|
7296
|
+
});
|
|
7297
|
+
this.tracer = new BasicTracer({
|
|
7298
|
+
sink: this.localTraceSink
|
|
7299
|
+
});
|
|
7300
|
+
this.agentManager.setTracer(this.tracer);
|
|
7301
|
+
}
|
|
7302
|
+
installTraceBundleUploader(machineDir) {
|
|
7303
|
+
if (!this.shouldEnableLocalTrace()) return;
|
|
7304
|
+
if (this.traceBundleUploader) return;
|
|
7305
|
+
const workerUrl = process.env.SLOCK_DAEMON_TRACE_UPLOAD_URL;
|
|
7306
|
+
if (!workerUrl) return;
|
|
7307
|
+
this.traceBundleUploader = new DaemonTraceBundleUploader({
|
|
7308
|
+
machineDir,
|
|
7309
|
+
serverUrl: this.options.serverUrl,
|
|
7310
|
+
apiKey: this.options.apiKey,
|
|
7311
|
+
workerUrl,
|
|
7312
|
+
tracer: this.tracer,
|
|
7313
|
+
currentFileProvider: () => this.localTraceSink?.getCurrentFile() ?? null
|
|
7314
|
+
});
|
|
7315
|
+
this.traceBundleUploader.start();
|
|
7316
|
+
}
|
|
5597
7317
|
start() {
|
|
5598
7318
|
logger.info("[Slock Daemon] Starting...");
|
|
5599
7319
|
if (!this.machineLock) {
|
|
@@ -5603,10 +7323,24 @@ var DaemonCore = class {
|
|
|
5603
7323
|
rootDir: this.resolveMachineStateRoot()
|
|
5604
7324
|
});
|
|
5605
7325
|
logger.info(`[Slock Daemon] Acquired machine lock: ${this.machineLock.lockDir}`);
|
|
7326
|
+
this.installLocalTraceSink(this.machineLock.machineDir);
|
|
7327
|
+
this.installTraceBundleUploader(this.machineLock.machineDir);
|
|
7328
|
+
const span = this.tracer.startSpan("daemon.lifecycle.start", {
|
|
7329
|
+
surface: "daemon",
|
|
7330
|
+
kind: "internal",
|
|
7331
|
+
attrs: {
|
|
7332
|
+
machine_dir_present: true,
|
|
7333
|
+
local_trace_enabled: this.shouldEnableLocalTrace()
|
|
7334
|
+
}
|
|
7335
|
+
});
|
|
7336
|
+
span.addEvent("daemon.machine_lock.acquired", { machine_dir_present: true });
|
|
7337
|
+
span.end("ok");
|
|
5606
7338
|
}
|
|
5607
7339
|
try {
|
|
5608
7340
|
this.connection.connect();
|
|
5609
7341
|
} catch (err) {
|
|
7342
|
+
this.traceBundleUploader?.stop();
|
|
7343
|
+
this.traceBundleUploader = null;
|
|
5610
7344
|
this.machineLock.release();
|
|
5611
7345
|
this.machineLock = null;
|
|
5612
7346
|
throw err;
|
|
@@ -5614,13 +7348,24 @@ var DaemonCore = class {
|
|
|
5614
7348
|
}
|
|
5615
7349
|
async stop() {
|
|
5616
7350
|
logger.info("[Slock Daemon] Shutting down...");
|
|
7351
|
+
const span = this.tracer.startSpan("daemon.lifecycle.stop", {
|
|
7352
|
+
surface: "daemon",
|
|
7353
|
+
kind: "internal",
|
|
7354
|
+
attrs: { machine_lock_present: Boolean(this.machineLock) }
|
|
7355
|
+
});
|
|
5617
7356
|
this.reminderCache.clear();
|
|
7357
|
+
this.traceBundleUploader?.stop();
|
|
7358
|
+
this.traceBundleUploader = null;
|
|
5618
7359
|
try {
|
|
5619
7360
|
await this.agentManager.stopAll();
|
|
7361
|
+
span.addEvent("daemon.agents.stopped");
|
|
5620
7362
|
} finally {
|
|
5621
7363
|
this.connection.disconnect();
|
|
7364
|
+
span.addEvent("daemon.connection.disconnect_requested");
|
|
5622
7365
|
this.machineLock?.release();
|
|
7366
|
+
if (this.machineLock) span.addEvent("daemon.machine_lock.released");
|
|
5623
7367
|
this.machineLock = null;
|
|
7368
|
+
span.end("ok");
|
|
5624
7369
|
}
|
|
5625
7370
|
}
|
|
5626
7371
|
get connected() {
|
|
@@ -5629,6 +7374,14 @@ var DaemonCore = class {
|
|
|
5629
7374
|
getRunningAgentIds() {
|
|
5630
7375
|
return this.agentManager.getRunningAgentIds();
|
|
5631
7376
|
}
|
|
7377
|
+
recordDaemonTrace(name, attrs, status = "ok") {
|
|
7378
|
+
const span = this.tracer.startSpan(name, {
|
|
7379
|
+
surface: "daemon",
|
|
7380
|
+
kind: "internal",
|
|
7381
|
+
attrs
|
|
7382
|
+
});
|
|
7383
|
+
span.end(status);
|
|
7384
|
+
}
|
|
5632
7385
|
handleMessage(msg) {
|
|
5633
7386
|
const summary = summarizeIncomingMessage(msg);
|
|
5634
7387
|
logger.info(`[Daemon] Received ${msg.type}${summary ? ` ${summary}` : ""}`);
|
|
@@ -5658,50 +7411,88 @@ var DaemonCore = class {
|
|
|
5658
7411
|
kind: "consumer",
|
|
5659
7412
|
attrs: {
|
|
5660
7413
|
agentId: msg.agentId,
|
|
7414
|
+
deliveryId: msg.deliveryId,
|
|
7415
|
+
delivery_correlation_id: msg.deliveryId ?? msg.message.message_id,
|
|
7416
|
+
messageId: msg.message.message_id,
|
|
7417
|
+
message_id_present: Boolean(msg.message.message_id),
|
|
5661
7418
|
seq: msg.seq
|
|
5662
7419
|
}
|
|
5663
7420
|
});
|
|
5664
7421
|
logger.info(`[Agent ${msg.agentId}] Delivery received (seq=${msg.seq}, from=@${msg.message.sender_name}, target=${formatChannelTarget(msg)})`);
|
|
5665
7422
|
try {
|
|
5666
|
-
span.addEvent("daemon.receive", { seq: msg.seq });
|
|
5667
|
-
this.agentManager.deliverMessage(msg.agentId, msg.message);
|
|
5668
|
-
span.addEvent("daemon.deliver_to_agent_manager");
|
|
7423
|
+
span.addEvent("daemon.receive", { seq: msg.seq, deliveryId: msg.deliveryId });
|
|
7424
|
+
const accepted = this.agentManager.deliverMessage(msg.agentId, msg.message, { deliveryId: msg.deliveryId });
|
|
7425
|
+
span.addEvent("daemon.deliver_to_agent_manager", { accepted });
|
|
7426
|
+
if (!accepted) {
|
|
7427
|
+
span.end("ok", { attrs: { outcome: "not-accepted" } });
|
|
7428
|
+
break;
|
|
7429
|
+
}
|
|
5669
7430
|
const ackSeq = msg.seq > 0 ? msg.seq : msg.message.seq ?? 0;
|
|
5670
7431
|
span.addEvent("daemon.ack.sent", { seq: ackSeq });
|
|
5671
7432
|
this.connection.send({
|
|
5672
7433
|
type: "agent:deliver:ack",
|
|
5673
7434
|
agentId: msg.agentId,
|
|
5674
7435
|
seq: ackSeq,
|
|
5675
|
-
traceparent: formatTraceparent(span.context)
|
|
7436
|
+
traceparent: formatTraceparent(span.context),
|
|
7437
|
+
deliveryId: msg.deliveryId
|
|
5676
7438
|
});
|
|
5677
|
-
span.end("ok", { attrs: { outcome: "ack-sent", ackSeq } });
|
|
7439
|
+
span.end("ok", { attrs: { outcome: "ack-sent", ackSeq, deliveryId: msg.deliveryId } });
|
|
5678
7440
|
} catch (err) {
|
|
5679
|
-
span.end("error", { attrs: {
|
|
7441
|
+
span.end("error", { attrs: { error_class: err instanceof Error ? err.name : typeof err } });
|
|
5680
7442
|
throw err;
|
|
5681
7443
|
}
|
|
5682
7444
|
break;
|
|
5683
7445
|
}
|
|
5684
|
-
case "agent:runtime_profile:migration":
|
|
7446
|
+
case "agent:runtime_profile:migration": {
|
|
7447
|
+
const span = this.tracer.startSpan("daemon.runtime_profile.control.received", {
|
|
7448
|
+
parent: parseTraceparent(msg.traceparent),
|
|
7449
|
+
surface: "daemon",
|
|
7450
|
+
kind: "consumer",
|
|
7451
|
+
attrs: {
|
|
7452
|
+
agentId: msg.agentId,
|
|
7453
|
+
control_kind: "migration",
|
|
7454
|
+
key_present: Boolean(msg.migrationKey),
|
|
7455
|
+
launchId: msg.launchId || void 0
|
|
7456
|
+
}
|
|
7457
|
+
});
|
|
5685
7458
|
logger.info(`[Agent ${msg.agentId}] Runtime profile migration received (${msg.migrationKey})`);
|
|
5686
|
-
this.agentManager.deliverRuntimeProfileNotification(msg.agentId, msg.migrationKey, "migration", msg.message);
|
|
7459
|
+
const accepted = this.agentManager.deliverRuntimeProfileNotification(msg.agentId, msg.migrationKey, "migration", msg.message, formatTraceparent(span.context));
|
|
7460
|
+
span.end("ok", { attrs: { outcome: accepted ? "accepted" : "no_injection_path" } });
|
|
5687
7461
|
break;
|
|
5688
|
-
|
|
7462
|
+
}
|
|
7463
|
+
case "agent:runtime_profile:daemon_release_notice": {
|
|
7464
|
+
const span = this.tracer.startSpan("daemon.runtime_profile.control.received", {
|
|
7465
|
+
parent: parseTraceparent(msg.traceparent),
|
|
7466
|
+
surface: "daemon",
|
|
7467
|
+
kind: "consumer",
|
|
7468
|
+
attrs: {
|
|
7469
|
+
agentId: msg.agentId,
|
|
7470
|
+
control_kind: "daemon_release_notice",
|
|
7471
|
+
key_present: Boolean(msg.noticeKey),
|
|
7472
|
+
launchId: msg.launchId || void 0
|
|
7473
|
+
}
|
|
7474
|
+
});
|
|
5689
7475
|
logger.info(`[Agent ${msg.agentId}] Runtime profile daemon release notice received (${msg.noticeKey})`);
|
|
5690
|
-
this.agentManager.deliverRuntimeProfileNotification(msg.agentId, msg.noticeKey, "daemon_release_notice", msg.message);
|
|
7476
|
+
const accepted = this.agentManager.deliverRuntimeProfileNotification(msg.agentId, msg.noticeKey, "daemon_release_notice", msg.message, formatTraceparent(span.context));
|
|
7477
|
+
span.end("ok", { attrs: { outcome: accepted ? "accepted" : "no_injection_path" } });
|
|
5691
7478
|
break;
|
|
7479
|
+
}
|
|
5692
7480
|
case "agent:workspace:list":
|
|
5693
7481
|
this.agentManager.getFileTree(msg.agentId, msg.dirPath).then((files) => {
|
|
5694
7482
|
this.connection.send({ type: "agent:workspace:file_tree", agentId: msg.agentId, files, dirPath: msg.dirPath });
|
|
5695
7483
|
});
|
|
5696
7484
|
break;
|
|
5697
7485
|
case "agent:workspace:read":
|
|
5698
|
-
this.agentManager.readFile(msg.agentId, msg.path).then(({ content, binary }) => {
|
|
7486
|
+
this.agentManager.readFile(msg.agentId, msg.path).then(({ content, binary, size, mimeType, encoding }) => {
|
|
5699
7487
|
this.connection.send({
|
|
5700
7488
|
type: "agent:workspace:file_content",
|
|
5701
7489
|
agentId: msg.agentId,
|
|
5702
7490
|
requestId: msg.requestId,
|
|
5703
7491
|
content,
|
|
5704
|
-
binary
|
|
7492
|
+
binary,
|
|
7493
|
+
size,
|
|
7494
|
+
mimeType,
|
|
7495
|
+
encoding
|
|
5705
7496
|
});
|
|
5706
7497
|
}).catch(() => {
|
|
5707
7498
|
this.connection.send({
|
|
@@ -5709,7 +7500,8 @@ var DaemonCore = class {
|
|
|
5709
7500
|
agentId: msg.agentId,
|
|
5710
7501
|
requestId: msg.requestId,
|
|
5711
7502
|
content: null,
|
|
5712
|
-
binary: false
|
|
7503
|
+
binary: false,
|
|
7504
|
+
size: 0
|
|
5713
7505
|
});
|
|
5714
7506
|
});
|
|
5715
7507
|
break;
|
|
@@ -5721,6 +7513,9 @@ var DaemonCore = class {
|
|
|
5721
7513
|
this.connection.send({ type: "agent:skills:list_result", agentId: msg.agentId, global: [], workspace: [] });
|
|
5722
7514
|
});
|
|
5723
7515
|
break;
|
|
7516
|
+
case "agent:activity_probe":
|
|
7517
|
+
this.agentManager.respondToActivityProbe(msg.agentId, msg.probeId);
|
|
7518
|
+
break;
|
|
5724
7519
|
case "machine:workspace:scan":
|
|
5725
7520
|
logger.info("[Daemon] Scanning all workspace directories");
|
|
5726
7521
|
this.agentManager.scanAllWorkspaces().then((directories) => {
|
|
@@ -5738,7 +7533,12 @@ var DaemonCore = class {
|
|
|
5738
7533
|
const detect = typeof driver?.detectModels === "function" ? driver.detectModels() : Promise.resolve(null);
|
|
5739
7534
|
Promise.resolve(detect).then((result) => {
|
|
5740
7535
|
if (result) {
|
|
5741
|
-
|
|
7536
|
+
const verified = driver.model.detectedModelsVerifiedAs;
|
|
7537
|
+
const models = result.models.map((model) => ({
|
|
7538
|
+
...model,
|
|
7539
|
+
verified: model.verified ?? verified
|
|
7540
|
+
}));
|
|
7541
|
+
this.connection.send({ type: "machine:runtime_models:result", requestId: msg.requestId, models, default: result.default });
|
|
5742
7542
|
} else {
|
|
5743
7543
|
this.connection.send({ type: "machine:runtime_models:result", requestId: msg.requestId, error: "unsupported" });
|
|
5744
7544
|
}
|
|
@@ -5777,35 +7577,59 @@ var DaemonCore = class {
|
|
|
5777
7577
|
const { ids: runtimes, versions: runtimeVersions } = this.runtimeDetector();
|
|
5778
7578
|
const runtimeInfo = runtimes.map((id) => runtimeVersions[id] ? `${id} (${runtimeVersions[id]})` : id);
|
|
5779
7579
|
logger.info(`[Daemon] Detected runtimes: ${runtimeInfo.join(", ") || "none"}`);
|
|
7580
|
+
const runningAgentIds = this.agentManager.getRunningAgentIds();
|
|
7581
|
+
const idleAgentSessions = this.agentManager.getIdleAgentSessionIds();
|
|
7582
|
+
const runtimeProfileReports = this.agentManager.getAgentRuntimeProfileReports();
|
|
5780
7583
|
this.connection.send({
|
|
5781
7584
|
type: "ready",
|
|
5782
7585
|
capabilities: ["agent:start", "agent:stop", "agent:deliver", "workspace:files"],
|
|
5783
7586
|
runtimes,
|
|
5784
|
-
runningAgents:
|
|
5785
|
-
hostname: this.options.hostname ??
|
|
5786
|
-
os: this.options.osDescription ?? `${
|
|
7587
|
+
runningAgents: runningAgentIds,
|
|
7588
|
+
hostname: this.options.hostname ?? os7.hostname(),
|
|
7589
|
+
os: this.options.osDescription ?? `${os7.platform()} ${os7.arch()}`,
|
|
5787
7590
|
daemonVersion: this.daemonVersion
|
|
5788
7591
|
});
|
|
5789
|
-
|
|
7592
|
+
this.recordDaemonTrace("daemon.ready.sent", {
|
|
7593
|
+
runtimes_count: runtimes.length,
|
|
7594
|
+
running_agents_count: runningAgentIds.length,
|
|
7595
|
+
idle_agents_count: idleAgentSessions.length,
|
|
7596
|
+
runtime_profile_reports_count: runtimeProfileReports.length,
|
|
7597
|
+
daemon_version_present: Boolean(this.daemonVersion)
|
|
7598
|
+
});
|
|
7599
|
+
for (const agentId of runningAgentIds) {
|
|
5790
7600
|
const sessionId = this.agentManager.getAgentSessionId(agentId);
|
|
5791
7601
|
const launchId = this.agentManager.getAgentLaunchId(agentId);
|
|
5792
7602
|
if (sessionId) {
|
|
5793
7603
|
this.connection.send({ type: "agent:session", agentId, sessionId, launchId: launchId || void 0 });
|
|
5794
7604
|
}
|
|
5795
7605
|
}
|
|
5796
|
-
for (const { agentId, sessionId, launchId } of
|
|
7606
|
+
for (const { agentId, sessionId, launchId } of idleAgentSessions) {
|
|
5797
7607
|
this.connection.send({ type: "agent:session", agentId, sessionId, launchId: launchId || void 0 });
|
|
5798
7608
|
}
|
|
5799
|
-
for (const report of
|
|
7609
|
+
for (const report of runtimeProfileReports) {
|
|
7610
|
+
const span = this.tracer.startSpan("daemon.runtime_profile.report.sent", {
|
|
7611
|
+
surface: "daemon",
|
|
7612
|
+
kind: "producer",
|
|
7613
|
+
attrs: {
|
|
7614
|
+
agentId: report.agentId,
|
|
7615
|
+
launchId: report.launchId || void 0,
|
|
7616
|
+
runtime: report.facts.runtime,
|
|
7617
|
+
model_present: Boolean(report.facts.model),
|
|
7618
|
+
session_ref_present: Boolean(report.facts.sessionRef),
|
|
7619
|
+
workspace_ref_present: Boolean(report.facts.workspaceRef || report.facts.workspacePathRef)
|
|
7620
|
+
}
|
|
7621
|
+
});
|
|
5800
7622
|
this.connection.send({
|
|
5801
7623
|
type: "agent:runtime_profile",
|
|
5802
7624
|
agentId: report.agentId,
|
|
5803
7625
|
facts: report.facts,
|
|
5804
|
-
launchId: report.launchId || void 0
|
|
7626
|
+
launchId: report.launchId || void 0,
|
|
7627
|
+
traceparent: formatTraceparent(span.context)
|
|
5805
7628
|
});
|
|
7629
|
+
span.end("ok");
|
|
5806
7630
|
}
|
|
5807
|
-
const agentsForSnapshot = new Set(
|
|
5808
|
-
for (const { agentId } of
|
|
7631
|
+
const agentsForSnapshot = new Set(runningAgentIds);
|
|
7632
|
+
for (const { agentId } of idleAgentSessions) {
|
|
5809
7633
|
agentsForSnapshot.add(agentId);
|
|
5810
7634
|
}
|
|
5811
7635
|
for (const agentId of agentsForSnapshot) {
|
|
@@ -5815,6 +7639,10 @@ var DaemonCore = class {
|
|
|
5815
7639
|
}
|
|
5816
7640
|
handleDisconnect() {
|
|
5817
7641
|
logger.warn("[Daemon] Lost connection \u2014 agents continue running locally");
|
|
7642
|
+
this.recordDaemonTrace("daemon.connection.local_disconnect_observed", {
|
|
7643
|
+
running_agents_count: this.agentManager.getRunningAgentIds().length,
|
|
7644
|
+
idle_agents_count: this.agentManager.getIdleAgentSessionIds().length
|
|
7645
|
+
}, "cancelled");
|
|
5818
7646
|
this.options.lifecycleHooks?.onDisconnect?.();
|
|
5819
7647
|
}
|
|
5820
7648
|
};
|