@slock-ai/daemon 0.43.0 → 0.46.0-play.20260508161002
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 +27 -110
- package/dist/{chunk-37O7EHYE.js → chunk-MEY33LAG.js} +2315 -277
- package/dist/{chunk-JG7ONJZ6.js → chunk-Z3PCMYZO.js} +101 -1
- package/dist/cli/index.js +268 -51
- 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
|
|
|
@@ -1031,6 +1093,17 @@ Keep the user informed. They cannot see your internal reasoning, so:
|
|
|
1031
1093
|
- For multi-step work, send short progress updates (e.g. "Working on step 2/3\u2026").
|
|
1032
1094
|
- When done, summarize the result.
|
|
1033
1095
|
- Keep updates concise \u2014 one or two sentences. Don't flood the chat.
|
|
1096
|
+
- For long answers where users need the conclusion first but details still matter, put the conclusion and next action outside any collapse, then use sanitized HTML details blocks for optional depth:
|
|
1097
|
+
|
|
1098
|
+
\`\`\`html
|
|
1099
|
+
<details>
|
|
1100
|
+
<summary>Evidence, logs, or edge cases</summary>
|
|
1101
|
+
|
|
1102
|
+
Detailed notes go here.
|
|
1103
|
+
</details>
|
|
1104
|
+
\`\`\`
|
|
1105
|
+
|
|
1106
|
+
Do not hide the main recommendation, blocker, or required action inside \`<details>\`; only fold supporting evidence, logs, alternatives, or extended rationale.
|
|
1034
1107
|
|
|
1035
1108
|
### Conversation etiquette
|
|
1036
1109
|
|
|
@@ -1301,6 +1374,7 @@ var CLAUDE_DESKTOP_CLI_RELATIVE_PATH = path3.join("Applications", "Claude Code U
|
|
|
1301
1374
|
var CLAUDE_DESKTOP_CLI_SYSTEM_PATH = "/Applications/Claude Code URL Handler.app/Contents/MacOS/claude";
|
|
1302
1375
|
var CLAUDE_SYSTEM_PROMPT_FILE = "claude-system-prompt.md";
|
|
1303
1376
|
var CLAUDE_MCP_CONFIG_FILE = "claude-mcp-config.json";
|
|
1377
|
+
var SLOCK_RUNTIME_ACTIONS_MCP_SERVER_NAME = "chat";
|
|
1304
1378
|
var CLAUDE_DISALLOWED_TOOLS = [
|
|
1305
1379
|
"EnterPlanMode",
|
|
1306
1380
|
"ExitPlanMode",
|
|
@@ -1326,10 +1400,101 @@ function probeClaude(deps = {}) {
|
|
|
1326
1400
|
version: readCommandVersion(command, [], deps) ?? void 0
|
|
1327
1401
|
};
|
|
1328
1402
|
}
|
|
1403
|
+
function isRecord(value) {
|
|
1404
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1405
|
+
}
|
|
1406
|
+
function expandClaudeMcpConfigVariables(raw, vars) {
|
|
1407
|
+
let expanded = raw;
|
|
1408
|
+
for (const [name, value] of Object.entries(vars)) {
|
|
1409
|
+
expanded = expanded.replaceAll(`\${${name}}`, value);
|
|
1410
|
+
}
|
|
1411
|
+
return expanded;
|
|
1412
|
+
}
|
|
1413
|
+
function readClaudeMcpServers(configPath, vars = {}) {
|
|
1414
|
+
try {
|
|
1415
|
+
const parsed = JSON.parse(
|
|
1416
|
+
expandClaudeMcpConfigVariables(readFileSync(configPath, "utf8"), vars)
|
|
1417
|
+
);
|
|
1418
|
+
if (!isRecord(parsed) || !isRecord(parsed.mcpServers)) return null;
|
|
1419
|
+
return parsed.mcpServers;
|
|
1420
|
+
} catch (err) {
|
|
1421
|
+
logger.warn(
|
|
1422
|
+
`[Claude] failed to read MCP config ${configPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
1423
|
+
);
|
|
1424
|
+
return null;
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
function resolveClaudeConfigDir(ctx, home) {
|
|
1428
|
+
const configured = ctx.config.envVars?.CLAUDE_CONFIG_DIR || process.env.CLAUDE_CONFIG_DIR;
|
|
1429
|
+
return configured && path3.isAbsolute(configured) ? configured : path3.join(home, ".claude");
|
|
1430
|
+
}
|
|
1431
|
+
function collectClaudeMcpConfigFiles(ctx, home) {
|
|
1432
|
+
const files = [];
|
|
1433
|
+
const pushIfFile = (candidate) => {
|
|
1434
|
+
try {
|
|
1435
|
+
if (existsSync2(candidate) && statSync(candidate).isFile()) {
|
|
1436
|
+
files.push({ path: candidate });
|
|
1437
|
+
}
|
|
1438
|
+
} catch {
|
|
1439
|
+
}
|
|
1440
|
+
};
|
|
1441
|
+
pushIfFile(path3.join(home, ".claude.json"));
|
|
1442
|
+
pushIfFile(path3.join(ctx.workingDirectory, ".mcp.json"));
|
|
1443
|
+
const pluginRoot = path3.join(resolveClaudeConfigDir(ctx, home), "plugins");
|
|
1444
|
+
try {
|
|
1445
|
+
for (const entry of readdirSync(pluginRoot)) {
|
|
1446
|
+
const pluginPath = path3.join(pluginRoot, entry);
|
|
1447
|
+
const configPath = path3.join(pluginPath, ".mcp.json");
|
|
1448
|
+
try {
|
|
1449
|
+
if (existsSync2(configPath) && statSync(configPath).isFile()) {
|
|
1450
|
+
files.push({
|
|
1451
|
+
path: configPath,
|
|
1452
|
+
vars: { CLAUDE_PLUGIN_ROOT: pluginPath }
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1455
|
+
} catch {
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
} catch {
|
|
1459
|
+
}
|
|
1460
|
+
return files;
|
|
1461
|
+
}
|
|
1462
|
+
function buildClaudeUserMcpServers(ctx, home) {
|
|
1463
|
+
const servers = /* @__PURE__ */ Object.create(null);
|
|
1464
|
+
for (const configFile of collectClaudeMcpConfigFiles(ctx, home)) {
|
|
1465
|
+
const mcpServers = readClaudeMcpServers(configFile.path, configFile.vars);
|
|
1466
|
+
if (!mcpServers) continue;
|
|
1467
|
+
for (const [name, server] of Object.entries(mcpServers)) {
|
|
1468
|
+
if (!isRecord(server)) {
|
|
1469
|
+
logger.warn(`[Claude] ignoring invalid MCP server "${name}" in ${configFile.path}`);
|
|
1470
|
+
continue;
|
|
1471
|
+
}
|
|
1472
|
+
servers[name] = server;
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
return servers;
|
|
1476
|
+
}
|
|
1329
1477
|
var ClaudeDriver = class {
|
|
1330
1478
|
id = "claude";
|
|
1479
|
+
lifecycle = {
|
|
1480
|
+
kind: "persistent",
|
|
1481
|
+
stdin: "gated",
|
|
1482
|
+
inFlightWake: "queue"
|
|
1483
|
+
};
|
|
1484
|
+
communication = {
|
|
1485
|
+
chat: "slock_cli",
|
|
1486
|
+
runtimeControl: "mcp_runtime_actions"
|
|
1487
|
+
};
|
|
1488
|
+
session = {
|
|
1489
|
+
recovery: "resume_or_fresh"
|
|
1490
|
+
};
|
|
1491
|
+
model = {
|
|
1492
|
+
detectedModelsVerifiedAs: "launchable",
|
|
1493
|
+
toLaunchSpec: (modelId) => ({ args: ["--model", modelId] })
|
|
1494
|
+
};
|
|
1331
1495
|
supportsStdinNotification = true;
|
|
1332
1496
|
mcpToolPrefix = "mcp__chat__";
|
|
1497
|
+
usesSlockCliForCommunication = true;
|
|
1333
1498
|
// Claude Code supports same-turn steering, but raw stdin injection at an
|
|
1334
1499
|
// arbitrary busy instant can collide with active signed thinking blocks. The
|
|
1335
1500
|
// daemon therefore gates busy delivery on Claude stream-json boundaries.
|
|
@@ -1364,36 +1529,46 @@ var ClaudeDriver = class {
|
|
|
1364
1529
|
}
|
|
1365
1530
|
return args;
|
|
1366
1531
|
}
|
|
1367
|
-
|
|
1532
|
+
buildRuntimeActionsMcpServer(ctx) {
|
|
1368
1533
|
const isTsSource = ctx.chatBridgePath.endsWith(".ts");
|
|
1369
1534
|
const command = isTsSource ? "npx" : "node";
|
|
1370
1535
|
const bridgeArgs = isTsSource ? ["tsx", ctx.chatBridgePath] : [ctx.chatBridgePath];
|
|
1536
|
+
return {
|
|
1537
|
+
command,
|
|
1538
|
+
args: [
|
|
1539
|
+
...bridgeArgs,
|
|
1540
|
+
"--agent-id",
|
|
1541
|
+
ctx.agentId,
|
|
1542
|
+
"--server-url",
|
|
1543
|
+
ctx.config.serverUrl,
|
|
1544
|
+
"--auth-token",
|
|
1545
|
+
ctx.config.authToken || ctx.daemonApiKey,
|
|
1546
|
+
"--runtime",
|
|
1547
|
+
this.id,
|
|
1548
|
+
...ctx.launchId ? ["--launch-id", ctx.launchId] : [],
|
|
1549
|
+
"--runtime-actions-only"
|
|
1550
|
+
]
|
|
1551
|
+
};
|
|
1552
|
+
}
|
|
1553
|
+
buildRuntimeActionsMcpConfig(ctx, home = os.homedir()) {
|
|
1554
|
+
const userMcpServers = buildClaudeUserMcpServers(ctx, home);
|
|
1555
|
+
if (Object.prototype.hasOwnProperty.call(userMcpServers, SLOCK_RUNTIME_ACTIONS_MCP_SERVER_NAME)) {
|
|
1556
|
+
logger.warn(
|
|
1557
|
+
`[Agent ${ctx.agentId}] Claude user MCP server "${SLOCK_RUNTIME_ACTIONS_MCP_SERVER_NAME}" is reserved by Slock runtime actions and will be ignored`
|
|
1558
|
+
);
|
|
1559
|
+
}
|
|
1371
1560
|
return JSON.stringify({
|
|
1372
1561
|
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
|
-
}
|
|
1562
|
+
...userMcpServers,
|
|
1563
|
+
[SLOCK_RUNTIME_ACTIONS_MCP_SERVER_NAME]: this.buildRuntimeActionsMcpServer(ctx)
|
|
1389
1564
|
}
|
|
1390
1565
|
});
|
|
1391
1566
|
}
|
|
1392
|
-
writeClaudeLaunchFiles(ctx, slockDir) {
|
|
1567
|
+
writeClaudeLaunchFiles(ctx, slockDir, home = os.homedir()) {
|
|
1393
1568
|
const systemPromptPath = path3.join(slockDir, CLAUDE_SYSTEM_PROMPT_FILE);
|
|
1394
1569
|
const mcpConfigPath = path3.join(slockDir, CLAUDE_MCP_CONFIG_FILE);
|
|
1395
1570
|
writeFileSync2(systemPromptPath, ctx.standingPrompt, { mode: 384 });
|
|
1396
|
-
writeFileSync2(mcpConfigPath, this.buildRuntimeActionsMcpConfig(ctx), { mode: 384 });
|
|
1571
|
+
writeFileSync2(mcpConfigPath, this.buildRuntimeActionsMcpConfig(ctx, home), { mode: 384 });
|
|
1397
1572
|
return { systemPromptPath, mcpConfigPath };
|
|
1398
1573
|
}
|
|
1399
1574
|
spawn(ctx) {
|
|
@@ -1541,8 +1716,8 @@ var ClaudeDriver = class {
|
|
|
1541
1716
|
|
|
1542
1717
|
// src/drivers/codex.ts
|
|
1543
1718
|
import { spawn as spawn2, execSync } from "child_process";
|
|
1544
|
-
import { existsSync as
|
|
1545
|
-
import
|
|
1719
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
1720
|
+
import os2 from "os";
|
|
1546
1721
|
import path4 from "path";
|
|
1547
1722
|
function getCodexNotificationErrorMessage(params) {
|
|
1548
1723
|
const topLevelMessage = params?.message;
|
|
@@ -1556,7 +1731,7 @@ function getCodexNotificationErrorMessage(params) {
|
|
|
1556
1731
|
return null;
|
|
1557
1732
|
}
|
|
1558
1733
|
function ensureGitRepoForCodex(workingDirectory, deps = {}) {
|
|
1559
|
-
const existsSyncFn = deps.existsSyncFn ??
|
|
1734
|
+
const existsSyncFn = deps.existsSyncFn ?? existsSync3;
|
|
1560
1735
|
const execSyncFn = deps.execSyncFn ?? execSync;
|
|
1561
1736
|
const gitDir = path4.join(workingDirectory, ".git");
|
|
1562
1737
|
if (existsSyncFn(gitDir)) return;
|
|
@@ -1603,14 +1778,14 @@ function resolveCodexSpawn(commandArgs, deps = {}) {
|
|
|
1603
1778
|
try {
|
|
1604
1779
|
const globalRoot = execSync("npm root -g", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
1605
1780
|
const candidate = path4.join(globalRoot, "@openai", "codex", "bin", "codex.js");
|
|
1606
|
-
if (
|
|
1781
|
+
if (existsSync3(candidate)) codexEntry = candidate;
|
|
1607
1782
|
} catch {
|
|
1608
1783
|
}
|
|
1609
1784
|
if (!codexEntry) {
|
|
1610
1785
|
try {
|
|
1611
1786
|
const cmdPath = execSync("where codex", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim().split(/\r?\n/)[0];
|
|
1612
1787
|
const candidate = path4.join(path4.dirname(cmdPath), "node_modules", "@openai", "codex", "bin", "codex.js");
|
|
1613
|
-
if (
|
|
1788
|
+
if (existsSync3(candidate)) codexEntry = candidate;
|
|
1614
1789
|
} catch {
|
|
1615
1790
|
}
|
|
1616
1791
|
}
|
|
@@ -1631,8 +1806,25 @@ function joinReasoningText(item) {
|
|
|
1631
1806
|
}
|
|
1632
1807
|
var CodexDriver = class {
|
|
1633
1808
|
id = "codex";
|
|
1809
|
+
lifecycle = {
|
|
1810
|
+
kind: "persistent",
|
|
1811
|
+
stdin: "direct",
|
|
1812
|
+
inFlightWake: "steer"
|
|
1813
|
+
};
|
|
1814
|
+
communication = {
|
|
1815
|
+
chat: "slock_cli",
|
|
1816
|
+
runtimeControl: "mcp_runtime_actions"
|
|
1817
|
+
};
|
|
1818
|
+
session = {
|
|
1819
|
+
recovery: "resume_or_fresh"
|
|
1820
|
+
};
|
|
1821
|
+
model = {
|
|
1822
|
+
detectedModelsVerifiedAs: "launchable",
|
|
1823
|
+
toLaunchSpec: (modelId) => ({ params: { model: modelId } })
|
|
1824
|
+
};
|
|
1634
1825
|
supportsStdinNotification = true;
|
|
1635
1826
|
mcpToolPrefix = "mcp_chat_";
|
|
1827
|
+
usesSlockCliForCommunication = true;
|
|
1636
1828
|
busyDeliveryMode = "direct";
|
|
1637
1829
|
supportsNativeStandingPrompt = true;
|
|
1638
1830
|
probe() {
|
|
@@ -2013,12 +2205,12 @@ var CodexDriver = class {
|
|
|
2013
2205
|
return detectCodexModels();
|
|
2014
2206
|
}
|
|
2015
2207
|
};
|
|
2016
|
-
function detectCodexModels(home =
|
|
2208
|
+
function detectCodexModels(home = os2.homedir()) {
|
|
2017
2209
|
const cachePath = path4.join(home, ".codex", "models_cache.json");
|
|
2018
2210
|
const configPath = path4.join(home, ".codex", "config.toml");
|
|
2019
2211
|
let models = [];
|
|
2020
2212
|
try {
|
|
2021
|
-
const raw =
|
|
2213
|
+
const raw = readFileSync2(cachePath, "utf8");
|
|
2022
2214
|
const parsed = JSON.parse(raw);
|
|
2023
2215
|
const entries = Array.isArray(parsed?.models) ? parsed.models : [];
|
|
2024
2216
|
for (const entry of entries) {
|
|
@@ -2027,7 +2219,7 @@ function detectCodexModels(home = os.homedir()) {
|
|
|
2027
2219
|
if (entry?.visibility && entry.visibility !== "public") continue;
|
|
2028
2220
|
if (entry?.supported_in_api === false) continue;
|
|
2029
2221
|
const label = typeof entry?.display_name === "string" && entry.display_name.length > 0 ? entry.display_name : slug;
|
|
2030
|
-
models.push({ id: slug, label });
|
|
2222
|
+
models.push({ id: slug, label, verified: "launchable" });
|
|
2031
2223
|
}
|
|
2032
2224
|
} catch {
|
|
2033
2225
|
return null;
|
|
@@ -2035,7 +2227,7 @@ function detectCodexModels(home = os.homedir()) {
|
|
|
2035
2227
|
if (models.length === 0) return null;
|
|
2036
2228
|
let defaultModel;
|
|
2037
2229
|
try {
|
|
2038
|
-
const raw =
|
|
2230
|
+
const raw = readFileSync2(configPath, "utf8");
|
|
2039
2231
|
const match = raw.match(/^\s*model\s*=\s*"([^"]+)"/m);
|
|
2040
2232
|
if (match) defaultModel = match[1];
|
|
2041
2233
|
} catch {
|
|
@@ -2049,6 +2241,23 @@ import path5 from "path";
|
|
|
2049
2241
|
import { writeFileSync as writeFileSync3 } from "fs";
|
|
2050
2242
|
var CopilotDriver = class {
|
|
2051
2243
|
id = "copilot";
|
|
2244
|
+
lifecycle = {
|
|
2245
|
+
kind: "per_turn",
|
|
2246
|
+
start: "immediate",
|
|
2247
|
+
exit: "natural",
|
|
2248
|
+
inFlightWake: "spawn_new"
|
|
2249
|
+
};
|
|
2250
|
+
communication = {
|
|
2251
|
+
chat: "mcp_chat_bridge",
|
|
2252
|
+
runtimeControl: "mcp_runtime_actions"
|
|
2253
|
+
};
|
|
2254
|
+
session = {
|
|
2255
|
+
recovery: "resume_or_fresh"
|
|
2256
|
+
};
|
|
2257
|
+
model = {
|
|
2258
|
+
detectedModelsVerifiedAs: "launchable",
|
|
2259
|
+
toLaunchSpec: (modelId) => ({ args: ["--model", modelId] })
|
|
2260
|
+
};
|
|
2052
2261
|
supportsStdinNotification = false;
|
|
2053
2262
|
mcpToolPrefix = "";
|
|
2054
2263
|
busyDeliveryMode = "none";
|
|
@@ -2182,17 +2391,34 @@ var CopilotDriver = class {
|
|
|
2182
2391
|
};
|
|
2183
2392
|
|
|
2184
2393
|
// src/drivers/cursor.ts
|
|
2185
|
-
import { spawn as spawn4 } from "child_process";
|
|
2186
|
-
import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync2, existsSync as
|
|
2394
|
+
import { spawn as spawn4, spawnSync } from "child_process";
|
|
2395
|
+
import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync2, existsSync as existsSync4 } from "fs";
|
|
2187
2396
|
import path6 from "path";
|
|
2188
2397
|
var CursorDriver = class {
|
|
2189
2398
|
id = "cursor";
|
|
2399
|
+
lifecycle = {
|
|
2400
|
+
kind: "per_turn",
|
|
2401
|
+
start: "immediate",
|
|
2402
|
+
exit: "natural",
|
|
2403
|
+
inFlightWake: "spawn_new"
|
|
2404
|
+
};
|
|
2405
|
+
communication = {
|
|
2406
|
+
chat: "mcp_chat_bridge",
|
|
2407
|
+
runtimeControl: "mcp_runtime_actions"
|
|
2408
|
+
};
|
|
2409
|
+
session = {
|
|
2410
|
+
recovery: "resume_or_fresh"
|
|
2411
|
+
};
|
|
2412
|
+
model = {
|
|
2413
|
+
detectedModelsVerifiedAs: "launchable",
|
|
2414
|
+
toLaunchSpec: (modelId) => ({ args: ["--model", modelId] })
|
|
2415
|
+
};
|
|
2190
2416
|
supportsStdinNotification = false;
|
|
2191
2417
|
mcpToolPrefix = "mcp__chat__";
|
|
2192
2418
|
busyDeliveryMode = "none";
|
|
2193
2419
|
spawn(ctx) {
|
|
2194
2420
|
const cursorDir = path6.join(ctx.workingDirectory, ".cursor");
|
|
2195
|
-
if (!
|
|
2421
|
+
if (!existsSync4(cursorDir)) {
|
|
2196
2422
|
mkdirSync2(cursorDir, { recursive: true });
|
|
2197
2423
|
}
|
|
2198
2424
|
const isTsSource = ctx.chatBridgePath.endsWith(".ts");
|
|
@@ -2299,49 +2525,89 @@ var CursorDriver = class {
|
|
|
2299
2525
|
messageNotificationStyle: "poll"
|
|
2300
2526
|
});
|
|
2301
2527
|
}
|
|
2528
|
+
async detectModels() {
|
|
2529
|
+
return detectCursorModels();
|
|
2530
|
+
}
|
|
2302
2531
|
};
|
|
2532
|
+
function parseCursorModelsOutput(output) {
|
|
2533
|
+
const stripAnsi = (value) => value.replace(/\u001b\[[0-9;]*m/g, "");
|
|
2534
|
+
const models = [];
|
|
2535
|
+
let defaultModel;
|
|
2536
|
+
for (const rawLine of stripAnsi(output).split(/\r?\n/)) {
|
|
2537
|
+
const line = rawLine.trim();
|
|
2538
|
+
if (!line || /^available models$/i.test(line) || /^tip:/i.test(line)) continue;
|
|
2539
|
+
if (/^no models available/i.test(line) || /^failed to load models:/i.test(line)) continue;
|
|
2540
|
+
let modelLine = line;
|
|
2541
|
+
const markerMatch = modelLine.match(/\s+\(([^)]+)\)$/);
|
|
2542
|
+
const markers = markerMatch?.[1]?.split(",").map((part) => part.trim().toLowerCase()) ?? [];
|
|
2543
|
+
if (markers.length > 0 && markers.every((part) => part === "current" || part === "default")) {
|
|
2544
|
+
const markerStart = markerMatch?.index ?? modelLine.length;
|
|
2545
|
+
modelLine = modelLine.slice(0, markerStart).trim();
|
|
2546
|
+
}
|
|
2547
|
+
const match = modelLine.match(/^(\S+)(?:\s+-\s+(.+))?$/);
|
|
2548
|
+
if (!match) continue;
|
|
2549
|
+
const id = match[1]?.trim();
|
|
2550
|
+
if (!id || id.startsWith("-")) continue;
|
|
2551
|
+
const label = match[2]?.trim() || id;
|
|
2552
|
+
models.push({ id, label, verified: "launchable" });
|
|
2553
|
+
if (markers.includes("default")) defaultModel = id;
|
|
2554
|
+
}
|
|
2555
|
+
if (models.length === 0) return null;
|
|
2556
|
+
return { models, default: defaultModel };
|
|
2557
|
+
}
|
|
2558
|
+
function detectCursorModels(runCommand = runCursorModelsCommand) {
|
|
2559
|
+
const result = runCommand();
|
|
2560
|
+
if (result.error || result.status !== 0) return null;
|
|
2561
|
+
return parseCursorModelsOutput(String(result.stdout || ""));
|
|
2562
|
+
}
|
|
2563
|
+
function runCursorModelsCommand() {
|
|
2564
|
+
return spawnSync("cursor-agent", ["models"], {
|
|
2565
|
+
env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" },
|
|
2566
|
+
encoding: "utf8",
|
|
2567
|
+
timeout: 5e3
|
|
2568
|
+
});
|
|
2569
|
+
}
|
|
2303
2570
|
|
|
2304
2571
|
// src/drivers/gemini.ts
|
|
2305
2572
|
import { spawn as spawn5 } from "child_process";
|
|
2306
|
-
import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync3
|
|
2573
|
+
import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync3 } from "fs";
|
|
2307
2574
|
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
|
-
};
|
|
2575
|
+
function buildGeminiSpawnEnv(ctx, platform = process.platform) {
|
|
2576
|
+
const { spawnEnv } = prepareCliTransport(ctx, { NO_COLOR: "1" }, platform);
|
|
2577
|
+
if (!Object.prototype.hasOwnProperty.call(ctx.config.envVars ?? {}, "GEMINI_CLI_TRUST_WORKSPACE")) {
|
|
2578
|
+
spawnEnv.GEMINI_CLI_TRUST_WORKSPACE = "true";
|
|
2579
|
+
}
|
|
2580
|
+
return spawnEnv;
|
|
2318
2581
|
}
|
|
2319
2582
|
var GeminiDriver = class {
|
|
2320
2583
|
id = "gemini";
|
|
2584
|
+
lifecycle = {
|
|
2585
|
+
kind: "per_turn",
|
|
2586
|
+
start: "immediate",
|
|
2587
|
+
exit: "natural",
|
|
2588
|
+
inFlightWake: "spawn_new"
|
|
2589
|
+
};
|
|
2590
|
+
communication = {
|
|
2591
|
+
chat: "slock_cli",
|
|
2592
|
+
runtimeControl: "mcp_runtime_actions"
|
|
2593
|
+
};
|
|
2594
|
+
session = {
|
|
2595
|
+
recovery: "resume_or_fresh"
|
|
2596
|
+
};
|
|
2597
|
+
model = {
|
|
2598
|
+
detectedModelsVerifiedAs: "suggestion_only",
|
|
2599
|
+
toLaunchSpec: (modelId) => modelId && modelId !== "default" ? { args: ["--model", modelId] } : { args: [] }
|
|
2600
|
+
};
|
|
2321
2601
|
supportsStdinNotification = false;
|
|
2322
2602
|
mcpToolPrefix = "";
|
|
2323
2603
|
busyDeliveryMode = "none";
|
|
2604
|
+
usesSlockCliForCommunication = true;
|
|
2324
2605
|
sessionId = null;
|
|
2325
2606
|
sessionAnnounced = false;
|
|
2326
2607
|
spawn(ctx) {
|
|
2327
2608
|
this.sessionId = ctx.config.sessionId || null;
|
|
2328
2609
|
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");
|
|
2610
|
+
this.writeGeminiSettings(ctx);
|
|
2345
2611
|
const args = [
|
|
2346
2612
|
"--output-format",
|
|
2347
2613
|
"stream-json",
|
|
@@ -2415,28 +2681,64 @@ var GeminiDriver = class {
|
|
|
2415
2681
|
return null;
|
|
2416
2682
|
}
|
|
2417
2683
|
buildSystemPrompt(config, _agentId) {
|
|
2418
|
-
return
|
|
2684
|
+
return buildCliTransportSystemPrompt(config, {
|
|
2419
2685
|
toolPrefix: "",
|
|
2420
2686
|
extraCriticalRules: [
|
|
2421
|
-
"-
|
|
2687
|
+
"- 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."
|
|
2688
|
+
],
|
|
2689
|
+
postStartupNotes: [
|
|
2690
|
+
"**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
2691
|
],
|
|
2423
|
-
postStartupNotes: [],
|
|
2424
2692
|
includeStdinNotificationSection: false,
|
|
2425
2693
|
messageNotificationStyle: "poll"
|
|
2426
2694
|
});
|
|
2427
2695
|
}
|
|
2696
|
+
writeGeminiSettings(ctx) {
|
|
2697
|
+
const geminiDir = path7.join(ctx.workingDirectory, ".gemini");
|
|
2698
|
+
mkdirSync3(geminiDir, { recursive: true });
|
|
2699
|
+
const settingsPath = path7.join(geminiDir, "settings.json");
|
|
2700
|
+
writeFileSync5(settingsPath, JSON.stringify(this.buildRuntimeActionsMcpSettings(ctx)), "utf8");
|
|
2701
|
+
}
|
|
2702
|
+
buildRuntimeActionsMcpSettings(ctx) {
|
|
2703
|
+
const isTsSource = ctx.chatBridgePath.endsWith(".ts");
|
|
2704
|
+
const command = isTsSource ? "npx" : "node";
|
|
2705
|
+
const bridgeArgs = isTsSource ? ["tsx", ctx.chatBridgePath] : [ctx.chatBridgePath];
|
|
2706
|
+
return {
|
|
2707
|
+
mcpServers: {
|
|
2708
|
+
chat: {
|
|
2709
|
+
command,
|
|
2710
|
+
args: [
|
|
2711
|
+
...bridgeArgs,
|
|
2712
|
+
"--agent-id",
|
|
2713
|
+
ctx.agentId,
|
|
2714
|
+
"--server-url",
|
|
2715
|
+
ctx.config.serverUrl,
|
|
2716
|
+
"--auth-token",
|
|
2717
|
+
ctx.config.authToken || ctx.daemonApiKey,
|
|
2718
|
+
"--runtime",
|
|
2719
|
+
this.id,
|
|
2720
|
+
...ctx.launchId ? ["--launch-id", ctx.launchId] : [],
|
|
2721
|
+
"--runtime-actions-only"
|
|
2722
|
+
]
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
};
|
|
2726
|
+
}
|
|
2428
2727
|
};
|
|
2429
2728
|
|
|
2430
2729
|
// src/drivers/kimi.ts
|
|
2431
2730
|
import { randomUUID } from "crypto";
|
|
2432
2731
|
import { spawn as spawn6 } from "child_process";
|
|
2433
|
-
import { existsSync as existsSync5, readFileSync as
|
|
2434
|
-
import
|
|
2732
|
+
import { chmodSync, existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync6 } from "fs";
|
|
2733
|
+
import os3 from "os";
|
|
2435
2734
|
import path8 from "path";
|
|
2436
2735
|
var KIMI_WIRE_PROTOCOL_VERSION = "1.3";
|
|
2437
2736
|
var KIMI_SYSTEM_PROMPT_FILE = ".slock-kimi-system.md";
|
|
2438
2737
|
var KIMI_AGENT_FILE = ".slock-kimi-agent.yaml";
|
|
2439
2738
|
var KIMI_MCP_FILE = ".slock-kimi-mcp.json";
|
|
2739
|
+
var KIMI_GENERATED_CONFIG_FILE = ".slock-kimi-config.toml";
|
|
2740
|
+
var SLOCK_KIMI_CONFIG_CONTENT_ENV = "SLOCK_KIMI_CONFIG_CONTENT";
|
|
2741
|
+
var SLOCK_KIMI_CONFIG_FILE_ENV = "SLOCK_KIMI_CONFIG_FILE";
|
|
2440
2742
|
function parseToolArguments(raw) {
|
|
2441
2743
|
if (typeof raw !== "string") return raw;
|
|
2442
2744
|
try {
|
|
@@ -2445,8 +2747,109 @@ function parseToolArguments(raw) {
|
|
|
2445
2747
|
return raw;
|
|
2446
2748
|
}
|
|
2447
2749
|
}
|
|
2750
|
+
function readKimiConfigSource(home = os3.homedir(), env = process.env) {
|
|
2751
|
+
const inlineConfig = env[SLOCK_KIMI_CONFIG_CONTENT_ENV];
|
|
2752
|
+
if (inlineConfig && inlineConfig.trim()) {
|
|
2753
|
+
return {
|
|
2754
|
+
raw: inlineConfig,
|
|
2755
|
+
explicitPath: null,
|
|
2756
|
+
sourcePath: SLOCK_KIMI_CONFIG_CONTENT_ENV
|
|
2757
|
+
};
|
|
2758
|
+
}
|
|
2759
|
+
const explicitPath = env[SLOCK_KIMI_CONFIG_FILE_ENV];
|
|
2760
|
+
const configPath = explicitPath && explicitPath.trim() ? explicitPath : path8.join(home, ".kimi", "config.toml");
|
|
2761
|
+
try {
|
|
2762
|
+
return {
|
|
2763
|
+
raw: readFileSync3(configPath, "utf8"),
|
|
2764
|
+
explicitPath: explicitPath && explicitPath.trim() ? explicitPath : null,
|
|
2765
|
+
sourcePath: configPath
|
|
2766
|
+
};
|
|
2767
|
+
} catch {
|
|
2768
|
+
return {
|
|
2769
|
+
raw: null,
|
|
2770
|
+
explicitPath: explicitPath && explicitPath.trim() ? explicitPath : null,
|
|
2771
|
+
sourcePath: configPath
|
|
2772
|
+
};
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
function buildKimiSpawnEnv(env = process.env) {
|
|
2776
|
+
const spawnEnv = { ...env, FORCE_COLOR: "0", NO_COLOR: "1" };
|
|
2777
|
+
delete spawnEnv[SLOCK_KIMI_CONFIG_CONTENT_ENV];
|
|
2778
|
+
delete spawnEnv[SLOCK_KIMI_CONFIG_FILE_ENV];
|
|
2779
|
+
return spawnEnv;
|
|
2780
|
+
}
|
|
2781
|
+
function buildKimiEffectiveEnv(ctx, overrideEnv) {
|
|
2782
|
+
return {
|
|
2783
|
+
...process.env,
|
|
2784
|
+
...ctx.config.envVars || {},
|
|
2785
|
+
...overrideEnv || {}
|
|
2786
|
+
};
|
|
2787
|
+
}
|
|
2788
|
+
function buildKimiLaunchOptions(ctx, opts = {}) {
|
|
2789
|
+
const env = buildKimiEffectiveEnv(ctx, opts.env);
|
|
2790
|
+
const source = readKimiConfigSource(opts.home ?? os3.homedir(), env);
|
|
2791
|
+
const args = [];
|
|
2792
|
+
let configFilePath = null;
|
|
2793
|
+
let configContent = null;
|
|
2794
|
+
if (source.explicitPath) {
|
|
2795
|
+
configFilePath = source.explicitPath;
|
|
2796
|
+
} else if (source.raw !== null && source.sourcePath === SLOCK_KIMI_CONFIG_CONTENT_ENV) {
|
|
2797
|
+
configFilePath = path8.join(ctx.workingDirectory, KIMI_GENERATED_CONFIG_FILE);
|
|
2798
|
+
configContent = source.raw;
|
|
2799
|
+
if (opts.writeGeneratedConfig !== false) {
|
|
2800
|
+
writeFileSync6(configFilePath, source.raw, { encoding: "utf8", mode: 384 });
|
|
2801
|
+
chmodSync(configFilePath, 384);
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
if (configFilePath) {
|
|
2805
|
+
args.push("--config-file", configFilePath);
|
|
2806
|
+
}
|
|
2807
|
+
if (ctx.config.model && ctx.config.model !== "default") {
|
|
2808
|
+
args.push("--model", ctx.config.model);
|
|
2809
|
+
}
|
|
2810
|
+
return {
|
|
2811
|
+
args,
|
|
2812
|
+
env: buildKimiSpawnEnv(env),
|
|
2813
|
+
configFilePath,
|
|
2814
|
+
configContent
|
|
2815
|
+
};
|
|
2816
|
+
}
|
|
2448
2817
|
var KimiDriver = class {
|
|
2449
2818
|
id = "kimi";
|
|
2819
|
+
lifecycle = {
|
|
2820
|
+
kind: "persistent",
|
|
2821
|
+
stdin: "direct",
|
|
2822
|
+
inFlightWake: "steer"
|
|
2823
|
+
};
|
|
2824
|
+
communication = {
|
|
2825
|
+
chat: "mcp_chat_bridge",
|
|
2826
|
+
runtimeControl: "mcp_runtime_actions"
|
|
2827
|
+
};
|
|
2828
|
+
session = {
|
|
2829
|
+
recovery: "resume_or_fresh"
|
|
2830
|
+
};
|
|
2831
|
+
model = {
|
|
2832
|
+
detectedModelsVerifiedAs: "launchable",
|
|
2833
|
+
toLaunchSpec: (modelId, ctx, opts) => {
|
|
2834
|
+
if (!ctx) return { args: ["--model", modelId] };
|
|
2835
|
+
const launchCtx = {
|
|
2836
|
+
...ctx,
|
|
2837
|
+
config: {
|
|
2838
|
+
...ctx.config,
|
|
2839
|
+
model: modelId
|
|
2840
|
+
}
|
|
2841
|
+
};
|
|
2842
|
+
const launch = buildKimiLaunchOptions(launchCtx, {
|
|
2843
|
+
home: opts?.home,
|
|
2844
|
+
writeGeneratedConfig: false
|
|
2845
|
+
});
|
|
2846
|
+
return {
|
|
2847
|
+
args: launch.args,
|
|
2848
|
+
env: launch.env,
|
|
2849
|
+
configFiles: launch.configFilePath ? [launch.configFilePath] : void 0
|
|
2850
|
+
};
|
|
2851
|
+
}
|
|
2852
|
+
};
|
|
2450
2853
|
supportsStdinNotification = true;
|
|
2451
2854
|
mcpToolPrefix = "";
|
|
2452
2855
|
busyDeliveryMode = "direct";
|
|
@@ -2497,6 +2900,7 @@ var KimiDriver = class {
|
|
|
2497
2900
|
}
|
|
2498
2901
|
}
|
|
2499
2902
|
}), "utf8");
|
|
2903
|
+
const launch = buildKimiLaunchOptions(ctx);
|
|
2500
2904
|
const args = [
|
|
2501
2905
|
"--wire",
|
|
2502
2906
|
"--yolo",
|
|
@@ -2505,16 +2909,13 @@ var KimiDriver = class {
|
|
|
2505
2909
|
"--mcp-config-file",
|
|
2506
2910
|
mcpConfigPath,
|
|
2507
2911
|
"--session",
|
|
2508
|
-
this.sessionId
|
|
2912
|
+
this.sessionId,
|
|
2913
|
+
...launch.args
|
|
2509
2914
|
];
|
|
2510
|
-
if (ctx.config.model && ctx.config.model !== "default") {
|
|
2511
|
-
args.push("--model", ctx.config.model);
|
|
2512
|
-
}
|
|
2513
|
-
const spawnEnv = { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" };
|
|
2514
2915
|
const proc = spawn6("kimi", args, {
|
|
2515
2916
|
cwd: ctx.workingDirectory,
|
|
2516
2917
|
stdio: ["pipe", "pipe", "pipe"],
|
|
2517
|
-
env:
|
|
2918
|
+
env: launch.env,
|
|
2518
2919
|
shell: process.platform === "win32"
|
|
2519
2920
|
});
|
|
2520
2921
|
proc.stdin?.write(JSON.stringify({
|
|
@@ -2631,14 +3032,9 @@ var KimiDriver = class {
|
|
|
2631
3032
|
return detectKimiModels();
|
|
2632
3033
|
}
|
|
2633
3034
|
};
|
|
2634
|
-
function detectKimiModels(home =
|
|
2635
|
-
const
|
|
2636
|
-
|
|
2637
|
-
try {
|
|
2638
|
-
raw = readFileSync2(configPath, "utf8");
|
|
2639
|
-
} catch {
|
|
2640
|
-
return null;
|
|
2641
|
-
}
|
|
3035
|
+
function detectKimiModels(home = os3.homedir(), opts = {}) {
|
|
3036
|
+
const raw = readKimiConfigSource(home, opts.env).raw;
|
|
3037
|
+
if (raw === null) return null;
|
|
2642
3038
|
const models = [];
|
|
2643
3039
|
const sectionRe = /^\s*\[models(?:\.([^\]]+)|"\.[^"]+"|\."[^"]+")\s*\]\s*$/gm;
|
|
2644
3040
|
const lineRe = /^\s*\[models\.(.+?)\s*\]\s*$/gm;
|
|
@@ -2647,7 +3043,7 @@ function detectKimiModels(home = os2.homedir()) {
|
|
|
2647
3043
|
let key = match[1].trim();
|
|
2648
3044
|
if (key.startsWith('"') && key.endsWith('"')) key = key.slice(1, -1);
|
|
2649
3045
|
if (!key) continue;
|
|
2650
|
-
models.push({ id: key, label: key });
|
|
3046
|
+
models.push({ id: key, label: key, verified: "launchable" });
|
|
2651
3047
|
}
|
|
2652
3048
|
void sectionRe;
|
|
2653
3049
|
if (models.length === 0) return null;
|
|
@@ -2658,9 +3054,9 @@ function detectKimiModels(home = os2.homedir()) {
|
|
|
2658
3054
|
}
|
|
2659
3055
|
|
|
2660
3056
|
// src/drivers/opencode.ts
|
|
2661
|
-
import { spawn as spawn7 } from "child_process";
|
|
2662
|
-
import { readFileSync as
|
|
2663
|
-
import
|
|
3057
|
+
import { spawn as spawn7, spawnSync as spawnSync2 } from "child_process";
|
|
3058
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
3059
|
+
import os4 from "os";
|
|
2664
3060
|
import path9 from "path";
|
|
2665
3061
|
var CHAT_MCP_SERVER_NAME = "chat";
|
|
2666
3062
|
var CHAT_MCP_TOOL_PREFIX = `${CHAT_MCP_SERVER_NAME}_`;
|
|
@@ -2668,6 +3064,14 @@ var SLOCK_AGENT_NAME = "slock";
|
|
|
2668
3064
|
var NO_MESSAGE_PROMPT = "No new messages are pending. Stop now.";
|
|
2669
3065
|
var FIRST_MESSAGE_TASK_PREFIX = "First message task (system-triggered):";
|
|
2670
3066
|
var MIN_SUPPORTED_OPENCODE_VERSION = "1.14.30";
|
|
3067
|
+
var OPENCODE_PROVIDER_LABELS = {
|
|
3068
|
+
opencode: "OpenCode",
|
|
3069
|
+
"opencode-go": "OpenCode Go",
|
|
3070
|
+
openai: "OpenAI",
|
|
3071
|
+
openrouter: "OpenRouter",
|
|
3072
|
+
deepseek: "DeepSeek",
|
|
3073
|
+
fusecode: "FuseCode"
|
|
3074
|
+
};
|
|
2671
3075
|
function buildChatBridgeCommand(ctx) {
|
|
2672
3076
|
const isTsSource = ctx.chatBridgePath.endsWith(".ts");
|
|
2673
3077
|
return [
|
|
@@ -2700,10 +3104,10 @@ function parseUserOpenCodeConfig(ctx) {
|
|
|
2700
3104
|
const raw = ctx.config.envVars?.OPENCODE_CONFIG_CONTENT;
|
|
2701
3105
|
return parseOpenCodeConfigContent(raw);
|
|
2702
3106
|
}
|
|
2703
|
-
function readLocalOpenCodeConfig(home =
|
|
3107
|
+
function readLocalOpenCodeConfig(home = os4.homedir()) {
|
|
2704
3108
|
const configPath = path9.join(home, ".config", "opencode", "opencode.json");
|
|
2705
3109
|
try {
|
|
2706
|
-
return parseOpenCodeConfigContent(
|
|
3110
|
+
return parseOpenCodeConfigContent(readFileSync4(configPath, "utf8"));
|
|
2707
3111
|
} catch {
|
|
2708
3112
|
}
|
|
2709
3113
|
return {};
|
|
@@ -2749,7 +3153,7 @@ function mergeOpenCodeConfigs(localConfig, envConfig) {
|
|
|
2749
3153
|
}
|
|
2750
3154
|
};
|
|
2751
3155
|
}
|
|
2752
|
-
function buildOpenCodeConfig(ctx, home =
|
|
3156
|
+
function buildOpenCodeConfig(ctx, home = os4.homedir()) {
|
|
2753
3157
|
const userConfig = mergeOpenCodeConfigs(readLocalOpenCodeConfig(home), parseUserOpenCodeConfig(ctx));
|
|
2754
3158
|
const userAgents = recordField(userConfig.agent);
|
|
2755
3159
|
const userSlockAgent = recordField(userAgents[SLOCK_AGENT_NAME]);
|
|
@@ -2774,7 +3178,7 @@ function buildOpenCodeConfig(ctx, home = os3.homedir()) {
|
|
|
2774
3178
|
}
|
|
2775
3179
|
};
|
|
2776
3180
|
}
|
|
2777
|
-
function buildOpenCodeLaunchOptions(ctx, home =
|
|
3181
|
+
function buildOpenCodeLaunchOptions(ctx, home = os4.homedir()) {
|
|
2778
3182
|
const slock = prepareCliTransport(ctx, { NO_COLOR: "1" });
|
|
2779
3183
|
const config = buildOpenCodeConfig(ctx, home);
|
|
2780
3184
|
const env = {
|
|
@@ -2801,21 +3205,92 @@ function buildOpenCodeLaunchOptions(ctx, home = os3.homedir()) {
|
|
|
2801
3205
|
args.push("--", turnPrompt);
|
|
2802
3206
|
return { args, env, config };
|
|
2803
3207
|
}
|
|
2804
|
-
function
|
|
2805
|
-
const
|
|
2806
|
-
const
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
3208
|
+
function parseOpenCodeModelsOutput(output) {
|
|
3209
|
+
const stripAnsi = (value) => value.replace(/\u001b\[[0-9;]*m/g, "");
|
|
3210
|
+
const models = [];
|
|
3211
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3212
|
+
for (const rawLine of stripAnsi(output).split(/\r?\n/)) {
|
|
3213
|
+
const line = rawLine.trim();
|
|
3214
|
+
if (!line || line.startsWith("{") || line.startsWith("}") || line.startsWith('"')) continue;
|
|
3215
|
+
if (/^opencode models\b/i.test(line) || /^list all available models$/i.test(line)) continue;
|
|
3216
|
+
if (!line.includes("/") || /\s/.test(line) || line.startsWith("-")) continue;
|
|
3217
|
+
if (seen.has(line)) continue;
|
|
3218
|
+
seen.add(line);
|
|
3219
|
+
models.push({
|
|
3220
|
+
id: line,
|
|
3221
|
+
label: formatOpenCodeModelLabel(line),
|
|
3222
|
+
verified: "launchable"
|
|
3223
|
+
});
|
|
2816
3224
|
}
|
|
2817
3225
|
return models.length > 0 ? { models } : null;
|
|
2818
3226
|
}
|
|
3227
|
+
function formatOpenCodeModelLabel(modelId) {
|
|
3228
|
+
const separatorIndex = modelId.indexOf("/");
|
|
3229
|
+
if (separatorIndex <= 0 || separatorIndex === modelId.length - 1) return modelId;
|
|
3230
|
+
const providerId = modelId.slice(0, separatorIndex);
|
|
3231
|
+
const modelName = modelId.slice(separatorIndex + 1);
|
|
3232
|
+
const providerLabel = OPENCODE_PROVIDER_LABELS[providerId] || humanizeOpenCodeSegment(providerId);
|
|
3233
|
+
const modelParts = modelName.split("/");
|
|
3234
|
+
const modelLabel = humanizeOpenCodeSegment(modelParts[modelParts.length - 1] || modelName);
|
|
3235
|
+
if (modelParts.length === 1) return `${modelLabel} \xB7 ${providerLabel}`;
|
|
3236
|
+
const upstreamLabel = modelParts.slice(0, -1).map(humanizeOpenCodeSegment).join(" / ");
|
|
3237
|
+
return `${modelLabel} \xB7 ${upstreamLabel} via ${providerLabel}`;
|
|
3238
|
+
}
|
|
3239
|
+
function humanizeOpenCodeSegment(value) {
|
|
3240
|
+
return value.replace(/\[(\d+)m\]/gi, "-$1m").split(/[-_]/).filter(Boolean).map(formatOpenCodeLabelToken).join(" ");
|
|
3241
|
+
}
|
|
3242
|
+
function formatOpenCodeLabelToken(token) {
|
|
3243
|
+
const normalized = token.toLowerCase();
|
|
3244
|
+
const specialCases = {
|
|
3245
|
+
ai: "AI",
|
|
3246
|
+
api: "API",
|
|
3247
|
+
b: "B",
|
|
3248
|
+
chatgpt: "ChatGPT",
|
|
3249
|
+
claude: "Claude",
|
|
3250
|
+
codestral: "Codestral",
|
|
3251
|
+
deepseek: "DeepSeek",
|
|
3252
|
+
flash: "Flash",
|
|
3253
|
+
free: "Free",
|
|
3254
|
+
gemini: "Gemini",
|
|
3255
|
+
glm: "GLM",
|
|
3256
|
+
gpt: "GPT",
|
|
3257
|
+
hy3: "HY3",
|
|
3258
|
+
kimi: "Kimi",
|
|
3259
|
+
m: "M",
|
|
3260
|
+
minimax: "MiniMax",
|
|
3261
|
+
nano: "Nano",
|
|
3262
|
+
nemotron: "Nemotron",
|
|
3263
|
+
omni: "Omni",
|
|
3264
|
+
opus: "Opus",
|
|
3265
|
+
pro: "Pro",
|
|
3266
|
+
sonnet: "Sonnet",
|
|
3267
|
+
super: "Super"
|
|
3268
|
+
};
|
|
3269
|
+
if (specialCases[normalized]) return specialCases[normalized];
|
|
3270
|
+
if (/^v\d+(\.\d+)?$/.test(normalized)) return normalized.toUpperCase();
|
|
3271
|
+
if (/^\d+m$/i.test(token)) return token.toUpperCase();
|
|
3272
|
+
if (/^\d+[bk]$/i.test(token)) return token.toUpperCase();
|
|
3273
|
+
if (/^m\d+(\.\d+)?$/i.test(token)) return token.toUpperCase();
|
|
3274
|
+
if (/^\d/.test(token)) return token;
|
|
3275
|
+
return normalized.charAt(0).toUpperCase() + normalized.slice(1);
|
|
3276
|
+
}
|
|
3277
|
+
function detectOpenCodeModels(home = os4.homedir(), runCommand = runOpenCodeModelsCommand) {
|
|
3278
|
+
const commandResult = runCommand(home);
|
|
3279
|
+
if (commandResult.error || commandResult.status !== 0) return null;
|
|
3280
|
+
return parseOpenCodeModelsOutput(commandResult.stdout);
|
|
3281
|
+
}
|
|
3282
|
+
function runOpenCodeModelsCommand(home) {
|
|
3283
|
+
const result = spawnSync2("opencode", ["models"], {
|
|
3284
|
+
env: { ...process.env, HOME: home, FORCE_COLOR: "0", NO_COLOR: "1" },
|
|
3285
|
+
encoding: "utf8",
|
|
3286
|
+
timeout: 5e3
|
|
3287
|
+
});
|
|
3288
|
+
return {
|
|
3289
|
+
status: result.status,
|
|
3290
|
+
stdout: String(result.stdout || ""),
|
|
3291
|
+
error: result.error
|
|
3292
|
+
};
|
|
3293
|
+
}
|
|
2819
3294
|
function isSystemFirstMessageTask(message) {
|
|
2820
3295
|
return message.sender_id === "system" && message.channel_type === "channel" && message.channel_name === "all" && message.content.trimStart().startsWith(FIRST_MESSAGE_TASK_PREFIX);
|
|
2821
3296
|
}
|
|
@@ -2834,6 +3309,38 @@ function buildOpenCodeSystemPrompt(config) {
|
|
|
2834
3309
|
}
|
|
2835
3310
|
var OpenCodeDriver = class {
|
|
2836
3311
|
id = "opencode";
|
|
3312
|
+
lifecycle = {
|
|
3313
|
+
kind: "per_turn",
|
|
3314
|
+
start: "defer_until_concrete_message",
|
|
3315
|
+
exit: "terminate_on_turn_end",
|
|
3316
|
+
inFlightWake: "coalesce_into_pending"
|
|
3317
|
+
};
|
|
3318
|
+
communication = {
|
|
3319
|
+
chat: "slock_cli",
|
|
3320
|
+
runtimeControl: "mcp_runtime_actions"
|
|
3321
|
+
};
|
|
3322
|
+
session = {
|
|
3323
|
+
recovery: "resume_or_fresh"
|
|
3324
|
+
};
|
|
3325
|
+
model = {
|
|
3326
|
+
detectedModelsVerifiedAs: "launchable",
|
|
3327
|
+
toLaunchSpec: (modelId, ctx, opts) => {
|
|
3328
|
+
if (!ctx) return { args: ["--model", modelId] };
|
|
3329
|
+
const launchCtx = {
|
|
3330
|
+
...ctx,
|
|
3331
|
+
config: {
|
|
3332
|
+
...ctx.config,
|
|
3333
|
+
model: modelId
|
|
3334
|
+
}
|
|
3335
|
+
};
|
|
3336
|
+
const launch = buildOpenCodeLaunchOptions(launchCtx, opts?.home);
|
|
3337
|
+
return {
|
|
3338
|
+
args: launch.args,
|
|
3339
|
+
env: launch.env,
|
|
3340
|
+
config: launch.config
|
|
3341
|
+
};
|
|
3342
|
+
}
|
|
3343
|
+
};
|
|
2837
3344
|
supportsStdinNotification = false;
|
|
2838
3345
|
mcpToolPrefix = CHAT_MCP_TOOL_PREFIX;
|
|
2839
3346
|
busyDeliveryMode = "none";
|
|
@@ -3059,7 +3566,53 @@ async function deleteWorkspaceDirectory(dataDir, directoryName) {
|
|
|
3059
3566
|
}
|
|
3060
3567
|
|
|
3061
3568
|
// src/agentProcessManager.ts
|
|
3062
|
-
var DATA_DIR = path11.join(
|
|
3569
|
+
var DATA_DIR = path11.join(os5.homedir(), ".slock", "agents");
|
|
3570
|
+
var DEFAULT_MAX_CONCURRENT_AGENT_STARTS = 5;
|
|
3571
|
+
var DEFAULT_AGENT_START_INTERVAL_MS = 500;
|
|
3572
|
+
var WORKSPACE_TEXT_FILE_MAX_BYTES = 1048576;
|
|
3573
|
+
var WORKSPACE_IMAGE_PREVIEW_MAX_BYTES = 5 * 1024 * 1024;
|
|
3574
|
+
var WORKSPACE_TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
3575
|
+
".md",
|
|
3576
|
+
".txt",
|
|
3577
|
+
".json",
|
|
3578
|
+
".js",
|
|
3579
|
+
".ts",
|
|
3580
|
+
".jsx",
|
|
3581
|
+
".tsx",
|
|
3582
|
+
".yaml",
|
|
3583
|
+
".yml",
|
|
3584
|
+
".toml",
|
|
3585
|
+
".log",
|
|
3586
|
+
".csv",
|
|
3587
|
+
".xml",
|
|
3588
|
+
".html",
|
|
3589
|
+
".css",
|
|
3590
|
+
".sh",
|
|
3591
|
+
".py"
|
|
3592
|
+
]);
|
|
3593
|
+
var WORKSPACE_IMAGE_MIME_BY_EXTENSION = {
|
|
3594
|
+
".apng": "image/apng",
|
|
3595
|
+
".avif": "image/avif",
|
|
3596
|
+
".gif": "image/gif",
|
|
3597
|
+
".jpg": "image/jpeg",
|
|
3598
|
+
".jpeg": "image/jpeg",
|
|
3599
|
+
".png": "image/png",
|
|
3600
|
+
".webp": "image/webp"
|
|
3601
|
+
};
|
|
3602
|
+
function readPositiveIntegerEnv(name, fallback) {
|
|
3603
|
+
const raw = process.env[name];
|
|
3604
|
+
if (!raw) return fallback;
|
|
3605
|
+
const parsed = Number(raw);
|
|
3606
|
+
if (!Number.isFinite(parsed) || parsed < 1) return fallback;
|
|
3607
|
+
return Math.floor(parsed);
|
|
3608
|
+
}
|
|
3609
|
+
function readNonNegativeIntegerEnv(name, fallback) {
|
|
3610
|
+
const raw = process.env[name];
|
|
3611
|
+
if (!raw) return fallback;
|
|
3612
|
+
const parsed = Number(raw);
|
|
3613
|
+
if (!Number.isFinite(parsed) || parsed < 0) return fallback;
|
|
3614
|
+
return Math.floor(parsed);
|
|
3615
|
+
}
|
|
3063
3616
|
function toLocalTime(iso) {
|
|
3064
3617
|
const d = new Date(iso);
|
|
3065
3618
|
if (isNaN(d.getTime())) return iso;
|
|
@@ -3085,6 +3638,7 @@ function formatMessageTarget(message) {
|
|
|
3085
3638
|
function getMessageShortId(messageId) {
|
|
3086
3639
|
return messageId.startsWith("thread-") ? messageId.slice(7) : messageId.slice(0, 8);
|
|
3087
3640
|
}
|
|
3641
|
+
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
3642
|
function findSessionJsonl(root, predicate) {
|
|
3089
3643
|
let visited = 0;
|
|
3090
3644
|
const maxEntries = 1e4;
|
|
@@ -3093,7 +3647,7 @@ function findSessionJsonl(root, predicate) {
|
|
|
3093
3647
|
if (depth < 0 || visited >= maxEntries) return null;
|
|
3094
3648
|
let entries;
|
|
3095
3649
|
try {
|
|
3096
|
-
entries =
|
|
3650
|
+
entries = readdirSync2(dir, { withFileTypes: true }).sort((a, b) => b.name.localeCompare(a.name));
|
|
3097
3651
|
} catch {
|
|
3098
3652
|
return null;
|
|
3099
3653
|
}
|
|
@@ -3139,11 +3693,11 @@ function writeRuntimeSessionHandoff(runtime, sessionId, fallbackDir) {
|
|
|
3139
3693
|
return null;
|
|
3140
3694
|
}
|
|
3141
3695
|
}
|
|
3142
|
-
function resolveRuntimeSessionRef(runtime, sessionId, homeDir =
|
|
3696
|
+
function resolveRuntimeSessionRef(runtime, sessionId, homeDir = os5.homedir(), fallbackDir) {
|
|
3143
3697
|
const directPath = path11.isAbsolute(sessionId) ? sessionId : null;
|
|
3144
3698
|
if (directPath) {
|
|
3145
3699
|
try {
|
|
3146
|
-
if (
|
|
3700
|
+
if (statSync2(directPath).isFile()) {
|
|
3147
3701
|
return { label: sessionId, path: directPath, runtime, reachable: true };
|
|
3148
3702
|
}
|
|
3149
3703
|
} catch {
|
|
@@ -3239,6 +3793,16 @@ function formatRuntimeProfileControlPrompt(messages) {
|
|
|
3239
3793
|
return null;
|
|
3240
3794
|
}
|
|
3241
3795
|
const body = controls.map(({ message }) => message.content).join("\n\n---\n\n");
|
|
3796
|
+
const hasMigration = controls.some(({ notification }) => notification?.kind === "migration");
|
|
3797
|
+
if (!hasMigration) {
|
|
3798
|
+
return [
|
|
3799
|
+
"Runtime Profile daemon release notice.",
|
|
3800
|
+
"",
|
|
3801
|
+
"Read the notice below before continuing. No chat reply or runtime control action is required for this notice \u2014 resume normal inbox processing afterward.",
|
|
3802
|
+
"",
|
|
3803
|
+
body
|
|
3804
|
+
].join("\n");
|
|
3805
|
+
}
|
|
3242
3806
|
return [
|
|
3243
3807
|
"Runtime Profile control notice.",
|
|
3244
3808
|
"",
|
|
@@ -3744,6 +4308,15 @@ function classifyTerminalFailure(ap) {
|
|
|
3744
4308
|
}
|
|
3745
4309
|
return null;
|
|
3746
4310
|
}
|
|
4311
|
+
function hasDirectStdinRecoveryEvidence(ap) {
|
|
4312
|
+
const candidates = [
|
|
4313
|
+
ap.lastRuntimeError,
|
|
4314
|
+
...ap.recentStderr
|
|
4315
|
+
].filter((value) => !!value);
|
|
4316
|
+
return candidates.some(
|
|
4317
|
+
(text) => /write_stdin failed|stdin is closed|closed for this session|session.*closed/i.test(text)
|
|
4318
|
+
);
|
|
4319
|
+
}
|
|
3747
4320
|
function isMissingResumeSession(ap) {
|
|
3748
4321
|
if (!ap.sessionId) return false;
|
|
3749
4322
|
const candidates = [
|
|
@@ -3760,6 +4333,66 @@ function isMissingResumeSession(ap) {
|
|
|
3760
4333
|
}
|
|
3761
4334
|
return false;
|
|
3762
4335
|
}
|
|
4336
|
+
function classifyActivityDetailForTrace(detail) {
|
|
4337
|
+
if (!detail) return void 0;
|
|
4338
|
+
if (detail === "Message received") return "message_received";
|
|
4339
|
+
if (detail === "Starting\u2026") return "starting";
|
|
4340
|
+
if (detail === "Running command\u2026") return "running_command";
|
|
4341
|
+
if (detail === "Checking messages\u2026") return "checking_messages";
|
|
4342
|
+
if (detail === "Compacting context") return "compacting_context";
|
|
4343
|
+
if (detail === "Context compaction finished") return "compaction_finished";
|
|
4344
|
+
if (detail === "Context compaction still running; no finish event observed") return "compaction_stale";
|
|
4345
|
+
if (detail === "Idle" || detail === "Process idle") return "idle";
|
|
4346
|
+
if (detail.startsWith("Restarting stalled ") && detail.endsWith(" runtime for queued message")) return "stalled_recovery";
|
|
4347
|
+
if (detail.startsWith("Runtime stalled: no runtime events for ")) return "runtime_stalled";
|
|
4348
|
+
return "other";
|
|
4349
|
+
}
|
|
4350
|
+
function buildRuntimeStallDiagnostic(ap, staleForMs, staleForMinutes) {
|
|
4351
|
+
const context = [];
|
|
4352
|
+
if (ap.lastActivityDetail) {
|
|
4353
|
+
context.push(`after ${ap.lastActivityDetail}`);
|
|
4354
|
+
}
|
|
4355
|
+
if (ap.driver.busyDeliveryMode === "gated") {
|
|
4356
|
+
context.push(`phase=${ap.gatedSteering.phase}`);
|
|
4357
|
+
}
|
|
4358
|
+
if (ap.gatedSteering.outstandingToolUses > 0) {
|
|
4359
|
+
context.push(`tools=${ap.gatedSteering.outstandingToolUses}`);
|
|
4360
|
+
}
|
|
4361
|
+
if (ap.gatedSteering.compacting) {
|
|
4362
|
+
context.push("compacting");
|
|
4363
|
+
}
|
|
4364
|
+
if (ap.inbox.length > 0) {
|
|
4365
|
+
context.push(`queued=${ap.inbox.length}`);
|
|
4366
|
+
}
|
|
4367
|
+
const detail = [
|
|
4368
|
+
`Runtime stalled: no runtime events for ${staleForMinutes}m`,
|
|
4369
|
+
context.length > 0 ? ` (${context.join(", ")})` : ""
|
|
4370
|
+
].join("");
|
|
4371
|
+
return {
|
|
4372
|
+
detail,
|
|
4373
|
+
traceAttrs: {
|
|
4374
|
+
ageMs: staleForMs,
|
|
4375
|
+
staleForMinutes,
|
|
4376
|
+
lastActivity: ap.lastActivity,
|
|
4377
|
+
lastActivityDetailPresent: Boolean(ap.lastActivityDetail),
|
|
4378
|
+
lastActivityDetailKind: classifyActivityDetailForTrace(ap.lastActivityDetail),
|
|
4379
|
+
runtime: ap.config.runtime,
|
|
4380
|
+
model: ap.config.model,
|
|
4381
|
+
launchId: ap.launchId || void 0,
|
|
4382
|
+
sessionIdPresent: Boolean(ap.sessionId),
|
|
4383
|
+
inboxCount: ap.inbox.length,
|
|
4384
|
+
pendingNotificationCount: ap.pendingNotificationCount,
|
|
4385
|
+
processPidPresent: typeof ap.process.pid === "number",
|
|
4386
|
+
busyDeliveryMode: ap.driver.busyDeliveryMode,
|
|
4387
|
+
supportsStdinNotification: ap.driver.supportsStdinNotification,
|
|
4388
|
+
gatedPhase: ap.driver.busyDeliveryMode === "gated" ? ap.gatedSteering.phase : void 0,
|
|
4389
|
+
outstandingToolUses: ap.gatedSteering.outstandingToolUses,
|
|
4390
|
+
compacting: ap.gatedSteering.compacting,
|
|
4391
|
+
recentStderrCount: ap.recentStderr.length,
|
|
4392
|
+
recentStdoutCount: ap.recentStdout.length
|
|
4393
|
+
}
|
|
4394
|
+
};
|
|
4395
|
+
}
|
|
3763
4396
|
function getMessageDeliveryText(driver) {
|
|
3764
4397
|
return driver.supportsStdinNotification ? "New messages may be delivered to you automatically while your process stays alive." : "The daemon will automatically restart you when new messages arrive.";
|
|
3765
4398
|
}
|
|
@@ -3783,6 +4416,14 @@ var AgentProcessManager = class _AgentProcessManager {
|
|
|
3783
4416
|
agents = /* @__PURE__ */ new Map();
|
|
3784
4417
|
agentsStarting = /* @__PURE__ */ new Set();
|
|
3785
4418
|
// Prevent concurrent starts of same agent
|
|
4419
|
+
queuedAgentStarts = /* @__PURE__ */ new Map();
|
|
4420
|
+
agentStartQueue = [];
|
|
4421
|
+
activeAgentStartCount = 0;
|
|
4422
|
+
agentStartPumpTimer = null;
|
|
4423
|
+
lastAgentStartAt = 0;
|
|
4424
|
+
lastAgentStartAgentId = null;
|
|
4425
|
+
maxConcurrentAgentStarts;
|
|
4426
|
+
agentStartIntervalMs;
|
|
3786
4427
|
startingInboxes = /* @__PURE__ */ new Map();
|
|
3787
4428
|
/** Cached configs for agents whose process exited normally — enables auto-restart on next message */
|
|
3788
4429
|
idleAgentConfigs = /* @__PURE__ */ new Map();
|
|
@@ -3796,6 +4437,7 @@ var AgentProcessManager = class _AgentProcessManager {
|
|
|
3796
4437
|
driverResolver;
|
|
3797
4438
|
defaultAgentEnvVarsProvider;
|
|
3798
4439
|
tracer;
|
|
4440
|
+
deliveryTraceContexts = /* @__PURE__ */ new WeakMap();
|
|
3799
4441
|
constructor(chatBridgePath, sendToServer, daemonApiKey, opts) {
|
|
3800
4442
|
this.chatBridgePath = chatBridgePath;
|
|
3801
4443
|
this.slockCliPath = opts.slockCliPath ?? "";
|
|
@@ -3803,21 +4445,248 @@ var AgentProcessManager = class _AgentProcessManager {
|
|
|
3803
4445
|
this.daemonApiKey = daemonApiKey;
|
|
3804
4446
|
this.serverUrl = opts.serverUrl;
|
|
3805
4447
|
this.dataDir = opts.dataDir || DATA_DIR;
|
|
3806
|
-
this.runtimeSessionHomeDir = opts.runtimeSessionHomeDir ||
|
|
4448
|
+
this.runtimeSessionHomeDir = opts.runtimeSessionHomeDir || os5.homedir();
|
|
3807
4449
|
this.driverResolver = opts.driverResolver || getDriver;
|
|
3808
4450
|
this.defaultAgentEnvVarsProvider = opts.defaultAgentEnvVarsProvider || null;
|
|
3809
4451
|
this.tracer = opts.tracer ?? noopTracer;
|
|
4452
|
+
this.maxConcurrentAgentStarts = Math.max(
|
|
4453
|
+
1,
|
|
4454
|
+
Math.floor(
|
|
4455
|
+
opts.runtimeStartScheduler?.maxConcurrentStarts ?? readPositiveIntegerEnv("SLOCK_DAEMON_MAX_CONCURRENT_AGENT_STARTS", DEFAULT_MAX_CONCURRENT_AGENT_STARTS)
|
|
4456
|
+
)
|
|
4457
|
+
);
|
|
4458
|
+
this.agentStartIntervalMs = Math.max(
|
|
4459
|
+
0,
|
|
4460
|
+
Math.floor(
|
|
4461
|
+
opts.runtimeStartScheduler?.minStartIntervalMs ?? readNonNegativeIntegerEnv("SLOCK_DAEMON_AGENT_START_INTERVAL_MS", DEFAULT_AGENT_START_INTERVAL_MS)
|
|
4462
|
+
)
|
|
4463
|
+
);
|
|
3810
4464
|
}
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
4465
|
+
setTracer(tracer) {
|
|
4466
|
+
this.tracer = tracer;
|
|
4467
|
+
}
|
|
4468
|
+
recordDaemonTrace(name, attrs, status = "ok", parentTraceparent) {
|
|
4469
|
+
const span = this.tracer.startSpan(name, {
|
|
4470
|
+
parent: parseTraceparent(parentTraceparent),
|
|
4471
|
+
surface: "daemon",
|
|
4472
|
+
kind: "internal",
|
|
4473
|
+
attrs
|
|
4474
|
+
});
|
|
4475
|
+
span.end(status);
|
|
4476
|
+
}
|
|
4477
|
+
startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId) {
|
|
4478
|
+
return {
|
|
4479
|
+
agentId,
|
|
4480
|
+
launchId,
|
|
4481
|
+
runtime: config.runtime,
|
|
4482
|
+
model: config.model,
|
|
4483
|
+
session_id_present: Boolean(config.sessionId),
|
|
4484
|
+
launch_id_present: Boolean(launchId),
|
|
4485
|
+
wake_message_present: Boolean(wakeMessage),
|
|
4486
|
+
unread_channels_count: unreadSummary ? Object.keys(unreadSummary).length : 0,
|
|
4487
|
+
resume_prompt_present: Boolean(resumePrompt),
|
|
4488
|
+
queue_depth: this.agentStartQueue.length,
|
|
4489
|
+
active_starts: this.activeAgentStartCount,
|
|
4490
|
+
max_concurrent_starts: this.maxConcurrentAgentStarts,
|
|
4491
|
+
min_start_interval_ms: this.agentStartIntervalMs
|
|
4492
|
+
};
|
|
4493
|
+
}
|
|
4494
|
+
getDeliveryTraceContext(message) {
|
|
4495
|
+
return this.deliveryTraceContexts.get(message) ?? {};
|
|
4496
|
+
}
|
|
4497
|
+
deliveryTraceAttrs(agentId, message, attrs = {}) {
|
|
4498
|
+
const context = this.getDeliveryTraceContext(message);
|
|
4499
|
+
const deliveryCorrelationId = context.deliveryId ?? message.message_id;
|
|
4500
|
+
return {
|
|
4501
|
+
agentId,
|
|
4502
|
+
deliveryId: context.deliveryId,
|
|
4503
|
+
delivery_correlation_id: deliveryCorrelationId,
|
|
4504
|
+
channel_type: message.channel_type,
|
|
4505
|
+
sender_type: message.sender_type,
|
|
4506
|
+
messageId: message.message_id,
|
|
4507
|
+
message_id_present: Boolean(message.message_id),
|
|
4508
|
+
...attrs
|
|
4509
|
+
};
|
|
4510
|
+
}
|
|
4511
|
+
async startAgent(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId) {
|
|
4512
|
+
this.recordDaemonTrace("daemon.agent.start.requested", this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId));
|
|
4513
|
+
if (this.agents.has(agentId)) {
|
|
4514
|
+
this.recordDaemonTrace("daemon.agent.start.ignored", {
|
|
4515
|
+
...this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId),
|
|
4516
|
+
reason: "already_running"
|
|
4517
|
+
});
|
|
4518
|
+
logger.info(`[Agent ${agentId}] Start ignored (already running)`);
|
|
4519
|
+
return;
|
|
4520
|
+
}
|
|
3816
4521
|
if (this.agentsStarting.has(agentId)) {
|
|
4522
|
+
this.recordDaemonTrace("daemon.agent.start.ignored", {
|
|
4523
|
+
...this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId),
|
|
4524
|
+
reason: "already_starting"
|
|
4525
|
+
});
|
|
4526
|
+
logger.info(`[Agent ${agentId}] Start ignored (startup in progress)`);
|
|
4527
|
+
return;
|
|
4528
|
+
}
|
|
4529
|
+
if (this.queuedAgentStarts.has(agentId)) {
|
|
4530
|
+
this.recordDaemonTrace("daemon.agent.start.ignored", {
|
|
4531
|
+
...this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId),
|
|
4532
|
+
reason: "already_queued"
|
|
4533
|
+
});
|
|
4534
|
+
logger.info(`[Agent ${agentId}] Start ignored (startup already queued)`);
|
|
4535
|
+
return;
|
|
4536
|
+
}
|
|
4537
|
+
return new Promise((resolve, reject) => {
|
|
4538
|
+
const item = {
|
|
4539
|
+
agentId,
|
|
4540
|
+
config,
|
|
4541
|
+
wakeMessage,
|
|
4542
|
+
unreadSummary,
|
|
4543
|
+
resumePrompt,
|
|
4544
|
+
launchId,
|
|
4545
|
+
resolve,
|
|
4546
|
+
reject
|
|
4547
|
+
};
|
|
4548
|
+
this.agentStartQueue.push(item);
|
|
4549
|
+
this.queuedAgentStarts.set(agentId, item);
|
|
4550
|
+
this.recordDaemonTrace("daemon.agent.start.queued", this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId));
|
|
4551
|
+
logger.info(
|
|
4552
|
+
`[Agent ${agentId}] Start queued (queue=${this.agentStartQueue.length}, active=${this.activeAgentStartCount}, max=${this.maxConcurrentAgentStarts}, interval=${this.agentStartIntervalMs}ms)`
|
|
4553
|
+
);
|
|
4554
|
+
this.pumpAgentStartQueue();
|
|
4555
|
+
});
|
|
4556
|
+
}
|
|
4557
|
+
pumpAgentStartQueue() {
|
|
4558
|
+
if (this.agentStartPumpTimer) return;
|
|
4559
|
+
if (this.agentStartQueue.length === 0) return;
|
|
4560
|
+
if (this.activeAgentStartCount >= this.maxConcurrentAgentStarts) return;
|
|
4561
|
+
const next = this.agentStartQueue[0];
|
|
4562
|
+
const shouldRateLimit = next ? next.agentId !== this.lastAgentStartAgentId : true;
|
|
4563
|
+
const elapsed = Date.now() - this.lastAgentStartAt;
|
|
4564
|
+
const waitMs = shouldRateLimit ? Math.max(0, this.agentStartIntervalMs - elapsed) : 0;
|
|
4565
|
+
if (waitMs > 0) {
|
|
4566
|
+
this.recordDaemonTrace("daemon.agent.start.rate_limited", {
|
|
4567
|
+
...this.startQueueTraceAttrs(next.agentId, next.config, next.wakeMessage, next.unreadSummary, next.resumePrompt, next.launchId),
|
|
4568
|
+
wait_ms: waitMs
|
|
4569
|
+
});
|
|
4570
|
+
this.agentStartPumpTimer = setTimeout(() => {
|
|
4571
|
+
this.agentStartPumpTimer = null;
|
|
4572
|
+
this.pumpAgentStartQueue();
|
|
4573
|
+
}, waitMs);
|
|
4574
|
+
return;
|
|
4575
|
+
}
|
|
4576
|
+
const item = this.agentStartQueue.shift();
|
|
4577
|
+
if (!item) return;
|
|
4578
|
+
if (this.queuedAgentStarts.get(item.agentId) !== item) {
|
|
4579
|
+
this.recordDaemonTrace("daemon.agent.start.skipped", {
|
|
4580
|
+
...this.startQueueTraceAttrs(item.agentId, item.config, item.wakeMessage, item.unreadSummary, item.resumePrompt, item.launchId),
|
|
4581
|
+
reason: "stale_queue_item"
|
|
4582
|
+
});
|
|
4583
|
+
this.pumpAgentStartQueue();
|
|
4584
|
+
return;
|
|
4585
|
+
}
|
|
4586
|
+
this.queuedAgentStarts.delete(item.agentId);
|
|
4587
|
+
if (this.agents.has(item.agentId) || this.agentsStarting.has(item.agentId)) {
|
|
4588
|
+
this.recordDaemonTrace("daemon.agent.start.skipped", {
|
|
4589
|
+
...this.startQueueTraceAttrs(item.agentId, item.config, item.wakeMessage, item.unreadSummary, item.resumePrompt, item.launchId),
|
|
4590
|
+
reason: "already_running_or_starting"
|
|
4591
|
+
});
|
|
4592
|
+
logger.info(`[Agent ${item.agentId}] Queued start skipped (already running or starting)`);
|
|
4593
|
+
item.resolve();
|
|
4594
|
+
this.pumpAgentStartQueue();
|
|
4595
|
+
return;
|
|
4596
|
+
}
|
|
4597
|
+
this.activeAgentStartCount++;
|
|
4598
|
+
this.lastAgentStartAt = Date.now();
|
|
4599
|
+
this.lastAgentStartAgentId = item.agentId;
|
|
4600
|
+
logger.info(
|
|
4601
|
+
`[Agent ${item.agentId}] Dequeued start (remaining=${this.agentStartQueue.length}, active=${this.activeAgentStartCount})`
|
|
4602
|
+
);
|
|
4603
|
+
this.recordDaemonTrace("daemon.agent.start.dequeued", this.startQueueTraceAttrs(item.agentId, item.config, item.wakeMessage, item.unreadSummary, item.resumePrompt, item.launchId));
|
|
4604
|
+
this.startAgentNow(
|
|
4605
|
+
item.agentId,
|
|
4606
|
+
item.config,
|
|
4607
|
+
item.wakeMessage,
|
|
4608
|
+
item.unreadSummary,
|
|
4609
|
+
item.resumePrompt,
|
|
4610
|
+
item.launchId
|
|
4611
|
+
).then(() => {
|
|
4612
|
+
this.releaseAgentStartSlot(item.agentId, "spawn attempted");
|
|
4613
|
+
item.resolve();
|
|
4614
|
+
}, (err) => {
|
|
4615
|
+
this.releaseAgentStartSlot(item.agentId, "start failed");
|
|
4616
|
+
item.reject(err);
|
|
4617
|
+
});
|
|
4618
|
+
}
|
|
4619
|
+
releaseAgentStartSlot(agentId, reason) {
|
|
4620
|
+
if (this.activeAgentStartCount <= 0) return;
|
|
4621
|
+
this.activeAgentStartCount = Math.max(0, this.activeAgentStartCount - 1);
|
|
4622
|
+
this.recordDaemonTrace("daemon.agent.start.slot_released", {
|
|
4623
|
+
agentId,
|
|
4624
|
+
reason,
|
|
4625
|
+
active_starts: this.activeAgentStartCount,
|
|
4626
|
+
queue_depth: this.agentStartQueue.length,
|
|
4627
|
+
max_concurrent_starts: this.maxConcurrentAgentStarts
|
|
4628
|
+
});
|
|
4629
|
+
logger.info(
|
|
4630
|
+
`[Agent ${agentId}] Start slot released (${reason}) (active=${this.activeAgentStartCount}, queue=${this.agentStartQueue.length})`
|
|
4631
|
+
);
|
|
4632
|
+
this.pumpAgentStartQueue();
|
|
4633
|
+
}
|
|
4634
|
+
cancelQueuedAgentStart(agentId, reason) {
|
|
4635
|
+
const item = this.queuedAgentStarts.get(agentId);
|
|
4636
|
+
if (!item) return false;
|
|
4637
|
+
this.queuedAgentStarts.delete(agentId);
|
|
4638
|
+
this.agentStartQueue = this.agentStartQueue.filter((candidate) => candidate !== item);
|
|
4639
|
+
this.startingInboxes.delete(agentId);
|
|
4640
|
+
if (this.agentStartQueue.length === 0 && this.agentStartPumpTimer) {
|
|
4641
|
+
clearTimeout(this.agentStartPumpTimer);
|
|
4642
|
+
this.agentStartPumpTimer = null;
|
|
4643
|
+
}
|
|
4644
|
+
this.recordDaemonTrace("daemon.agent.start.cancelled", {
|
|
4645
|
+
...this.startQueueTraceAttrs(agentId, item.config, item.wakeMessage, item.unreadSummary, item.resumePrompt, item.launchId),
|
|
4646
|
+
reason
|
|
4647
|
+
}, "cancelled");
|
|
4648
|
+
logger.info(`[Agent ${agentId}] Queued start cancelled (${reason})`);
|
|
4649
|
+
item.resolve();
|
|
4650
|
+
return true;
|
|
4651
|
+
}
|
|
4652
|
+
cancelAllQueuedAgentStarts(reason) {
|
|
4653
|
+
for (const item of this.agentStartQueue) {
|
|
4654
|
+
if (this.queuedAgentStarts.get(item.agentId) === item) {
|
|
4655
|
+
this.recordDaemonTrace("daemon.agent.start.cancelled", {
|
|
4656
|
+
...this.startQueueTraceAttrs(item.agentId, item.config, item.wakeMessage, item.unreadSummary, item.resumePrompt, item.launchId),
|
|
4657
|
+
reason
|
|
4658
|
+
}, "cancelled");
|
|
4659
|
+
logger.info(`[Agent ${item.agentId}] Queued start cancelled (${reason})`);
|
|
4660
|
+
item.resolve();
|
|
4661
|
+
}
|
|
4662
|
+
}
|
|
4663
|
+
this.agentStartQueue = [];
|
|
4664
|
+
this.queuedAgentStarts.clear();
|
|
4665
|
+
this.startingInboxes.clear();
|
|
4666
|
+
if (this.agentStartPumpTimer) {
|
|
4667
|
+
clearTimeout(this.agentStartPumpTimer);
|
|
4668
|
+
this.agentStartPumpTimer = null;
|
|
4669
|
+
}
|
|
4670
|
+
}
|
|
4671
|
+
async startAgentNow(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId) {
|
|
4672
|
+
if (this.agents.has(agentId)) {
|
|
4673
|
+
this.recordDaemonTrace("daemon.agent.spawn.skipped", {
|
|
4674
|
+
...this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId),
|
|
4675
|
+
reason: "already_running"
|
|
4676
|
+
});
|
|
4677
|
+
logger.info(`[Agent ${agentId}] Start ignored (already running)`);
|
|
4678
|
+
return;
|
|
4679
|
+
}
|
|
4680
|
+
if (this.agentsStarting.has(agentId)) {
|
|
4681
|
+
this.recordDaemonTrace("daemon.agent.spawn.skipped", {
|
|
4682
|
+
...this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId),
|
|
4683
|
+
reason: "already_starting"
|
|
4684
|
+
});
|
|
3817
4685
|
logger.info(`[Agent ${agentId}] Start ignored (startup in progress)`);
|
|
3818
4686
|
return;
|
|
3819
4687
|
}
|
|
3820
4688
|
this.agentsStarting.add(agentId);
|
|
4689
|
+
this.recordDaemonTrace("daemon.agent.spawn.started", this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId));
|
|
3821
4690
|
try {
|
|
3822
4691
|
const driver = this.driverResolver(config.runtime || "claude");
|
|
3823
4692
|
const agentDataDir = path11.join(this.dataDir, agentId);
|
|
@@ -3877,6 +4746,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up, or respond to t
|
|
|
3877
4746
|
prompt += `
|
|
3878
4747
|
|
|
3879
4748
|
Respond as appropriate \u2014 ${dynamicReplyInstruction(driver)}, or take action as needed. Complete ALL your work before stopping.
|
|
4749
|
+
${RESPONSE_TARGET_HINT}
|
|
3880
4750
|
|
|
3881
4751
|
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
4752
|
prompt += getBusyDeliveryNote(driver);
|
|
@@ -3909,6 +4779,11 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
|
|
|
3909
4779
|
});
|
|
3910
4780
|
this.sendAgentStatus(agentId, "active", launchId || null);
|
|
3911
4781
|
this.broadcastActivity(agentId, "online", "Process idle");
|
|
4782
|
+
this.recordDaemonTrace("daemon.agent.spawn.deferred", {
|
|
4783
|
+
...this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId),
|
|
4784
|
+
pending_messages_count: pendingMessages.length,
|
|
4785
|
+
reason: "defer_until_concrete_message"
|
|
4786
|
+
});
|
|
3912
4787
|
logger.info(`[Agent ${agentId}] Deferred ${driver.id} spawn until first concrete message`);
|
|
3913
4788
|
for (const message of pendingMessages) {
|
|
3914
4789
|
this.deliverMessage(agentId, message);
|
|
@@ -3926,6 +4801,12 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
|
|
|
3926
4801
|
daemonApiKey: this.daemonApiKey,
|
|
3927
4802
|
launchId: launchId || null
|
|
3928
4803
|
});
|
|
4804
|
+
this.recordDaemonTrace("daemon.agent.spawn.created", {
|
|
4805
|
+
...this.startQueueTraceAttrs(agentId, effectiveConfig, wakeMessage, unreadSummary, resumePrompt, launchId),
|
|
4806
|
+
detached: false,
|
|
4807
|
+
new_session: false,
|
|
4808
|
+
process_pid_present: typeof proc.pid === "number"
|
|
4809
|
+
});
|
|
3929
4810
|
const agentProcess = {
|
|
3930
4811
|
process: proc,
|
|
3931
4812
|
driver,
|
|
@@ -3947,6 +4828,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
|
|
|
3947
4828
|
runtimeTraceSpan: null,
|
|
3948
4829
|
lastActivity: "",
|
|
3949
4830
|
lastActivityDetail: "",
|
|
4831
|
+
activityClientSeq: 0,
|
|
3950
4832
|
recentStdout: [],
|
|
3951
4833
|
recentStderr: [],
|
|
3952
4834
|
lastRuntimeError: null,
|
|
@@ -3959,7 +4841,12 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
|
|
|
3959
4841
|
};
|
|
3960
4842
|
this.startingInboxes.delete(agentId);
|
|
3961
4843
|
this.agents.set(agentId, agentProcess);
|
|
3962
|
-
this.
|
|
4844
|
+
this.idleAgentConfigs.set(agentId, {
|
|
4845
|
+
config: { ...effectiveConfig, sessionId: effectiveConfig.sessionId || null },
|
|
4846
|
+
sessionId: effectiveConfig.sessionId || null,
|
|
4847
|
+
launchId: launchId || null
|
|
4848
|
+
});
|
|
4849
|
+
this.startRuntimeTrace(agentId, agentProcess, "spawn", wakeMessage ? [wakeMessage] : void 0);
|
|
3963
4850
|
this.agentsStarting.delete(agentId);
|
|
3964
4851
|
if (config.runtimeProfileControl) {
|
|
3965
4852
|
this.ackInjectedRuntimeProfileControl(agentId, config.runtimeProfileControl, agentProcess.launchId);
|
|
@@ -3998,6 +4885,13 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
|
|
|
3998
4885
|
proc.on("error", (err) => {
|
|
3999
4886
|
const current = this.agents.get(agentId);
|
|
4000
4887
|
if (current) current.spawnError = err.message;
|
|
4888
|
+
this.recordDaemonTrace("daemon.agent.process.error", {
|
|
4889
|
+
agentId,
|
|
4890
|
+
launchId: current?.launchId || void 0,
|
|
4891
|
+
runtime: config.runtime,
|
|
4892
|
+
model: config.model,
|
|
4893
|
+
error_class: err.name || typeof err
|
|
4894
|
+
}, "error");
|
|
4001
4895
|
logger.error(`[Agent ${agentId}] Process error: ${err.message}`);
|
|
4002
4896
|
});
|
|
4003
4897
|
proc.on("exit", (code, signal) => {
|
|
@@ -4006,6 +4900,18 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
|
|
|
4006
4900
|
current.exitCode = code;
|
|
4007
4901
|
current.exitSignal = signal;
|
|
4008
4902
|
}
|
|
4903
|
+
this.recordDaemonTrace("daemon.agent.process.exited", {
|
|
4904
|
+
agentId,
|
|
4905
|
+
launchId: current?.launchId || void 0,
|
|
4906
|
+
runtime: config.runtime,
|
|
4907
|
+
model: config.model,
|
|
4908
|
+
exit_code: code,
|
|
4909
|
+
exit_signal: signal,
|
|
4910
|
+
clean_exit: code === 0,
|
|
4911
|
+
runtime_trace_active: Boolean(current?.runtimeTraceSpan),
|
|
4912
|
+
inbox_count: current?.inbox.length ?? 0,
|
|
4913
|
+
pending_notification_count: current?.pendingNotificationCount ?? 0
|
|
4914
|
+
});
|
|
4009
4915
|
logger.info(`[Agent ${agentId}] Process exited with code ${code}${signal ? ` (signal ${signal})` : ""}`);
|
|
4010
4916
|
});
|
|
4011
4917
|
proc.on("close", (code, signal) => {
|
|
@@ -4069,7 +4975,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
|
|
|
4069
4975
|
}
|
|
4070
4976
|
if (processEndedCleanly) {
|
|
4071
4977
|
let queuedWakeMessage;
|
|
4072
|
-
if (!ap.driver.supportsStdinNotification) {
|
|
4978
|
+
if (!ap.driver.supportsStdinNotification || ap.expectedTerminationReason === "stalled_recovery") {
|
|
4073
4979
|
while (ap.inbox.length > 0) {
|
|
4074
4980
|
const candidate = ap.inbox.shift();
|
|
4075
4981
|
if (this.shouldDeferWakeMessage(agentId, ap.driver, candidate)) continue;
|
|
@@ -4175,7 +5081,68 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
|
|
|
4175
5081
|
}
|
|
4176
5082
|
return leftKeys.every((key) => left?.[key] === right?.[key]);
|
|
4177
5083
|
}
|
|
5084
|
+
enqueueRuntimeProfileNotification(agentId, ap, message, kind, key) {
|
|
5085
|
+
ap.inbox.push(message);
|
|
5086
|
+
if (ap.driver.supportsStdinNotification && ap.sessionId) {
|
|
5087
|
+
ap.pendingNotificationCount++;
|
|
5088
|
+
if (ap.driver.busyDeliveryMode === "gated") {
|
|
5089
|
+
this.recordGatedSteeringEvent(agentId, ap, "buffer", {
|
|
5090
|
+
reason: "runtime_profile",
|
|
5091
|
+
kind,
|
|
5092
|
+
pendingMessages: ap.inbox.length
|
|
5093
|
+
});
|
|
5094
|
+
} else if (!ap.notificationTimer) {
|
|
5095
|
+
ap.notificationTimer = setTimeout(() => {
|
|
5096
|
+
this.sendStdinNotification(agentId);
|
|
5097
|
+
}, 3e3);
|
|
5098
|
+
}
|
|
5099
|
+
}
|
|
5100
|
+
this.recordDaemonTrace("daemon.agent.runtime_profile.routed", {
|
|
5101
|
+
agentId,
|
|
5102
|
+
kind,
|
|
5103
|
+
key_present: Boolean(key),
|
|
5104
|
+
outcome: ap.sessionId ? "queued_busy" : "queued_before_session",
|
|
5105
|
+
runtime: ap.config.runtime,
|
|
5106
|
+
session_id_present: Boolean(ap.sessionId),
|
|
5107
|
+
launchId: ap.launchId || void 0,
|
|
5108
|
+
inbox_count: ap.inbox.length,
|
|
5109
|
+
pending_notification_count: ap.pendingNotificationCount,
|
|
5110
|
+
busy_delivery_mode: ap.driver.busyDeliveryMode,
|
|
5111
|
+
supports_stdin_notification: ap.driver.supportsStdinNotification
|
|
5112
|
+
});
|
|
5113
|
+
logger.info(
|
|
5114
|
+
`[Agent ${agentId}] Queued runtime profile ${kind} ${key} for ${ap.sessionId ? "busy" : "pre-session"} ${ap.driver.id} delivery`
|
|
5115
|
+
);
|
|
5116
|
+
}
|
|
5117
|
+
queueRuntimeProfileNotificationDuringStart(agentId, message, kind, key) {
|
|
5118
|
+
const pending = this.startingInboxes.get(agentId) || [];
|
|
5119
|
+
pending.push(message);
|
|
5120
|
+
this.startingInboxes.set(agentId, pending);
|
|
5121
|
+
const queuedStart = this.queuedAgentStarts.get(agentId);
|
|
5122
|
+
this.recordDaemonTrace("daemon.agent.runtime_profile.routed", {
|
|
5123
|
+
agentId,
|
|
5124
|
+
kind,
|
|
5125
|
+
key_present: Boolean(key),
|
|
5126
|
+
outcome: "queued_during_start",
|
|
5127
|
+
startup_pending: true,
|
|
5128
|
+
starting_inbox_count: pending.length,
|
|
5129
|
+
launchId: queuedStart?.launchId
|
|
5130
|
+
});
|
|
5131
|
+
logger.info(`[Agent ${agentId}] Queued runtime profile ${kind} ${key} during startup`);
|
|
5132
|
+
}
|
|
5133
|
+
splitRuntimeProfileControlBatch(messages) {
|
|
5134
|
+
const controlMessages = messages.filter((message) => runtimeProfileNotificationFromMessage(message));
|
|
5135
|
+
if (controlMessages.length === 0 || controlMessages.length === messages.length) {
|
|
5136
|
+
return { nextMessages: messages, deferredMessages: [] };
|
|
5137
|
+
}
|
|
5138
|
+
const deferredMessages = messages.filter((message) => !runtimeProfileNotificationFromMessage(message));
|
|
5139
|
+
return { nextMessages: controlMessages, deferredMessages };
|
|
5140
|
+
}
|
|
5141
|
+
containsOrdinaryInboxMessage(messages) {
|
|
5142
|
+
return messages.some((message) => !runtimeProfileNotificationFromMessage(message));
|
|
5143
|
+
}
|
|
4178
5144
|
async stopAgent(agentId, { wait = false, silent = false } = {}) {
|
|
5145
|
+
this.cancelQueuedAgentStart(agentId, "stop requested");
|
|
4179
5146
|
this.idleAgentConfigs.delete(agentId);
|
|
4180
5147
|
const ap = this.agents.get(agentId);
|
|
4181
5148
|
if (!ap) {
|
|
@@ -4222,51 +5189,155 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
|
|
|
4222
5189
|
});
|
|
4223
5190
|
}
|
|
4224
5191
|
}
|
|
4225
|
-
deliverMessage(agentId, message) {
|
|
5192
|
+
deliverMessage(agentId, message, traceContext = {}) {
|
|
5193
|
+
if (traceContext.deliveryId) {
|
|
5194
|
+
this.deliveryTraceContexts.set(message, traceContext);
|
|
5195
|
+
}
|
|
4226
5196
|
const ap = this.agents.get(agentId);
|
|
4227
5197
|
if (!ap) {
|
|
4228
|
-
if (this.agentsStarting.has(agentId)) {
|
|
5198
|
+
if (this.agentsStarting.has(agentId) || this.queuedAgentStarts.has(agentId)) {
|
|
5199
|
+
const queuedStart = this.queuedAgentStarts.get(agentId);
|
|
4229
5200
|
const pending = this.startingInboxes.get(agentId) || [];
|
|
4230
5201
|
pending.push(message);
|
|
4231
5202
|
this.startingInboxes.set(agentId, pending);
|
|
4232
|
-
|
|
5203
|
+
this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
|
|
5204
|
+
outcome: "queued_during_start",
|
|
5205
|
+
accepted: true,
|
|
5206
|
+
process_present: false,
|
|
5207
|
+
startup_pending: true,
|
|
5208
|
+
starting_inbox_count: pending.length,
|
|
5209
|
+
launchId: queuedStart?.launchId
|
|
5210
|
+
}));
|
|
5211
|
+
return true;
|
|
4233
5212
|
}
|
|
4234
5213
|
const cached = this.idleAgentConfigs.get(agentId);
|
|
4235
5214
|
if (cached) {
|
|
4236
5215
|
const driver = this.driverResolver(cached.config.runtime || "claude");
|
|
4237
5216
|
if (this.shouldDeferWakeMessage(agentId, driver, message)) {
|
|
4238
|
-
|
|
5217
|
+
this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
|
|
5218
|
+
outcome: "deferred_wake_message",
|
|
5219
|
+
accepted: true,
|
|
5220
|
+
process_present: false,
|
|
5221
|
+
cached_idle_config_present: true,
|
|
5222
|
+
runtime: cached.config.runtime,
|
|
5223
|
+
session_id_present: Boolean(cached.sessionId),
|
|
5224
|
+
launchId: cached.launchId || void 0
|
|
5225
|
+
}));
|
|
5226
|
+
return true;
|
|
4239
5227
|
}
|
|
4240
5228
|
logger.info(`[Agent ${agentId}] Starting from idle state for new message`);
|
|
4241
5229
|
this.idleAgentConfigs.delete(agentId);
|
|
5230
|
+
this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
|
|
5231
|
+
outcome: "auto_restart_from_idle",
|
|
5232
|
+
accepted: true,
|
|
5233
|
+
process_present: false,
|
|
5234
|
+
cached_idle_config_present: true,
|
|
5235
|
+
runtime: cached.config.runtime,
|
|
5236
|
+
session_id_present: Boolean(cached.sessionId),
|
|
5237
|
+
launchId: cached.launchId || void 0
|
|
5238
|
+
}));
|
|
4242
5239
|
this.startAgent(agentId, cached.config, message, void 0, void 0, cached.launchId || void 0).catch((err) => {
|
|
4243
5240
|
logger.error(`[Agent ${agentId}] Failed to auto-restart`, err);
|
|
4244
5241
|
});
|
|
5242
|
+
return true;
|
|
4245
5243
|
}
|
|
4246
|
-
|
|
5244
|
+
logger.warn(`[Agent ${agentId}] Delivery received but no running process or cached idle config exists`);
|
|
5245
|
+
this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
|
|
5246
|
+
outcome: "rejected_no_process",
|
|
5247
|
+
accepted: false,
|
|
5248
|
+
process_present: false,
|
|
5249
|
+
cached_idle_config_present: false
|
|
5250
|
+
}), "error");
|
|
5251
|
+
this.sendAgentStatus(agentId, "inactive", null);
|
|
5252
|
+
this.broadcastActivity(agentId, "offline", "Process unavailable; restart required");
|
|
5253
|
+
return false;
|
|
4247
5254
|
}
|
|
4248
5255
|
if (this.shouldDeferWakeMessage(agentId, ap.driver, message)) {
|
|
4249
|
-
|
|
5256
|
+
this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
|
|
5257
|
+
outcome: "deferred_wake_message",
|
|
5258
|
+
accepted: true,
|
|
5259
|
+
process_present: true,
|
|
5260
|
+
runtime: ap.config.runtime,
|
|
5261
|
+
session_id_present: Boolean(ap.sessionId),
|
|
5262
|
+
launchId: ap.launchId || void 0,
|
|
5263
|
+
is_idle: ap.isIdle,
|
|
5264
|
+
inbox_count: ap.inbox.length
|
|
5265
|
+
}));
|
|
5266
|
+
return true;
|
|
4250
5267
|
}
|
|
4251
5268
|
if (ap.isIdle && ap.driver.supportsStdinNotification && ap.sessionId) {
|
|
4252
5269
|
const nextMessages = ap.inbox.splice(0, ap.inbox.length);
|
|
4253
5270
|
nextMessages.push(message);
|
|
4254
5271
|
ap.isIdle = false;
|
|
4255
|
-
this.startRuntimeTrace(agentId, ap, "stdin-idle-delivery");
|
|
5272
|
+
this.startRuntimeTrace(agentId, ap, "stdin-idle-delivery", nextMessages);
|
|
4256
5273
|
this.broadcastActivity(agentId, "working", "Message received");
|
|
4257
|
-
this.deliverMessagesViaStdin(agentId, ap, nextMessages, "idle");
|
|
4258
|
-
|
|
5274
|
+
const stdinAccepted = this.deliverMessagesViaStdin(agentId, ap, nextMessages, "idle");
|
|
5275
|
+
this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
|
|
5276
|
+
outcome: "stdin_idle_delivery",
|
|
5277
|
+
accepted: true,
|
|
5278
|
+
process_present: true,
|
|
5279
|
+
runtime: ap.config.runtime,
|
|
5280
|
+
session_id_present: true,
|
|
5281
|
+
launchId: ap.launchId || void 0,
|
|
5282
|
+
stdin_delivery_accepted: stdinAccepted,
|
|
5283
|
+
delivered_messages_count: nextMessages.length
|
|
5284
|
+
}));
|
|
5285
|
+
return true;
|
|
4259
5286
|
}
|
|
4260
5287
|
ap.inbox.push(message);
|
|
4261
|
-
if (
|
|
4262
|
-
|
|
5288
|
+
if (this.recoverStaleProcessForQueuedMessageIfNeeded(agentId, ap)) {
|
|
5289
|
+
this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
|
|
5290
|
+
outcome: "queued_stalled_recovery",
|
|
5291
|
+
accepted: true,
|
|
5292
|
+
process_present: true,
|
|
5293
|
+
runtime: ap.config.runtime,
|
|
5294
|
+
session_id_present: Boolean(ap.sessionId),
|
|
5295
|
+
launchId: ap.launchId || void 0,
|
|
5296
|
+
inbox_count: ap.inbox.length
|
|
5297
|
+
}));
|
|
5298
|
+
return true;
|
|
5299
|
+
}
|
|
5300
|
+
if (!ap.driver.supportsStdinNotification) {
|
|
5301
|
+
this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
|
|
5302
|
+
outcome: "queued_busy_non_stdin",
|
|
5303
|
+
accepted: true,
|
|
5304
|
+
process_present: true,
|
|
5305
|
+
runtime: ap.config.runtime,
|
|
5306
|
+
session_id_present: Boolean(ap.sessionId),
|
|
5307
|
+
launchId: ap.launchId || void 0,
|
|
5308
|
+
inbox_count: ap.inbox.length
|
|
5309
|
+
}));
|
|
5310
|
+
return true;
|
|
5311
|
+
}
|
|
5312
|
+
if (!ap.sessionId) {
|
|
5313
|
+
this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
|
|
5314
|
+
outcome: "queued_before_session",
|
|
5315
|
+
accepted: true,
|
|
5316
|
+
process_present: true,
|
|
5317
|
+
runtime: ap.config.runtime,
|
|
5318
|
+
session_id_present: false,
|
|
5319
|
+
launchId: ap.launchId || void 0,
|
|
5320
|
+
inbox_count: ap.inbox.length
|
|
5321
|
+
}));
|
|
5322
|
+
return true;
|
|
5323
|
+
}
|
|
4263
5324
|
if (ap.driver.busyDeliveryMode === "gated") {
|
|
4264
5325
|
ap.pendingNotificationCount++;
|
|
4265
5326
|
this.recordGatedSteeringEvent(agentId, ap, "buffer", {
|
|
4266
5327
|
reason: "busy_message",
|
|
4267
5328
|
pendingMessages: ap.inbox.length
|
|
4268
5329
|
});
|
|
4269
|
-
|
|
5330
|
+
this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
|
|
5331
|
+
outcome: "queued_busy_gated",
|
|
5332
|
+
accepted: true,
|
|
5333
|
+
process_present: true,
|
|
5334
|
+
runtime: ap.config.runtime,
|
|
5335
|
+
session_id_present: true,
|
|
5336
|
+
launchId: ap.launchId || void 0,
|
|
5337
|
+
inbox_count: ap.inbox.length,
|
|
5338
|
+
pending_notification_count: ap.pendingNotificationCount
|
|
5339
|
+
}));
|
|
5340
|
+
return true;
|
|
4270
5341
|
}
|
|
4271
5342
|
ap.pendingNotificationCount++;
|
|
4272
5343
|
if (!ap.notificationTimer) {
|
|
@@ -4274,6 +5345,17 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
|
|
|
4274
5345
|
this.sendStdinNotification(agentId);
|
|
4275
5346
|
}, 3e3);
|
|
4276
5347
|
}
|
|
5348
|
+
this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
|
|
5349
|
+
outcome: "queued_busy_notification",
|
|
5350
|
+
accepted: true,
|
|
5351
|
+
process_present: true,
|
|
5352
|
+
runtime: ap.config.runtime,
|
|
5353
|
+
session_id_present: true,
|
|
5354
|
+
inbox_count: ap.inbox.length,
|
|
5355
|
+
pending_notification_count: ap.pendingNotificationCount,
|
|
5356
|
+
notification_timer_present: Boolean(ap.notificationTimer)
|
|
5357
|
+
}));
|
|
5358
|
+
return true;
|
|
4277
5359
|
}
|
|
4278
5360
|
async resetWorkspace(agentId) {
|
|
4279
5361
|
const agentDataDir = path11.join(this.dataDir, agentId);
|
|
@@ -4285,6 +5367,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
|
|
|
4285
5367
|
}
|
|
4286
5368
|
}
|
|
4287
5369
|
async stopAll() {
|
|
5370
|
+
this.cancelAllQueuedAgentStarts("daemon shutdown");
|
|
4288
5371
|
this.idleAgentConfigs.clear();
|
|
4289
5372
|
const ids = [...this.agents.keys()];
|
|
4290
5373
|
await Promise.all(ids.map((id) => this.stopAgent(id, { wait: true, silent: true })));
|
|
@@ -4306,6 +5389,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
|
|
|
4306
5389
|
getIdleAgentSessionIds() {
|
|
4307
5390
|
const result = [];
|
|
4308
5391
|
for (const [agentId, { sessionId, launchId }] of this.idleAgentConfigs) {
|
|
5392
|
+
if (this.agents.has(agentId)) continue;
|
|
4309
5393
|
if (sessionId) result.push({ agentId, sessionId, launchId });
|
|
4310
5394
|
}
|
|
4311
5395
|
return result;
|
|
@@ -4357,7 +5441,17 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
|
|
|
4357
5441
|
}
|
|
4358
5442
|
return reports;
|
|
4359
5443
|
}
|
|
4360
|
-
deliverRuntimeProfileNotification(agentId, key, kind, content) {
|
|
5444
|
+
deliverRuntimeProfileNotification(agentId, key, kind, content, traceparent) {
|
|
5445
|
+
const span = this.tracer.startSpan("daemon.runtime_profile.control.inject", {
|
|
5446
|
+
parent: parseTraceparent(traceparent),
|
|
5447
|
+
surface: "daemon",
|
|
5448
|
+
kind: "consumer",
|
|
5449
|
+
attrs: {
|
|
5450
|
+
agentId,
|
|
5451
|
+
control_kind: kind,
|
|
5452
|
+
key_present: Boolean(key)
|
|
5453
|
+
}
|
|
5454
|
+
});
|
|
4361
5455
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4362
5456
|
const message = {
|
|
4363
5457
|
channel_id: "system",
|
|
@@ -4368,18 +5462,65 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
|
|
|
4368
5462
|
sender_type: "system",
|
|
4369
5463
|
content,
|
|
4370
5464
|
timestamp: now,
|
|
4371
|
-
message_id: `${kind === "migration" ? RUNTIME_PROFILE_MIGRATION_MESSAGE_PREFIX : RUNTIME_PROFILE_DAEMON_NOTICE_MESSAGE_PREFIX}${key}
|
|
5465
|
+
message_id: `${kind === "migration" ? RUNTIME_PROFILE_MIGRATION_MESSAGE_PREFIX : RUNTIME_PROFILE_DAEMON_NOTICE_MESSAGE_PREFIX}${key}`,
|
|
5466
|
+
traceparent: formatTraceparent(span.context)
|
|
4372
5467
|
};
|
|
4373
5468
|
const ap = this.agents.get(agentId);
|
|
5469
|
+
if (ap && !(ap.sessionId && ap.driver.supportsStdinNotification && ap.isIdle) && !(ap.sessionId && ap.driver.busyDeliveryMode === "direct")) {
|
|
5470
|
+
this.enqueueRuntimeProfileNotification(agentId, ap, message, kind, key);
|
|
5471
|
+
span.end("ok", {
|
|
5472
|
+
attrs: {
|
|
5473
|
+
outcome: ap.sessionId ? "queued_busy" : "queued_before_session",
|
|
5474
|
+
runtime: ap.config.runtime,
|
|
5475
|
+
launchId: ap.launchId || void 0,
|
|
5476
|
+
session_id_present: Boolean(ap.sessionId),
|
|
5477
|
+
supports_stdin_notification: ap.driver.supportsStdinNotification,
|
|
5478
|
+
busy_delivery_mode: ap.driver.busyDeliveryMode
|
|
5479
|
+
}
|
|
5480
|
+
});
|
|
5481
|
+
return true;
|
|
5482
|
+
}
|
|
4374
5483
|
if (ap?.sessionId && ap.driver.supportsStdinNotification && ap.isIdle) {
|
|
4375
5484
|
ap.isIdle = false;
|
|
4376
5485
|
this.startRuntimeTrace(agentId, ap, "runtime-profile");
|
|
4377
|
-
this.deliverMessagesViaStdin(agentId, ap, [message], "idle");
|
|
4378
|
-
|
|
5486
|
+
const written = this.deliverMessagesViaStdin(agentId, ap, [message], "idle");
|
|
5487
|
+
span.end(written ? "ok" : "error", {
|
|
5488
|
+
attrs: {
|
|
5489
|
+
outcome: written ? "stdin_idle" : "stdin_failed",
|
|
5490
|
+
runtime: ap.config.runtime,
|
|
5491
|
+
launchId: ap.launchId || void 0,
|
|
5492
|
+
session_id_present: true,
|
|
5493
|
+
supports_stdin_notification: true,
|
|
5494
|
+
busy_delivery_mode: ap.driver.busyDeliveryMode
|
|
5495
|
+
}
|
|
5496
|
+
});
|
|
5497
|
+
return written;
|
|
4379
5498
|
}
|
|
4380
5499
|
if (ap?.sessionId && ap.driver.busyDeliveryMode === "direct") {
|
|
4381
|
-
this.deliverMessagesViaStdin(agentId, ap, [message], "busy");
|
|
4382
|
-
|
|
5500
|
+
const written = this.deliverMessagesViaStdin(agentId, ap, [message], "busy");
|
|
5501
|
+
span.end(written ? "ok" : "error", {
|
|
5502
|
+
attrs: {
|
|
5503
|
+
outcome: written ? "stdin_busy" : "stdin_failed",
|
|
5504
|
+
runtime: ap.config.runtime,
|
|
5505
|
+
launchId: ap.launchId || void 0,
|
|
5506
|
+
session_id_present: true,
|
|
5507
|
+
supports_stdin_notification: ap.driver.supportsStdinNotification,
|
|
5508
|
+
busy_delivery_mode: ap.driver.busyDeliveryMode
|
|
5509
|
+
}
|
|
5510
|
+
});
|
|
5511
|
+
return written;
|
|
5512
|
+
}
|
|
5513
|
+
if (this.agentsStarting.has(agentId) || this.queuedAgentStarts.has(agentId)) {
|
|
5514
|
+
const queuedStart = this.queuedAgentStarts.get(agentId);
|
|
5515
|
+
this.queueRuntimeProfileNotificationDuringStart(agentId, message, kind, key);
|
|
5516
|
+
span.end("ok", {
|
|
5517
|
+
attrs: {
|
|
5518
|
+
outcome: "queued_during_start",
|
|
5519
|
+
startup_pending: true,
|
|
5520
|
+
launchId: queuedStart?.launchId
|
|
5521
|
+
}
|
|
5522
|
+
});
|
|
5523
|
+
return true;
|
|
4383
5524
|
}
|
|
4384
5525
|
const cached = this.idleAgentConfigs.get(agentId);
|
|
4385
5526
|
if (cached) {
|
|
@@ -4389,9 +5530,19 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
|
|
|
4389
5530
|
logger.error(`[Agent ${agentId}] Failed to auto-restart for runtime profile notification`, err);
|
|
4390
5531
|
this.idleAgentConfigs.set(agentId, cached);
|
|
4391
5532
|
});
|
|
4392
|
-
|
|
5533
|
+
span.end("ok", {
|
|
5534
|
+
attrs: {
|
|
5535
|
+
outcome: "restart_queued",
|
|
5536
|
+
runtime: cached.config.runtime,
|
|
5537
|
+
launchId: cached.launchId || void 0,
|
|
5538
|
+
session_id_present: Boolean(cached.sessionId)
|
|
5539
|
+
}
|
|
5540
|
+
});
|
|
5541
|
+
return true;
|
|
4393
5542
|
}
|
|
4394
5543
|
logger.warn(`[Agent ${agentId}] Runtime profile ${kind} ${key} has no runtime injection path yet; leaving unacked for retry`);
|
|
5544
|
+
span.end("ok", { attrs: { outcome: "no_path" } });
|
|
5545
|
+
return false;
|
|
4395
5546
|
}
|
|
4396
5547
|
ackInjectedRuntimeProfileMessages(agentId, messages, launchId) {
|
|
4397
5548
|
for (const message of messages) {
|
|
@@ -4404,19 +5555,32 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
|
|
|
4404
5555
|
type: "agent:runtime_profile:migration:ack",
|
|
4405
5556
|
agentId,
|
|
4406
5557
|
migrationKey: notification.key,
|
|
4407
|
-
launchId: launchId || void 0
|
|
5558
|
+
launchId: launchId || void 0,
|
|
5559
|
+
traceparent: message.traceparent
|
|
4408
5560
|
});
|
|
4409
5561
|
} else {
|
|
4410
5562
|
this.sendToServer({
|
|
4411
5563
|
type: "agent:runtime_profile:daemon_release_notice:ack",
|
|
4412
5564
|
agentId,
|
|
4413
5565
|
noticeKey: notification.key,
|
|
4414
|
-
launchId: launchId || void 0
|
|
5566
|
+
launchId: launchId || void 0,
|
|
5567
|
+
traceparent: message.traceparent
|
|
4415
5568
|
});
|
|
4416
5569
|
}
|
|
4417
5570
|
}
|
|
4418
5571
|
}
|
|
4419
5572
|
ackInjectedRuntimeProfileControl(agentId, control, launchId) {
|
|
5573
|
+
const span = this.tracer.startSpan("daemon.runtime_profile.control.inject", {
|
|
5574
|
+
surface: "daemon",
|
|
5575
|
+
kind: "internal",
|
|
5576
|
+
attrs: {
|
|
5577
|
+
agentId,
|
|
5578
|
+
control_kind: control.kind,
|
|
5579
|
+
key_present: Boolean(control.key),
|
|
5580
|
+
launchId: launchId || void 0,
|
|
5581
|
+
source: "agent_config"
|
|
5582
|
+
}
|
|
5583
|
+
});
|
|
4420
5584
|
const title = runtimeProfileNotificationTitle(control.kind);
|
|
4421
5585
|
this.broadcastActivity(agentId, "working", title, [{ kind: "system", title, text: control.message }], launchId);
|
|
4422
5586
|
if (control.kind === "migration") {
|
|
@@ -4424,24 +5588,41 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
|
|
|
4424
5588
|
type: "agent:runtime_profile:migration:ack",
|
|
4425
5589
|
agentId,
|
|
4426
5590
|
migrationKey: control.key,
|
|
4427
|
-
launchId: launchId || void 0
|
|
5591
|
+
launchId: launchId || void 0,
|
|
5592
|
+
traceparent: formatTraceparent(span.context)
|
|
4428
5593
|
});
|
|
4429
5594
|
} else {
|
|
4430
5595
|
this.sendToServer({
|
|
4431
5596
|
type: "agent:runtime_profile:daemon_release_notice:ack",
|
|
4432
5597
|
agentId,
|
|
4433
5598
|
noticeKey: control.key,
|
|
4434
|
-
launchId: launchId || void 0
|
|
5599
|
+
launchId: launchId || void 0,
|
|
5600
|
+
traceparent: formatTraceparent(span.context)
|
|
4435
5601
|
});
|
|
4436
5602
|
}
|
|
5603
|
+
span.end("ok", { attrs: { outcome: "agent_config_ack_sent" } });
|
|
4437
5604
|
}
|
|
4438
5605
|
sendRuntimeProfileWireReport(report) {
|
|
5606
|
+
const span = this.tracer.startSpan("daemon.runtime_profile.report.sent", {
|
|
5607
|
+
surface: "daemon",
|
|
5608
|
+
kind: "producer",
|
|
5609
|
+
attrs: {
|
|
5610
|
+
agentId: report.agentId,
|
|
5611
|
+
launchId: report.launchId || void 0,
|
|
5612
|
+
runtime: report.facts.runtime,
|
|
5613
|
+
model_present: Boolean(report.facts.model),
|
|
5614
|
+
session_ref_present: Boolean(report.facts.sessionRef),
|
|
5615
|
+
workspace_ref_present: Boolean(report.facts.workspaceRef || report.facts.workspacePathRef)
|
|
5616
|
+
}
|
|
5617
|
+
});
|
|
4439
5618
|
this.sendToServer({
|
|
4440
5619
|
type: "agent:runtime_profile",
|
|
4441
5620
|
agentId: report.agentId,
|
|
4442
5621
|
facts: report.facts,
|
|
4443
|
-
launchId: report.launchId || void 0
|
|
5622
|
+
launchId: report.launchId || void 0,
|
|
5623
|
+
traceparent: formatTraceparent(span.context)
|
|
4444
5624
|
});
|
|
5625
|
+
span.end("ok");
|
|
4445
5626
|
}
|
|
4446
5627
|
sendRuntimeProfileReportFor(agentId, config, sessionId, launchId) {
|
|
4447
5628
|
this.sendRuntimeProfileWireReport(this.buildRuntimeProfileReport(agentId, config, sessionId, launchId));
|
|
@@ -4484,32 +5665,21 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
|
|
|
4484
5665
|
}
|
|
4485
5666
|
const info = await stat2(resolved);
|
|
4486
5667
|
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
5668
|
const ext = path11.extname(resolved).toLowerCase();
|
|
4507
|
-
if (
|
|
4508
|
-
|
|
5669
|
+
if (WORKSPACE_TEXT_EXTENSIONS.has(ext) || ext === "") {
|
|
5670
|
+
if (info.size > WORKSPACE_TEXT_FILE_MAX_BYTES) throw new Error("File too large");
|
|
5671
|
+
const content = await readFile(resolved, "utf-8");
|
|
5672
|
+
return { content, binary: false, size: info.size, encoding: "utf-8" };
|
|
5673
|
+
}
|
|
5674
|
+
const imageMimeType = WORKSPACE_IMAGE_MIME_BY_EXTENSION[ext];
|
|
5675
|
+
if (imageMimeType) {
|
|
5676
|
+
if (info.size > WORKSPACE_IMAGE_PREVIEW_MAX_BYTES) {
|
|
5677
|
+
return { content: null, binary: true, size: info.size, mimeType: imageMimeType };
|
|
5678
|
+
}
|
|
5679
|
+
const content = await readFile(resolved, "base64");
|
|
5680
|
+
return { content, binary: true, size: info.size, mimeType: imageMimeType, encoding: "base64" };
|
|
4509
5681
|
}
|
|
4510
|
-
|
|
4511
|
-
const content = await readFile(resolved, "utf-8");
|
|
4512
|
-
return { content, binary: false };
|
|
5682
|
+
return { content: null, binary: true, size: info.size };
|
|
4513
5683
|
}
|
|
4514
5684
|
// Skill scanning
|
|
4515
5685
|
// Per-runtime skill search paths (relative to home dir for global, workspace dir for workspace).
|
|
@@ -4529,7 +5699,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
|
|
|
4529
5699
|
async listSkills(agentId, runtimeHint) {
|
|
4530
5700
|
const agent = this.agents.get(agentId);
|
|
4531
5701
|
const runtime = runtimeHint || agent?.config.runtime || "claude";
|
|
4532
|
-
const home =
|
|
5702
|
+
const home = os5.homedir();
|
|
4533
5703
|
const workspaceDir = path11.join(this.dataDir, agentId);
|
|
4534
5704
|
const paths = _AgentProcessManager.SKILL_PATHS[runtime] || _AgentProcessManager.SKILL_PATHS.claude;
|
|
4535
5705
|
const globalResults = await Promise.all(
|
|
@@ -4619,13 +5789,15 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
|
|
|
4619
5789
|
if (!hasToolStart) {
|
|
4620
5790
|
entries.push({ kind: "status", activity, detail });
|
|
4621
5791
|
}
|
|
5792
|
+
if (ap) ap.activityClientSeq += 1;
|
|
4622
5793
|
this.sendToServer({
|
|
4623
5794
|
type: "agent:activity",
|
|
4624
5795
|
agentId,
|
|
4625
5796
|
activity,
|
|
4626
5797
|
detail,
|
|
4627
5798
|
entries,
|
|
4628
|
-
launchId: launchIdOverride || ap?.launchId || void 0
|
|
5799
|
+
launchId: launchIdOverride || ap?.launchId || void 0,
|
|
5800
|
+
clientSeq: ap?.activityClientSeq
|
|
4629
5801
|
});
|
|
4630
5802
|
if (ap) {
|
|
4631
5803
|
ap.lastActivity = activity;
|
|
@@ -4637,12 +5809,14 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
|
|
|
4637
5809
|
this.recordRuntimeTraceEvent(agentId, ap, "activity.heartbeat.sent", {
|
|
4638
5810
|
activity: ap.lastActivity
|
|
4639
5811
|
});
|
|
5812
|
+
ap.activityClientSeq += 1;
|
|
4640
5813
|
this.sendToServer({
|
|
4641
5814
|
type: "agent:activity",
|
|
4642
5815
|
agentId,
|
|
4643
5816
|
activity: ap.lastActivity,
|
|
4644
5817
|
detail: ap.lastActivityDetail,
|
|
4645
|
-
launchId: launchIdOverride || ap.launchId || void 0
|
|
5818
|
+
launchId: launchIdOverride || ap.launchId || void 0,
|
|
5819
|
+
clientSeq: ap.activityClientSeq
|
|
4646
5820
|
});
|
|
4647
5821
|
}, ACTIVITY_HEARTBEAT_MS);
|
|
4648
5822
|
}
|
|
@@ -4654,6 +5828,42 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
|
|
|
4654
5828
|
}
|
|
4655
5829
|
}
|
|
4656
5830
|
}
|
|
5831
|
+
/**
|
|
5832
|
+
* Respond to a server-issued `agent:activity_probe`. Echoes the
|
|
5833
|
+
* agent's current `lastActivity` back through the existing
|
|
5834
|
+
* `agent:activity` upstream channel with the matching `probeId`.
|
|
5835
|
+
*
|
|
5836
|
+
* Why this exists: the server's stale-activity sweep used to
|
|
5837
|
+
* synthesize `online` whenever a transient state went 90s without
|
|
5838
|
+
* an update. That invented state without consulting ground truth
|
|
5839
|
+
* and produced "agent shows green/idle but is actually working" UI
|
|
5840
|
+
* staleness (#engineering:72283cf7 task #340 RCA).
|
|
5841
|
+
*
|
|
5842
|
+
* The new flow: server sends `agent:activity_probe` for stale
|
|
5843
|
+
* agents, daemon replies here with the *real* current activity, and
|
|
5844
|
+
* the server only falls back to synth-online if the probe times out
|
|
5845
|
+
* (5s). The body is intentionally minimal — no entries, no
|
|
5846
|
+
* heartbeat side-effects, no state mutation. We just echo what we
|
|
5847
|
+
* already know.
|
|
5848
|
+
*
|
|
5849
|
+
* If the agent is no longer running locally (`ap` undefined), we
|
|
5850
|
+
* report `offline` so the server stops believing the agent is busy.
|
|
5851
|
+
*/
|
|
5852
|
+
respondToActivityProbe(agentId, probeId) {
|
|
5853
|
+
const ap = this.agents.get(agentId);
|
|
5854
|
+
const activity = ap?.lastActivity || "offline";
|
|
5855
|
+
const detail = ap?.lastActivityDetail || (ap ? "" : "Agent not running");
|
|
5856
|
+
if (ap) ap.activityClientSeq += 1;
|
|
5857
|
+
this.sendToServer({
|
|
5858
|
+
type: "agent:activity",
|
|
5859
|
+
agentId,
|
|
5860
|
+
activity,
|
|
5861
|
+
detail,
|
|
5862
|
+
launchId: ap?.launchId || void 0,
|
|
5863
|
+
probeId,
|
|
5864
|
+
clientSeq: ap?.activityClientSeq
|
|
5865
|
+
});
|
|
5866
|
+
}
|
|
4657
5867
|
flushPendingTrajectory(agentId) {
|
|
4658
5868
|
const ap = this.agents.get(agentId);
|
|
4659
5869
|
const pending = ap?.pendingTrajectory;
|
|
@@ -4719,7 +5929,30 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
|
|
|
4719
5929
|
this.clearCompactionWatchdog(ap);
|
|
4720
5930
|
this.broadcastActivity(agentId, "working", detail, [{ kind: "compaction_finished" }]);
|
|
4721
5931
|
}
|
|
4722
|
-
|
|
5932
|
+
messagesTraceAttrs(messages) {
|
|
5933
|
+
if (!messages || messages.length === 0) return {};
|
|
5934
|
+
const first = messages[0];
|
|
5935
|
+
const context = this.getDeliveryTraceContext(first);
|
|
5936
|
+
const notification = runtimeProfileNotificationFromMessage(first);
|
|
5937
|
+
if (notification) {
|
|
5938
|
+
return {
|
|
5939
|
+
messages_count: messages.length,
|
|
5940
|
+
message_id_present: Boolean(first.message_id),
|
|
5941
|
+
deliveryId: context.deliveryId,
|
|
5942
|
+
delivery_correlation_id: context.deliveryId,
|
|
5943
|
+
control_kind: notification.kind,
|
|
5944
|
+
key_present: Boolean(notification.key)
|
|
5945
|
+
};
|
|
5946
|
+
}
|
|
5947
|
+
return {
|
|
5948
|
+
messages_count: messages.length,
|
|
5949
|
+
messageId: first.message_id,
|
|
5950
|
+
message_id_present: Boolean(first.message_id),
|
|
5951
|
+
deliveryId: context.deliveryId,
|
|
5952
|
+
delivery_correlation_id: context.deliveryId ?? first.message_id
|
|
5953
|
+
};
|
|
5954
|
+
}
|
|
5955
|
+
startRuntimeTrace(agentId, ap, reason, messages) {
|
|
4723
5956
|
if (ap.runtimeTraceSpan) return ap.runtimeTraceSpan;
|
|
4724
5957
|
const span = this.tracer.startSpan("daemon.runtime.turn", {
|
|
4725
5958
|
surface: "daemon",
|
|
@@ -4729,10 +5962,11 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
|
|
|
4729
5962
|
runtime: ap.config.runtime,
|
|
4730
5963
|
model: ap.config.model,
|
|
4731
5964
|
reason,
|
|
4732
|
-
hasSession: Boolean(ap.sessionId)
|
|
5965
|
+
hasSession: Boolean(ap.sessionId),
|
|
5966
|
+
...this.messagesTraceAttrs(messages)
|
|
4733
5967
|
}
|
|
4734
5968
|
});
|
|
4735
|
-
span.addEvent("daemon.turn.started", { reason });
|
|
5969
|
+
span.addEvent("daemon.turn.started", { reason, ...this.messagesTraceAttrs(messages) });
|
|
4736
5970
|
ap.runtimeTraceSpan = span;
|
|
4737
5971
|
return span;
|
|
4738
5972
|
}
|
|
@@ -4831,17 +6065,59 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
|
|
|
4831
6065
|
if (staleForMs < RUNTIME_PROGRESS_STALE_MS) return false;
|
|
4832
6066
|
ap.runtimeProgressStaleSince = Date.now();
|
|
4833
6067
|
const staleForMinutes = Math.max(1, Math.floor(staleForMs / 6e4));
|
|
4834
|
-
|
|
6068
|
+
const diagnostic = buildRuntimeStallDiagnostic(ap, staleForMs, staleForMinutes);
|
|
6069
|
+
this.recordRuntimeTraceEvent(agentId, ap, "runtime.progress.stalled", diagnostic.traceAttrs);
|
|
6070
|
+
this.endRuntimeTrace(ap, "error", {
|
|
6071
|
+
outcome: "runtime-stalled",
|
|
4835
6072
|
ageMs: staleForMs,
|
|
4836
|
-
|
|
4837
|
-
|
|
6073
|
+
lastActivity: ap.lastActivity,
|
|
6074
|
+
lastActivityDetailPresent: Boolean(ap.lastActivityDetail),
|
|
6075
|
+
lastActivityDetailKind: classifyActivityDetailForTrace(ap.lastActivityDetail)
|
|
6076
|
+
});
|
|
6077
|
+
this.broadcastActivity(agentId, "error", diagnostic.detail);
|
|
6078
|
+
return true;
|
|
6079
|
+
}
|
|
6080
|
+
recoverStaleProcessForQueuedMessageIfNeeded(agentId, ap) {
|
|
6081
|
+
if (ap.inbox.length === 0) return false;
|
|
6082
|
+
if (ap.expectedTerminationReason === "stalled_recovery") {
|
|
6083
|
+
return true;
|
|
6084
|
+
}
|
|
6085
|
+
const directStdinRuntime = ap.driver.supportsStdinNotification && ap.driver.busyDeliveryMode === "direct";
|
|
6086
|
+
const canRestartDirectStdinProcess = directStdinRuntime && Boolean(ap.sessionId) && (ap.gatedSteering.outstandingToolUses === 0 || hasDirectStdinRecoveryEvidence(ap));
|
|
6087
|
+
const canRestartStalledProcess = !ap.driver.supportsStdinNotification || canRestartDirectStdinProcess;
|
|
6088
|
+
if (!canRestartStalledProcess) return false;
|
|
6089
|
+
const staleForMs = Date.now() - ap.lastRuntimeEventAt;
|
|
6090
|
+
if (staleForMs < RUNTIME_PROGRESS_STALE_MS && !ap.runtimeProgressStaleSince) return false;
|
|
6091
|
+
const staleForMinutes = Math.max(1, Math.floor(staleForMs / 6e4));
|
|
6092
|
+
ap.runtimeProgressStaleSince ??= Date.now();
|
|
6093
|
+
const diagnostic = buildRuntimeStallDiagnostic(ap, staleForMs, staleForMinutes);
|
|
6094
|
+
this.recordRuntimeTraceEvent(agentId, ap, "runtime.progress.stalled", {
|
|
6095
|
+
...diagnostic.traceAttrs,
|
|
6096
|
+
pendingMessages: ap.inbox.length,
|
|
6097
|
+
recovery: "terminate_for_queued_message"
|
|
4838
6098
|
});
|
|
4839
6099
|
this.endRuntimeTrace(ap, "error", {
|
|
4840
6100
|
outcome: "runtime-stalled",
|
|
4841
6101
|
ageMs: staleForMs,
|
|
4842
|
-
lastActivity: ap.lastActivity
|
|
6102
|
+
lastActivity: ap.lastActivity,
|
|
6103
|
+
lastActivityDetailPresent: Boolean(ap.lastActivityDetail),
|
|
6104
|
+
lastActivityDetailKind: classifyActivityDetailForTrace(ap.lastActivityDetail),
|
|
6105
|
+
pendingMessages: ap.inbox.length,
|
|
6106
|
+
recovery: "terminate_for_queued_message"
|
|
4843
6107
|
});
|
|
4844
|
-
|
|
6108
|
+
ap.expectedTerminationReason = "stalled_recovery";
|
|
6109
|
+
const runtimeLabel = ap.driver.id === "opencode" ? "OpenCode" : ap.driver.id;
|
|
6110
|
+
logger.warn(
|
|
6111
|
+
`[Agent ${agentId}] ${runtimeLabel} process stalled for ${staleForMinutes}m with ${ap.inbox.length} queued message(s); terminating for restart`
|
|
6112
|
+
);
|
|
6113
|
+
this.broadcastActivity(agentId, "working", `Restarting stalled ${runtimeLabel} runtime for queued message`);
|
|
6114
|
+
try {
|
|
6115
|
+
ap.process.kill("SIGTERM");
|
|
6116
|
+
} catch (err) {
|
|
6117
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
6118
|
+
logger.warn(`[Agent ${agentId}] Failed to terminate stalled ${runtimeLabel} process: ${reason}`);
|
|
6119
|
+
return false;
|
|
6120
|
+
}
|
|
4845
6121
|
return true;
|
|
4846
6122
|
}
|
|
4847
6123
|
/** Handle a single ParsedEvent from any runtime driver */
|
|
@@ -4967,7 +6243,11 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
|
|
|
4967
6243
|
}
|
|
4968
6244
|
} else {
|
|
4969
6245
|
ap.isIdle = true;
|
|
4970
|
-
|
|
6246
|
+
if (ap.lastRuntimeError) {
|
|
6247
|
+
this.broadcastActivity(agentId, "error", ap.lastRuntimeError);
|
|
6248
|
+
} else {
|
|
6249
|
+
this.broadcastActivity(agentId, "online", "Idle");
|
|
6250
|
+
}
|
|
4971
6251
|
}
|
|
4972
6252
|
this.endRuntimeTrace(ap, "ok", { outcome: "turn-completed" });
|
|
4973
6253
|
if (ap.driver.terminateProcessOnTurnEnd) {
|
|
@@ -5006,6 +6286,15 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
|
|
|
5006
6286
|
}
|
|
5007
6287
|
this.recordRuntimeTraceEvent(agentId, ap, "runtime.error", { message: event.message });
|
|
5008
6288
|
this.endRuntimeTrace(ap, "error", { outcome: "runtime-error", errorMessage: event.message });
|
|
6289
|
+
if (ap.driver.supportsStdinNotification && classifyTerminalFailure(ap)) {
|
|
6290
|
+
ap.isIdle = true;
|
|
6291
|
+
ap.pendingNotificationCount = 0;
|
|
6292
|
+
if (ap.notificationTimer) {
|
|
6293
|
+
clearTimeout(ap.notificationTimer);
|
|
6294
|
+
ap.notificationTimer = null;
|
|
6295
|
+
}
|
|
6296
|
+
logger.info(`[Agent ${agentId}] Marked ${ap.driver.id} wakeable after terminal runtime error`);
|
|
6297
|
+
}
|
|
5009
6298
|
}
|
|
5010
6299
|
this.broadcastActivity(agentId, "error", event.message, [
|
|
5011
6300
|
{ kind: "text", text: `Error: ${event.message}` }
|
|
@@ -5052,20 +6341,73 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
|
|
|
5052
6341
|
const encoded = ap.driver.encodeStdinMessage(notification, ap.sessionId, { mode: "busy" });
|
|
5053
6342
|
if (encoded) {
|
|
5054
6343
|
ap.process.stdin?.write(encoded + "\n");
|
|
6344
|
+
this.recordDaemonTrace("daemon.agent.stdin_notification", {
|
|
6345
|
+
agentId,
|
|
6346
|
+
runtime: ap.config.runtime,
|
|
6347
|
+
model: ap.config.model,
|
|
6348
|
+
launchId: ap.launchId || void 0,
|
|
6349
|
+
outcome: "written",
|
|
6350
|
+
mode: "busy",
|
|
6351
|
+
pending_notification_count: count,
|
|
6352
|
+
session_id_present: true
|
|
6353
|
+
});
|
|
6354
|
+
} else {
|
|
6355
|
+
this.recordDaemonTrace("daemon.agent.stdin_notification", {
|
|
6356
|
+
agentId,
|
|
6357
|
+
runtime: ap.config.runtime,
|
|
6358
|
+
model: ap.config.model,
|
|
6359
|
+
launchId: ap.launchId || void 0,
|
|
6360
|
+
outcome: "encode_failed",
|
|
6361
|
+
mode: "busy",
|
|
6362
|
+
pending_notification_count: count,
|
|
6363
|
+
session_id_present: true
|
|
6364
|
+
}, "error");
|
|
5055
6365
|
}
|
|
5056
6366
|
}
|
|
5057
6367
|
/** Deliver a message to an agent via stdin, formatting it the same way as the MCP bridge */
|
|
5058
6368
|
deliverMessagesViaStdin(agentId, ap, messages, mode) {
|
|
5059
6369
|
if (messages.length === 0) return true;
|
|
6370
|
+
const split = this.splitRuntimeProfileControlBatch(messages);
|
|
6371
|
+
if (split.deferredMessages.length > 0) {
|
|
6372
|
+
ap.inbox.unshift(...split.deferredMessages);
|
|
6373
|
+
ap.pendingNotificationCount += split.deferredMessages.length;
|
|
6374
|
+
messages = split.nextMessages;
|
|
6375
|
+
this.recordDaemonTrace("daemon.agent.runtime_profile.split_batch", {
|
|
6376
|
+
agentId,
|
|
6377
|
+
launchId: ap.launchId || void 0,
|
|
6378
|
+
runtime: ap.config.runtime,
|
|
6379
|
+
mode,
|
|
6380
|
+
delivered_control_messages_count: messages.length,
|
|
6381
|
+
deferred_messages_count: split.deferredMessages.length,
|
|
6382
|
+
inbox_count: ap.inbox.length,
|
|
6383
|
+
pending_notification_count: ap.pendingNotificationCount
|
|
6384
|
+
});
|
|
6385
|
+
}
|
|
6386
|
+
const traceAttrs = {
|
|
6387
|
+
agentId,
|
|
6388
|
+
launchId: ap.launchId || void 0,
|
|
6389
|
+
runtime: ap.config.runtime,
|
|
6390
|
+
model: ap.config.model,
|
|
6391
|
+
mode,
|
|
6392
|
+
messages_count: messages.length,
|
|
6393
|
+
session_id_present: Boolean(ap.sessionId),
|
|
6394
|
+
inbox_count: ap.inbox.length,
|
|
6395
|
+
pending_notification_count: ap.pendingNotificationCount,
|
|
6396
|
+
busy_delivery_mode: ap.driver.busyDeliveryMode,
|
|
6397
|
+
supports_stdin_notification: ap.driver.supportsStdinNotification,
|
|
6398
|
+
...this.messagesTraceAttrs(messages)
|
|
6399
|
+
};
|
|
5060
6400
|
const prompt = formatRuntimeProfileControlPrompt(messages) ?? (messages.length === 1 ? `New message received:
|
|
5061
6401
|
|
|
5062
6402
|
${formatIncomingMessage(messages[0], ap.driver)}
|
|
5063
6403
|
|
|
5064
|
-
Respond as appropriate. Complete all your work before stopping
|
|
6404
|
+
Respond as appropriate. Complete all your work before stopping.
|
|
6405
|
+
${RESPONSE_TARGET_HINT}` : `New messages received:
|
|
5065
6406
|
|
|
5066
6407
|
${messages.map((message) => formatIncomingMessage(message, ap.driver)).join("\n")}
|
|
5067
6408
|
|
|
5068
|
-
Respond as appropriate. Complete all your work before stopping
|
|
6409
|
+
Respond as appropriate. Complete all your work before stopping.
|
|
6410
|
+
${RESPONSE_TARGET_HINT}`);
|
|
5069
6411
|
const encoded = ap.driver.encodeStdinMessage(prompt, ap.sessionId, { mode });
|
|
5070
6412
|
if (!encoded) {
|
|
5071
6413
|
ap.inbox.unshift(...messages);
|
|
@@ -5075,14 +6417,27 @@ Respond as appropriate. Complete all your work before stopping.`);
|
|
|
5075
6417
|
logger.warn(
|
|
5076
6418
|
`[Agent ${agentId}] Failed to encode ${mode} stdin delivery; re-queued ${messages.length === 1 ? "message" : `${messages.length} messages`}`
|
|
5077
6419
|
);
|
|
6420
|
+
this.recordDaemonTrace("daemon.agent.stdin_delivery", {
|
|
6421
|
+
...traceAttrs,
|
|
6422
|
+
outcome: "encode_failed",
|
|
6423
|
+
requeued_messages_count: messages.length
|
|
6424
|
+
}, "error");
|
|
5078
6425
|
return false;
|
|
5079
6426
|
}
|
|
5080
6427
|
const senders = [...new Set(messages.map((message) => `@${message.sender_name}`))].join(", ");
|
|
5081
6428
|
logger.info(
|
|
5082
6429
|
`[Agent ${agentId}] Delivering ${mode} ${messages.length === 1 ? "message" : `${messages.length} messages`} via stdin from ${senders}`
|
|
5083
6430
|
);
|
|
6431
|
+
if (this.containsOrdinaryInboxMessage(messages)) {
|
|
6432
|
+
ap.lastRuntimeError = null;
|
|
6433
|
+
}
|
|
5084
6434
|
ap.process.stdin?.write(encoded + "\n");
|
|
5085
6435
|
this.ackInjectedRuntimeProfileMessages(agentId, messages, ap.launchId);
|
|
6436
|
+
this.recordDaemonTrace("daemon.agent.stdin_delivery", {
|
|
6437
|
+
...traceAttrs,
|
|
6438
|
+
outcome: "written",
|
|
6439
|
+
stdin_write_attempted: true
|
|
6440
|
+
});
|
|
5086
6441
|
return true;
|
|
5087
6442
|
}
|
|
5088
6443
|
/** List ONE level of a directory — directories returned without children (lazy-loaded on demand) */
|
|
@@ -5127,6 +6482,16 @@ var systemClock = {
|
|
|
5127
6482
|
clearTimeout: (timer) => clearTimeout(timer)
|
|
5128
6483
|
};
|
|
5129
6484
|
var INBOUND_WATCHDOG_MS = 7e4;
|
|
6485
|
+
function durationMsBucket(ms) {
|
|
6486
|
+
if (ms == null || !Number.isFinite(ms) || ms < 0) return "unknown";
|
|
6487
|
+
if (ms === 0) return "0";
|
|
6488
|
+
if (ms <= 1e3) return "1s";
|
|
6489
|
+
if (ms <= 1e4) return "1s-10s";
|
|
6490
|
+
if (ms <= 3e4) return "10s-30s";
|
|
6491
|
+
if (ms <= 6e4) return "30s-60s";
|
|
6492
|
+
if (ms <= 12e4) return "60s-120s";
|
|
6493
|
+
return "120s+";
|
|
6494
|
+
}
|
|
5130
6495
|
var DaemonConnection = class {
|
|
5131
6496
|
ws = null;
|
|
5132
6497
|
options;
|
|
@@ -5138,6 +6503,8 @@ var DaemonConnection = class {
|
|
|
5138
6503
|
shouldConnect = true;
|
|
5139
6504
|
reconnectAttempt = 0;
|
|
5140
6505
|
lastDroppedSendLogAt = 0;
|
|
6506
|
+
lastInboundAt = null;
|
|
6507
|
+
lastInboundMessageKind = null;
|
|
5141
6508
|
constructor(options) {
|
|
5142
6509
|
this.options = options;
|
|
5143
6510
|
this.clock = options.clock ?? systemClock;
|
|
@@ -5172,6 +6539,10 @@ var DaemonConnection = class {
|
|
|
5172
6539
|
this.lastDroppedSendLogAt = now;
|
|
5173
6540
|
logger.warn(`[Daemon] Dropping outbound message while disconnected: ${msg.type}`);
|
|
5174
6541
|
}
|
|
6542
|
+
this.trace("daemon.connection.outbound_dropped", {
|
|
6543
|
+
message_type: msg.type,
|
|
6544
|
+
ws_ready_state: this.ws?.readyState ?? null
|
|
6545
|
+
});
|
|
5175
6546
|
}
|
|
5176
6547
|
get connected() {
|
|
5177
6548
|
return this.ws?.readyState === WebSocket.OPEN;
|
|
@@ -5185,25 +6556,49 @@ var DaemonConnection = class {
|
|
|
5185
6556
|
if (wsOptions?.agent) {
|
|
5186
6557
|
logger.info("[Daemon] Using configured proxy for WebSocket connection");
|
|
5187
6558
|
}
|
|
6559
|
+
this.trace("daemon.connection.connecting", {
|
|
6560
|
+
reconnect_attempt: this.reconnectAttempt,
|
|
6561
|
+
server_url_present: Boolean(this.options.serverUrl),
|
|
6562
|
+
proxy_present: Boolean(wsOptions?.agent)
|
|
6563
|
+
});
|
|
5188
6564
|
const ws = this.options.wsFactory ? this.options.wsFactory(wsUrl, wsOptions) : new WebSocket(wsUrl, wsOptions);
|
|
5189
6565
|
this.ws = ws;
|
|
5190
6566
|
ws.on("open", () => {
|
|
5191
6567
|
if (this.ws !== ws) return;
|
|
5192
6568
|
if (!this.shouldConnect) return;
|
|
5193
6569
|
logger.info("[Daemon] Connected to server");
|
|
6570
|
+
const priorReconnectAttempt = this.reconnectAttempt;
|
|
5194
6571
|
this.reconnectAttempt = 0;
|
|
5195
6572
|
this.reconnectDelay = this.options.minReconnectDelayMs ?? 1e3;
|
|
6573
|
+
this.markInbound("websocket_open");
|
|
5196
6574
|
this.resetWatchdog();
|
|
6575
|
+
this.trace("daemon.connection.connected", {
|
|
6576
|
+
reconnect_attempt: priorReconnectAttempt,
|
|
6577
|
+
inbound_watchdog_ms: this.options.inboundWatchdogMs ?? INBOUND_WATCHDOG_MS
|
|
6578
|
+
});
|
|
5197
6579
|
this.options.onConnect();
|
|
5198
6580
|
});
|
|
5199
6581
|
ws.on("message", (data) => {
|
|
5200
6582
|
if (this.ws !== ws) return;
|
|
5201
|
-
|
|
6583
|
+
let messageKind = "unknown";
|
|
5202
6584
|
try {
|
|
5203
6585
|
const msg = JSON.parse(data.toString());
|
|
6586
|
+
messageKind = msg.type;
|
|
6587
|
+
this.markInbound(messageKind);
|
|
6588
|
+
this.resetWatchdog();
|
|
6589
|
+
this.trace("daemon.connection.inbound_received", {
|
|
6590
|
+
message_type: messageKind,
|
|
6591
|
+
last_inbound_age_ms_bucket: "0"
|
|
6592
|
+
});
|
|
5204
6593
|
this.options.onMessage(msg);
|
|
5205
6594
|
} catch (err) {
|
|
6595
|
+
this.markInbound("invalid_json");
|
|
6596
|
+
this.resetWatchdog();
|
|
5206
6597
|
logger.error("[Daemon] Invalid message from server", err);
|
|
6598
|
+
this.trace("daemon.connection.invalid_message", {
|
|
6599
|
+
error_class: err instanceof Error ? err.name : typeof err,
|
|
6600
|
+
last_inbound_message_kind: "invalid_json"
|
|
6601
|
+
}, "error");
|
|
5207
6602
|
}
|
|
5208
6603
|
});
|
|
5209
6604
|
ws.on("close", (code, reasonBuffer) => {
|
|
@@ -5214,12 +6609,23 @@ var DaemonConnection = class {
|
|
|
5214
6609
|
logger.warn(
|
|
5215
6610
|
`[Daemon] Disconnected from server (code=${code}, reason=${JSON.stringify(reason)}, reconnecting=${this.shouldConnect})`
|
|
5216
6611
|
);
|
|
6612
|
+
this.trace("daemon.connection.disconnected", {
|
|
6613
|
+
close_code: code,
|
|
6614
|
+
close_reason_present: Boolean(reason),
|
|
6615
|
+
reconnecting: this.shouldConnect,
|
|
6616
|
+
reconnect_attempt: this.reconnectAttempt,
|
|
6617
|
+
last_inbound_message_kind: this.lastInboundMessageKind,
|
|
6618
|
+
last_inbound_age_ms_bucket: this.lastInboundAgeBucket()
|
|
6619
|
+
}, this.shouldConnect ? "cancelled" : "ok");
|
|
5217
6620
|
this.options.onDisconnect();
|
|
5218
6621
|
this.scheduleReconnect();
|
|
5219
6622
|
});
|
|
5220
6623
|
ws.on("error", (err) => {
|
|
5221
6624
|
if (this.ws !== ws) return;
|
|
5222
6625
|
logger.error(`[Daemon] WebSocket error: ${err.message}`);
|
|
6626
|
+
this.trace("daemon.connection.error", {
|
|
6627
|
+
error_class: err.name || "Error"
|
|
6628
|
+
}, "error");
|
|
5223
6629
|
});
|
|
5224
6630
|
}
|
|
5225
6631
|
scheduleReconnect() {
|
|
@@ -5227,6 +6633,10 @@ var DaemonConnection = class {
|
|
|
5227
6633
|
if (this.reconnectTimer) return;
|
|
5228
6634
|
this.reconnectAttempt += 1;
|
|
5229
6635
|
logger.info(`[Daemon] Reconnecting to server in ${this.reconnectDelay}ms (attempt ${this.reconnectAttempt})`);
|
|
6636
|
+
this.trace("daemon.connection.reconnect_scheduled", {
|
|
6637
|
+
reconnect_attempt: this.reconnectAttempt,
|
|
6638
|
+
delay_ms: this.reconnectDelay
|
|
6639
|
+
});
|
|
5230
6640
|
this.reconnectTimer = this.clock.setTimeout(() => {
|
|
5231
6641
|
this.reconnectTimer = null;
|
|
5232
6642
|
this.doConnect();
|
|
@@ -5238,6 +6648,13 @@ var DaemonConnection = class {
|
|
|
5238
6648
|
const ms = this.options.inboundWatchdogMs ?? INBOUND_WATCHDOG_MS;
|
|
5239
6649
|
this.watchdogTimer = this.clock.setTimeout(() => {
|
|
5240
6650
|
logger.warn(`[Daemon] No inbound traffic for ${ms / 1e3}s \u2014 forcing reconnect`);
|
|
6651
|
+
this.trace("daemon.connection.watchdog_timeout", {
|
|
6652
|
+
inbound_watchdog_ms: ms,
|
|
6653
|
+
last_inbound_message_kind: this.lastInboundMessageKind,
|
|
6654
|
+
last_inbound_age_ms_bucket: this.lastInboundAgeBucket(),
|
|
6655
|
+
ws_ready_state: this.ws?.readyState ?? null,
|
|
6656
|
+
reconnecting: this.shouldConnect
|
|
6657
|
+
}, "error");
|
|
5241
6658
|
try {
|
|
5242
6659
|
this.ws?.terminate();
|
|
5243
6660
|
} catch {
|
|
@@ -5250,6 +6667,16 @@ var DaemonConnection = class {
|
|
|
5250
6667
|
this.watchdogTimer = null;
|
|
5251
6668
|
}
|
|
5252
6669
|
}
|
|
6670
|
+
markInbound(messageKind) {
|
|
6671
|
+
this.lastInboundAt = this.clock.now();
|
|
6672
|
+
this.lastInboundMessageKind = messageKind;
|
|
6673
|
+
}
|
|
6674
|
+
lastInboundAgeBucket() {
|
|
6675
|
+
return durationMsBucket(this.lastInboundAt == null ? null : this.clock.now() - this.lastInboundAt);
|
|
6676
|
+
}
|
|
6677
|
+
trace(name, attrs, status = "ok") {
|
|
6678
|
+
this.options.onTraceEvent?.(name, attrs, status);
|
|
6679
|
+
}
|
|
5253
6680
|
};
|
|
5254
6681
|
|
|
5255
6682
|
// src/reminderCache.ts
|
|
@@ -5333,10 +6760,10 @@ var ReminderCache = class {
|
|
|
5333
6760
|
|
|
5334
6761
|
// src/machineLock.ts
|
|
5335
6762
|
import { createHash, randomUUID as randomUUID2 } from "crypto";
|
|
5336
|
-
import { mkdirSync as mkdirSync5, readFileSync as
|
|
5337
|
-
import
|
|
6763
|
+
import { mkdirSync as mkdirSync5, readFileSync as readFileSync5, rmSync as rmSync2, statSync as statSync3, writeFileSync as writeFileSync8 } from "fs";
|
|
6764
|
+
import os6 from "os";
|
|
5338
6765
|
import path12 from "path";
|
|
5339
|
-
var DEFAULT_MACHINE_STATE_ROOT = path12.join(
|
|
6766
|
+
var DEFAULT_MACHINE_STATE_ROOT = path12.join(os6.homedir(), ".slock", "machines");
|
|
5340
6767
|
var INCOMPLETE_LOCK_STALE_MS = 3e4;
|
|
5341
6768
|
var DaemonMachineLockConflictError = class extends Error {
|
|
5342
6769
|
code = "DAEMON_MACHINE_LOCK_HELD";
|
|
@@ -5359,14 +6786,14 @@ function ownerPath(lockDir) {
|
|
|
5359
6786
|
}
|
|
5360
6787
|
function readOwner(lockDir) {
|
|
5361
6788
|
try {
|
|
5362
|
-
return JSON.parse(
|
|
6789
|
+
return JSON.parse(readFileSync5(ownerPath(lockDir), "utf8"));
|
|
5363
6790
|
} catch {
|
|
5364
6791
|
return null;
|
|
5365
6792
|
}
|
|
5366
6793
|
}
|
|
5367
6794
|
function lockAgeMs(lockDir) {
|
|
5368
6795
|
try {
|
|
5369
|
-
return Date.now() -
|
|
6796
|
+
return Date.now() - statSync3(lockDir).mtimeMs;
|
|
5370
6797
|
} catch {
|
|
5371
6798
|
return null;
|
|
5372
6799
|
}
|
|
@@ -5395,7 +6822,7 @@ function acquireDaemonMachineLock(options) {
|
|
|
5395
6822
|
const owner = {
|
|
5396
6823
|
pid: process.pid,
|
|
5397
6824
|
token,
|
|
5398
|
-
hostname:
|
|
6825
|
+
hostname: os6.hostname(),
|
|
5399
6826
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5400
6827
|
serverUrl: options.serverUrl,
|
|
5401
6828
|
apiKeyFingerprint: fingerprint.slice(0, 16)
|
|
@@ -5437,6 +6864,418 @@ function acquireDaemonMachineLock(options) {
|
|
|
5437
6864
|
throw new DaemonMachineLockConflictError(lockDir, readOwner(lockDir));
|
|
5438
6865
|
}
|
|
5439
6866
|
|
|
6867
|
+
// src/localTraceSink.ts
|
|
6868
|
+
import { appendFileSync, mkdirSync as mkdirSync6, readdirSync as readdirSync3, rmSync as rmSync3, statSync as statSync4, writeFileSync as writeFileSync9 } from "fs";
|
|
6869
|
+
import path13 from "path";
|
|
6870
|
+
var DEFAULT_MAX_FILE_BYTES = 5 * 1024 * 1024;
|
|
6871
|
+
var DEFAULT_MAX_FILES = 8;
|
|
6872
|
+
var DIAGNOSTIC_ID_ATTRS = /* @__PURE__ */ new Set([
|
|
6873
|
+
"serverId",
|
|
6874
|
+
"machineId",
|
|
6875
|
+
"agentId",
|
|
6876
|
+
"messageId",
|
|
6877
|
+
"launchId",
|
|
6878
|
+
"uploadId",
|
|
6879
|
+
"bundleId",
|
|
6880
|
+
"deliveryId",
|
|
6881
|
+
"deliveryCorrelationId",
|
|
6882
|
+
"delivery_correlation_id"
|
|
6883
|
+
]);
|
|
6884
|
+
var LocalRotatingTraceSink = class {
|
|
6885
|
+
traceDir;
|
|
6886
|
+
maxFileBytes;
|
|
6887
|
+
maxFiles;
|
|
6888
|
+
currentFile = null;
|
|
6889
|
+
currentSize = 0;
|
|
6890
|
+
sequence = 0;
|
|
6891
|
+
constructor(options) {
|
|
6892
|
+
this.traceDir = path13.join(options.machineDir, "traces");
|
|
6893
|
+
this.maxFileBytes = Math.max(1024, Math.floor(options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES));
|
|
6894
|
+
this.maxFiles = Math.max(1, Math.floor(options.maxFiles ?? DEFAULT_MAX_FILES));
|
|
6895
|
+
}
|
|
6896
|
+
record(span) {
|
|
6897
|
+
try {
|
|
6898
|
+
const line = `${JSON.stringify(toLocalTraceRecord(span))}
|
|
6899
|
+
`;
|
|
6900
|
+
this.ensureFile(Buffer.byteLength(line));
|
|
6901
|
+
appendFileSync(this.currentFile, line, { encoding: "utf8" });
|
|
6902
|
+
this.currentSize += Buffer.byteLength(line);
|
|
6903
|
+
} catch {
|
|
6904
|
+
}
|
|
6905
|
+
}
|
|
6906
|
+
getCurrentFile() {
|
|
6907
|
+
return this.currentFile;
|
|
6908
|
+
}
|
|
6909
|
+
ensureFile(nextBytes) {
|
|
6910
|
+
mkdirSync6(this.traceDir, { recursive: true, mode: 448 });
|
|
6911
|
+
if (!this.currentFile || this.currentSize + nextBytes > this.maxFileBytes) {
|
|
6912
|
+
this.currentFile = path13.join(
|
|
6913
|
+
this.traceDir,
|
|
6914
|
+
`daemon-trace-${safeTimestamp(Date.now())}-${process.pid}-${String(this.sequence++).padStart(4, "0")}.jsonl`
|
|
6915
|
+
);
|
|
6916
|
+
writeFileSync9(this.currentFile, "", { flag: "a", mode: 384 });
|
|
6917
|
+
this.currentSize = statSync4(this.currentFile).size;
|
|
6918
|
+
this.pruneOldFiles();
|
|
6919
|
+
}
|
|
6920
|
+
}
|
|
6921
|
+
pruneOldFiles() {
|
|
6922
|
+
const files = readdirSync3(this.traceDir).filter((name) => name.startsWith("daemon-trace-") && name.endsWith(".jsonl")).sort();
|
|
6923
|
+
const excess = files.length - this.maxFiles;
|
|
6924
|
+
if (excess <= 0) return;
|
|
6925
|
+
for (const file of files.slice(0, excess)) {
|
|
6926
|
+
rmSync3(path13.join(this.traceDir, file), { force: true });
|
|
6927
|
+
}
|
|
6928
|
+
}
|
|
6929
|
+
};
|
|
6930
|
+
function safeTimestamp(timeMs) {
|
|
6931
|
+
return new Date(timeMs).toISOString().replace(/[:.]/g, "-");
|
|
6932
|
+
}
|
|
6933
|
+
function toLocalTraceRecord(span) {
|
|
6934
|
+
return {
|
|
6935
|
+
type: "span",
|
|
6936
|
+
schema_version: 1,
|
|
6937
|
+
trace_id: span.context.traceId,
|
|
6938
|
+
span_id: span.context.spanId,
|
|
6939
|
+
parent_span_id: span.context.parentSpanId,
|
|
6940
|
+
name: span.name,
|
|
6941
|
+
surface: span.surface,
|
|
6942
|
+
kind: span.kind,
|
|
6943
|
+
status: span.status,
|
|
6944
|
+
start_time: new Date(span.startTimeMs).toISOString(),
|
|
6945
|
+
end_time: new Date(span.endTimeMs).toISOString(),
|
|
6946
|
+
duration_ms: span.durationMs,
|
|
6947
|
+
attrs: sanitizeAttrs(span.attrs),
|
|
6948
|
+
events: span.events.map(sanitizeEvent)
|
|
6949
|
+
};
|
|
6950
|
+
}
|
|
6951
|
+
function sanitizeEvent(event) {
|
|
6952
|
+
return {
|
|
6953
|
+
name: event.name,
|
|
6954
|
+
time: new Date(event.timeMs).toISOString(),
|
|
6955
|
+
attrs: sanitizeAttrs(event.attrs)
|
|
6956
|
+
};
|
|
6957
|
+
}
|
|
6958
|
+
function sanitizeAttrs(attrs) {
|
|
6959
|
+
if (!attrs) return void 0;
|
|
6960
|
+
const sanitized = {};
|
|
6961
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
6962
|
+
if (isDiagnosticIdAttr(key)) {
|
|
6963
|
+
if (value === null || value === void 0 || value === "") continue;
|
|
6964
|
+
sanitized[key] = sanitizeValue(value);
|
|
6965
|
+
continue;
|
|
6966
|
+
}
|
|
6967
|
+
if (shouldDropAttr(key)) continue;
|
|
6968
|
+
sanitized[key] = sanitizeValue(value);
|
|
6969
|
+
}
|
|
6970
|
+
return sanitized;
|
|
6971
|
+
}
|
|
6972
|
+
function sanitizeValue(value) {
|
|
6973
|
+
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
6974
|
+
return value;
|
|
6975
|
+
}
|
|
6976
|
+
if (Array.isArray(value)) {
|
|
6977
|
+
return { items_count: value.length };
|
|
6978
|
+
}
|
|
6979
|
+
if (typeof value === "object") {
|
|
6980
|
+
return { object_present: true };
|
|
6981
|
+
}
|
|
6982
|
+
return String(value);
|
|
6983
|
+
}
|
|
6984
|
+
function shouldDropAttr(key) {
|
|
6985
|
+
const normalized = key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
|
|
6986
|
+
if (/(^|_)(api_key|auth_token|token|secret|password|cookie|credential)(_|$)/i.test(normalized)) {
|
|
6987
|
+
return true;
|
|
6988
|
+
}
|
|
6989
|
+
if (/(^|_)(count|present|kind|mode|source|outcome|reason|class|status|bucket|ms|code|truncated)$/.test(normalized)) {
|
|
6990
|
+
return false;
|
|
6991
|
+
}
|
|
6992
|
+
if (/(^|_)id$/.test(normalized)) {
|
|
6993
|
+
return true;
|
|
6994
|
+
}
|
|
6995
|
+
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);
|
|
6996
|
+
}
|
|
6997
|
+
function isDiagnosticIdAttr(key) {
|
|
6998
|
+
return DIAGNOSTIC_ID_ATTRS.has(key);
|
|
6999
|
+
}
|
|
7000
|
+
|
|
7001
|
+
// src/traceBundleUpload.ts
|
|
7002
|
+
import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
|
|
7003
|
+
import { gzipSync } from "zlib";
|
|
7004
|
+
import { mkdir as mkdir2, readFile as readFile2, readdir as readdir3, stat as stat3, writeFile as writeFile2 } from "fs/promises";
|
|
7005
|
+
import path14 from "path";
|
|
7006
|
+
|
|
7007
|
+
// src/directUploadCapability.ts
|
|
7008
|
+
function joinUrl(base, path16) {
|
|
7009
|
+
return `${base.replace(/\/+$/, "")}${path16}`;
|
|
7010
|
+
}
|
|
7011
|
+
function jsonHeaders(apiKey) {
|
|
7012
|
+
return {
|
|
7013
|
+
"Content-Type": "application/json",
|
|
7014
|
+
...apiKey ? { Authorization: `Bearer ${apiKey}` } : {}
|
|
7015
|
+
};
|
|
7016
|
+
}
|
|
7017
|
+
async function requestDaemonScopeAttestation({
|
|
7018
|
+
serverUrl,
|
|
7019
|
+
apiKey,
|
|
7020
|
+
scope,
|
|
7021
|
+
metadata,
|
|
7022
|
+
fetchImpl = fetch,
|
|
7023
|
+
timeoutMs = DEFAULT_CHAT_BRIDGE_TOOL_TIMEOUT_MS
|
|
7024
|
+
}) {
|
|
7025
|
+
const { response, data } = await executeJsonRequest(
|
|
7026
|
+
joinUrl(serverUrl, "/internal/machine/scope-attestation"),
|
|
7027
|
+
{
|
|
7028
|
+
method: "POST",
|
|
7029
|
+
headers: jsonHeaders(apiKey),
|
|
7030
|
+
body: JSON.stringify({
|
|
7031
|
+
scope,
|
|
7032
|
+
...metadata ? { metadata } : {}
|
|
7033
|
+
})
|
|
7034
|
+
},
|
|
7035
|
+
{
|
|
7036
|
+
toolName: "daemon_direct_upload.scope_attestation",
|
|
7037
|
+
target: scope,
|
|
7038
|
+
timeoutMs,
|
|
7039
|
+
fetchImpl
|
|
7040
|
+
}
|
|
7041
|
+
);
|
|
7042
|
+
if (!response.ok) {
|
|
7043
|
+
throw new Error(`Failed to request daemon scope attestation (${response.status})`);
|
|
7044
|
+
}
|
|
7045
|
+
return data;
|
|
7046
|
+
}
|
|
7047
|
+
async function createDirectUploadSession({
|
|
7048
|
+
serverUrl,
|
|
7049
|
+
apiKey,
|
|
7050
|
+
workerUrl,
|
|
7051
|
+
scope,
|
|
7052
|
+
createPath = "/api/uploads",
|
|
7053
|
+
body,
|
|
7054
|
+
attestationMetadata,
|
|
7055
|
+
fetchImpl = fetch,
|
|
7056
|
+
timeoutMs = DEFAULT_CHAT_BRIDGE_TOOL_TIMEOUT_MS
|
|
7057
|
+
}) {
|
|
7058
|
+
const capability = await requestDaemonScopeAttestation({
|
|
7059
|
+
serverUrl,
|
|
7060
|
+
apiKey,
|
|
7061
|
+
scope,
|
|
7062
|
+
metadata: attestationMetadata,
|
|
7063
|
+
fetchImpl,
|
|
7064
|
+
timeoutMs
|
|
7065
|
+
});
|
|
7066
|
+
const { response, data } = await executeJsonRequest(
|
|
7067
|
+
joinUrl(workerUrl, createPath),
|
|
7068
|
+
{
|
|
7069
|
+
method: "POST",
|
|
7070
|
+
headers: jsonHeaders(),
|
|
7071
|
+
body: JSON.stringify({
|
|
7072
|
+
...body,
|
|
7073
|
+
attestation: capability.attestation
|
|
7074
|
+
})
|
|
7075
|
+
},
|
|
7076
|
+
{
|
|
7077
|
+
toolName: "daemon_direct_upload.create",
|
|
7078
|
+
target: capability.audience,
|
|
7079
|
+
timeoutMs,
|
|
7080
|
+
fetchImpl
|
|
7081
|
+
}
|
|
7082
|
+
);
|
|
7083
|
+
if (!response.ok) {
|
|
7084
|
+
throw new Error(`Failed to create direct upload session (${response.status})`);
|
|
7085
|
+
}
|
|
7086
|
+
return { capability, response: data };
|
|
7087
|
+
}
|
|
7088
|
+
async function uploadWithSignedCapability({
|
|
7089
|
+
serverUrl,
|
|
7090
|
+
apiKey,
|
|
7091
|
+
workerUrl,
|
|
7092
|
+
scope,
|
|
7093
|
+
createPath = "/api/uploads",
|
|
7094
|
+
createBody,
|
|
7095
|
+
attestationMetadata,
|
|
7096
|
+
uploadBody,
|
|
7097
|
+
fetchImpl = fetch,
|
|
7098
|
+
timeoutMs = DEFAULT_CHAT_BRIDGE_TOOL_TIMEOUT_MS
|
|
7099
|
+
}) {
|
|
7100
|
+
const { capability, response: session } = await createDirectUploadSession({
|
|
7101
|
+
serverUrl,
|
|
7102
|
+
apiKey,
|
|
7103
|
+
workerUrl,
|
|
7104
|
+
scope,
|
|
7105
|
+
createPath,
|
|
7106
|
+
body: createBody,
|
|
7107
|
+
attestationMetadata,
|
|
7108
|
+
fetchImpl,
|
|
7109
|
+
timeoutMs
|
|
7110
|
+
});
|
|
7111
|
+
const { response: uploadResponse } = await executeResponseRequest(
|
|
7112
|
+
session.upload.url,
|
|
7113
|
+
{
|
|
7114
|
+
method: session.upload.method,
|
|
7115
|
+
headers: session.upload.headers ?? {},
|
|
7116
|
+
body: uploadBody
|
|
7117
|
+
},
|
|
7118
|
+
{
|
|
7119
|
+
toolName: "daemon_direct_upload.put",
|
|
7120
|
+
target: capability.audience,
|
|
7121
|
+
timeoutMs,
|
|
7122
|
+
fetchImpl
|
|
7123
|
+
}
|
|
7124
|
+
);
|
|
7125
|
+
if (!uploadResponse.ok) {
|
|
7126
|
+
throw new Error(`Failed to upload with signed capability (${uploadResponse.status})`);
|
|
7127
|
+
}
|
|
7128
|
+
return { capability, session, uploadResponse };
|
|
7129
|
+
}
|
|
7130
|
+
|
|
7131
|
+
// src/traceBundleUpload.ts
|
|
7132
|
+
var TRACE_UPLOAD_SCOPE = "daemon-trace-bundle:create";
|
|
7133
|
+
var DEFAULT_UPLOAD_INTERVAL_MS = 5 * 60 * 1e3;
|
|
7134
|
+
var DEFAULT_MIN_FILE_AGE_MS = 60 * 1e3;
|
|
7135
|
+
var DEFAULT_MAX_FILES_PER_RUN = 4;
|
|
7136
|
+
var DaemonTraceBundleUploader = class {
|
|
7137
|
+
options;
|
|
7138
|
+
timer = null;
|
|
7139
|
+
constructor(options) {
|
|
7140
|
+
this.options = options;
|
|
7141
|
+
}
|
|
7142
|
+
start() {
|
|
7143
|
+
if (this.timer) return;
|
|
7144
|
+
void this.uploadOnce();
|
|
7145
|
+
this.timer = setInterval(() => {
|
|
7146
|
+
void this.uploadOnce();
|
|
7147
|
+
}, this.options.intervalMs ?? readPositiveIntegerEnv2("SLOCK_DAEMON_TRACE_UPLOAD_INTERVAL_MS", DEFAULT_UPLOAD_INTERVAL_MS));
|
|
7148
|
+
}
|
|
7149
|
+
stop() {
|
|
7150
|
+
if (!this.timer) return;
|
|
7151
|
+
clearInterval(this.timer);
|
|
7152
|
+
this.timer = null;
|
|
7153
|
+
}
|
|
7154
|
+
async uploadOnce() {
|
|
7155
|
+
const files = await this.findUploadCandidates();
|
|
7156
|
+
let uploaded = 0;
|
|
7157
|
+
for (const file of files.slice(0, this.options.maxFilesPerRun ?? DEFAULT_MAX_FILES_PER_RUN)) {
|
|
7158
|
+
if (await this.uploadFile(file)) uploaded += 1;
|
|
7159
|
+
}
|
|
7160
|
+
return { attempted: files.length, uploaded };
|
|
7161
|
+
}
|
|
7162
|
+
async findUploadCandidates() {
|
|
7163
|
+
const traceDir = path14.join(this.options.machineDir, "traces");
|
|
7164
|
+
let names;
|
|
7165
|
+
try {
|
|
7166
|
+
names = await readdir3(traceDir);
|
|
7167
|
+
} catch {
|
|
7168
|
+
return [];
|
|
7169
|
+
}
|
|
7170
|
+
const now = Date.now();
|
|
7171
|
+
const minAgeMs = this.options.minFileAgeMs ?? readPositiveIntegerEnv2("SLOCK_DAEMON_TRACE_UPLOAD_MIN_FILE_AGE_MS", DEFAULT_MIN_FILE_AGE_MS);
|
|
7172
|
+
const currentFile = this.options.currentFileProvider?.();
|
|
7173
|
+
const candidates = [];
|
|
7174
|
+
for (const name of names.filter((entry) => entry.startsWith("daemon-trace-") && entry.endsWith(".jsonl")).sort()) {
|
|
7175
|
+
const file = path14.join(traceDir, name);
|
|
7176
|
+
if (currentFile && path14.resolve(file) === path14.resolve(currentFile)) continue;
|
|
7177
|
+
if (await this.isUploaded(file)) continue;
|
|
7178
|
+
try {
|
|
7179
|
+
const info = await stat3(file);
|
|
7180
|
+
if (!info.isFile() || info.size <= 0) continue;
|
|
7181
|
+
if (now - info.mtimeMs < minAgeMs) continue;
|
|
7182
|
+
candidates.push(file);
|
|
7183
|
+
} catch {
|
|
7184
|
+
}
|
|
7185
|
+
}
|
|
7186
|
+
return candidates;
|
|
7187
|
+
}
|
|
7188
|
+
async uploadFile(file) {
|
|
7189
|
+
const span = this.options.tracer?.startSpan("daemon.bundle.upload", {
|
|
7190
|
+
surface: "daemon",
|
|
7191
|
+
kind: "producer",
|
|
7192
|
+
attrs: {
|
|
7193
|
+
file_present: true,
|
|
7194
|
+
worker_url_present: Boolean(this.options.workerUrl)
|
|
7195
|
+
}
|
|
7196
|
+
});
|
|
7197
|
+
try {
|
|
7198
|
+
const raw = await readFile2(file);
|
|
7199
|
+
if (raw.byteLength === 0) {
|
|
7200
|
+
span?.end("cancelled", { attrs: { outcome: "empty" } });
|
|
7201
|
+
return false;
|
|
7202
|
+
}
|
|
7203
|
+
const gzipped = gzipSync(raw);
|
|
7204
|
+
const bundleSha256 = sha256Hex(gzipped);
|
|
7205
|
+
const bundleId = randomUUID3();
|
|
7206
|
+
await uploadWithSignedCapability({
|
|
7207
|
+
serverUrl: this.options.serverUrl,
|
|
7208
|
+
apiKey: this.options.apiKey,
|
|
7209
|
+
workerUrl: this.options.workerUrl,
|
|
7210
|
+
scope: TRACE_UPLOAD_SCOPE,
|
|
7211
|
+
createPath: "/api/trace-bundles",
|
|
7212
|
+
attestationMetadata: {
|
|
7213
|
+
bundleId,
|
|
7214
|
+
bundleSha256,
|
|
7215
|
+
bundleSizeBytes: gzipped.byteLength
|
|
7216
|
+
},
|
|
7217
|
+
createBody: {
|
|
7218
|
+
bundleSha256,
|
|
7219
|
+
bundleSizeBytes: gzipped.byteLength
|
|
7220
|
+
},
|
|
7221
|
+
uploadBody: new Blob([new Uint8Array(gzipped)], { type: "application/x-ndjson" }),
|
|
7222
|
+
fetchImpl: this.options.fetchImpl
|
|
7223
|
+
});
|
|
7224
|
+
await this.markUploaded(file, {
|
|
7225
|
+
bundleId,
|
|
7226
|
+
bundleSha256,
|
|
7227
|
+
bundleSizeBytes: gzipped.byteLength
|
|
7228
|
+
});
|
|
7229
|
+
span?.end("ok", {
|
|
7230
|
+
attrs: {
|
|
7231
|
+
bundleId,
|
|
7232
|
+
bundle_size_bytes: gzipped.byteLength
|
|
7233
|
+
}
|
|
7234
|
+
});
|
|
7235
|
+
return true;
|
|
7236
|
+
} catch (err) {
|
|
7237
|
+
span?.end("error", {
|
|
7238
|
+
attrs: {
|
|
7239
|
+
error_class: err instanceof Error ? err.name : "Error",
|
|
7240
|
+
error_message_present: err instanceof Error && Boolean(err.message)
|
|
7241
|
+
}
|
|
7242
|
+
});
|
|
7243
|
+
return false;
|
|
7244
|
+
}
|
|
7245
|
+
}
|
|
7246
|
+
uploadStatePath(file) {
|
|
7247
|
+
const stateDir = path14.join(this.options.machineDir, "trace-uploads");
|
|
7248
|
+
return path14.join(stateDir, `${path14.basename(file)}.uploaded.json`);
|
|
7249
|
+
}
|
|
7250
|
+
async isUploaded(file) {
|
|
7251
|
+
try {
|
|
7252
|
+
await stat3(this.uploadStatePath(file));
|
|
7253
|
+
return true;
|
|
7254
|
+
} catch {
|
|
7255
|
+
return false;
|
|
7256
|
+
}
|
|
7257
|
+
}
|
|
7258
|
+
async markUploaded(file, metadata) {
|
|
7259
|
+
const stateFile = this.uploadStatePath(file);
|
|
7260
|
+
await mkdir2(path14.dirname(stateFile), { recursive: true, mode: 448 });
|
|
7261
|
+
await writeFile2(stateFile, `${JSON.stringify({
|
|
7262
|
+
file: path14.basename(file),
|
|
7263
|
+
uploadedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7264
|
+
...metadata
|
|
7265
|
+
}, null, 2)}
|
|
7266
|
+
`, { mode: 384 });
|
|
7267
|
+
}
|
|
7268
|
+
};
|
|
7269
|
+
function sha256Hex(body) {
|
|
7270
|
+
return createHash2("sha256").update(body).digest("hex");
|
|
7271
|
+
}
|
|
7272
|
+
function readPositiveIntegerEnv2(name, fallback) {
|
|
7273
|
+
const value = process.env[name];
|
|
7274
|
+
if (!value) return fallback;
|
|
7275
|
+
const parsed = Number(value);
|
|
7276
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
7277
|
+
}
|
|
7278
|
+
|
|
5440
7279
|
// src/core.ts
|
|
5441
7280
|
var DAEMON_CLI_USAGE = "Usage: slock-daemon --server-url <url> --api-key <key>";
|
|
5442
7281
|
function parseDaemonCliArgs(args) {
|
|
@@ -5458,59 +7297,110 @@ function readDaemonVersion(moduleUrl = import.meta.url) {
|
|
|
5458
7297
|
}
|
|
5459
7298
|
}
|
|
5460
7299
|
function resolveChatBridgePath(moduleUrl = import.meta.url) {
|
|
5461
|
-
const dirname =
|
|
5462
|
-
const jsPath =
|
|
7300
|
+
const dirname = path15.dirname(fileURLToPath(moduleUrl));
|
|
7301
|
+
const jsPath = path15.resolve(dirname, "chat-bridge.js");
|
|
5463
7302
|
try {
|
|
5464
7303
|
accessSync(jsPath);
|
|
5465
7304
|
return jsPath;
|
|
5466
7305
|
} catch {
|
|
5467
|
-
return
|
|
7306
|
+
return path15.resolve(dirname, "chat-bridge.ts");
|
|
5468
7307
|
}
|
|
5469
7308
|
}
|
|
5470
7309
|
function resolveSlockCliPath(moduleUrl = import.meta.url) {
|
|
5471
|
-
const thisDir =
|
|
5472
|
-
const bundledDistPath =
|
|
7310
|
+
const thisDir = path15.dirname(fileURLToPath(moduleUrl));
|
|
7311
|
+
const bundledDistPath = path15.resolve(thisDir, "cli", "index.js");
|
|
5473
7312
|
try {
|
|
5474
7313
|
accessSync(bundledDistPath);
|
|
5475
7314
|
return bundledDistPath;
|
|
5476
7315
|
} catch {
|
|
5477
|
-
const workspaceDistPath =
|
|
7316
|
+
const workspaceDistPath = path15.resolve(thisDir, "..", "..", "cli", "dist", "index.js");
|
|
5478
7317
|
accessSync(workspaceDistPath);
|
|
5479
7318
|
return workspaceDistPath;
|
|
5480
7319
|
}
|
|
5481
7320
|
}
|
|
5482
|
-
function detectRuntimes() {
|
|
7321
|
+
function detectRuntimes(tracer = noopTracer) {
|
|
5483
7322
|
const ids = [];
|
|
5484
7323
|
const versions = {};
|
|
7324
|
+
const span = tracer.startSpan("daemon.runtime.detect", {
|
|
7325
|
+
surface: "daemon",
|
|
7326
|
+
kind: "internal",
|
|
7327
|
+
attrs: {
|
|
7328
|
+
known_runtime_count: RUNTIMES.length
|
|
7329
|
+
}
|
|
7330
|
+
});
|
|
5485
7331
|
for (const runtime of RUNTIMES) {
|
|
5486
7332
|
const driver = getDriver(runtime.id);
|
|
7333
|
+
let probeErrorPresent = false;
|
|
5487
7334
|
try {
|
|
5488
7335
|
if (driver.probe) {
|
|
5489
7336
|
const probe = driver.probe();
|
|
5490
7337
|
if (!probe.available) {
|
|
5491
7338
|
if (probe.version) versions[runtime.id] = probe.version;
|
|
7339
|
+
span.addEvent("daemon.runtime.detect.checked", {
|
|
7340
|
+
runtime: runtime.id,
|
|
7341
|
+
outcome: "unavailable",
|
|
7342
|
+
version_present: Boolean(probe.version),
|
|
7343
|
+
binary_path_present: false
|
|
7344
|
+
});
|
|
5492
7345
|
continue;
|
|
5493
7346
|
}
|
|
5494
7347
|
ids.push(runtime.id);
|
|
5495
7348
|
if (probe.version) versions[runtime.id] = probe.version;
|
|
7349
|
+
span.addEvent("daemon.runtime.detect.checked", {
|
|
7350
|
+
runtime: runtime.id,
|
|
7351
|
+
outcome: "available",
|
|
7352
|
+
version_present: Boolean(probe.version),
|
|
7353
|
+
binary_path_present: false
|
|
7354
|
+
});
|
|
5496
7355
|
continue;
|
|
5497
7356
|
}
|
|
5498
7357
|
} catch {
|
|
7358
|
+
probeErrorPresent = true;
|
|
5499
7359
|
}
|
|
5500
7360
|
const detectionBinaries = [runtime.binary];
|
|
7361
|
+
let detectedByPath = false;
|
|
5501
7362
|
for (const binary of detectionBinaries) {
|
|
5502
7363
|
const resolved = resolveCommandOnPath(binary);
|
|
5503
7364
|
if (!resolved) continue;
|
|
5504
7365
|
ids.push(runtime.id);
|
|
7366
|
+
detectedByPath = true;
|
|
5505
7367
|
const version = readCommandVersion(binary);
|
|
5506
7368
|
if (version) {
|
|
5507
7369
|
versions[runtime.id] = version;
|
|
5508
7370
|
}
|
|
7371
|
+
span.addEvent("daemon.runtime.detect.checked", {
|
|
7372
|
+
runtime: runtime.id,
|
|
7373
|
+
outcome: "available",
|
|
7374
|
+
version_present: Boolean(version),
|
|
7375
|
+
binary_path_present: true,
|
|
7376
|
+
probe_error_present: probeErrorPresent
|
|
7377
|
+
});
|
|
5509
7378
|
break;
|
|
5510
7379
|
}
|
|
7380
|
+
if (!detectedByPath) {
|
|
7381
|
+
span.addEvent("daemon.runtime.detect.checked", {
|
|
7382
|
+
runtime: runtime.id,
|
|
7383
|
+
outcome: "unavailable",
|
|
7384
|
+
version_present: false,
|
|
7385
|
+
binary_path_present: false,
|
|
7386
|
+
probe_error_present: probeErrorPresent
|
|
7387
|
+
});
|
|
7388
|
+
}
|
|
5511
7389
|
}
|
|
7390
|
+
span.end("ok", {
|
|
7391
|
+
attrs: {
|
|
7392
|
+
detected_runtime_count: ids.length
|
|
7393
|
+
}
|
|
7394
|
+
});
|
|
5512
7395
|
return { ids, versions };
|
|
5513
7396
|
}
|
|
7397
|
+
function readPositiveIntegerEnv3(name, fallback) {
|
|
7398
|
+
const raw = process.env[name];
|
|
7399
|
+
if (!raw) return fallback;
|
|
7400
|
+
const parsed = Number(raw);
|
|
7401
|
+
if (!Number.isFinite(parsed) || parsed < 1) return fallback;
|
|
7402
|
+
return Math.floor(parsed);
|
|
7403
|
+
}
|
|
5514
7404
|
function formatChannelTarget(msg) {
|
|
5515
7405
|
return msg.message.channel_type === "dm" ? `dm:@${msg.message.channel_name}` : `#${msg.message.channel_name}`;
|
|
5516
7406
|
}
|
|
@@ -5534,6 +7424,8 @@ function summarizeIncomingMessage(msg) {
|
|
|
5534
7424
|
return `(agent=${msg.agentId}, path=${msg.path})`;
|
|
5535
7425
|
case "agent:skills:list":
|
|
5536
7426
|
return `(agent=${msg.agentId}, runtime=${msg.runtime || "auto"})`;
|
|
7427
|
+
case "agent:activity_probe":
|
|
7428
|
+
return `(agent=${msg.agentId}, probe=${msg.probeId.slice(0, 8)}, purpose=${msg.purpose})`;
|
|
5537
7429
|
case "machine:workspace:delete":
|
|
5538
7430
|
return `(directory=${msg.directoryName})`;
|
|
5539
7431
|
case "machine:runtime_models:detect":
|
|
@@ -5558,14 +7450,18 @@ var DaemonCore = class {
|
|
|
5558
7450
|
connection;
|
|
5559
7451
|
reminderCache;
|
|
5560
7452
|
tracer;
|
|
7453
|
+
injectedTracer;
|
|
5561
7454
|
machineLock = null;
|
|
7455
|
+
localTraceSink = null;
|
|
7456
|
+
traceBundleUploader = null;
|
|
5562
7457
|
constructor(options) {
|
|
5563
7458
|
this.options = options;
|
|
5564
7459
|
this.daemonVersion = options.daemonVersion ?? readDaemonVersion();
|
|
5565
7460
|
this.chatBridgePath = options.chatBridgePath ?? resolveChatBridgePath();
|
|
5566
7461
|
this.slockCliPath = options.slockCliPath ?? resolveSlockCliPath();
|
|
5567
|
-
this.
|
|
7462
|
+
this.injectedTracer = Boolean(options.tracer);
|
|
5568
7463
|
this.tracer = options.tracer ?? noopTracer;
|
|
7464
|
+
this.runtimeDetector = options.runtimeDetector ?? (() => detectRuntimes(this.tracer));
|
|
5569
7465
|
this.reminderCache = new ReminderCache({
|
|
5570
7466
|
clock: options.reminderClock,
|
|
5571
7467
|
onFire: (job) => this.onReminderFire(job)
|
|
@@ -5575,7 +7471,8 @@ var DaemonCore = class {
|
|
|
5575
7471
|
dataDir: options.dataDir,
|
|
5576
7472
|
serverUrl: options.serverUrl,
|
|
5577
7473
|
defaultAgentEnvVarsProvider: options.defaultAgentEnvVarsProvider,
|
|
5578
|
-
slockCliPath: this.slockCliPath
|
|
7474
|
+
slockCliPath: this.slockCliPath,
|
|
7475
|
+
tracer: this.tracer
|
|
5579
7476
|
};
|
|
5580
7477
|
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
7478
|
const connectionFactory = options.connectionFactory ?? ((connOptions) => new DaemonConnection(connOptions));
|
|
@@ -5585,15 +7482,48 @@ var DaemonCore = class {
|
|
|
5585
7482
|
...options.connectionOptions,
|
|
5586
7483
|
onMessage: (msg) => this.handleMessage(msg),
|
|
5587
7484
|
onConnect: () => this.handleConnect(),
|
|
5588
|
-
onDisconnect: () => this.handleDisconnect()
|
|
7485
|
+
onDisconnect: () => this.handleDisconnect(),
|
|
7486
|
+
onTraceEvent: (name, attrs, status) => this.recordDaemonTrace(name, attrs, status)
|
|
5589
7487
|
});
|
|
5590
7488
|
this.connection = connection;
|
|
5591
7489
|
}
|
|
5592
7490
|
resolveMachineStateRoot() {
|
|
5593
7491
|
if (this.options.machineStateDir) return this.options.machineStateDir;
|
|
5594
|
-
if (this.options.dataDir) return
|
|
7492
|
+
if (this.options.dataDir) return path15.join(path15.dirname(this.options.dataDir), "machines");
|
|
5595
7493
|
return DEFAULT_MACHINE_STATE_ROOT;
|
|
5596
7494
|
}
|
|
7495
|
+
shouldEnableLocalTrace() {
|
|
7496
|
+
if (this.injectedTracer) return false;
|
|
7497
|
+
if (!this.options.localTrace) return false;
|
|
7498
|
+
return process.env.SLOCK_DAEMON_LOCAL_TRACE !== "0";
|
|
7499
|
+
}
|
|
7500
|
+
installLocalTraceSink(machineDir) {
|
|
7501
|
+
if (!this.shouldEnableLocalTrace()) return;
|
|
7502
|
+
this.localTraceSink = new LocalRotatingTraceSink({
|
|
7503
|
+
machineDir,
|
|
7504
|
+
maxFileBytes: this.options.localTraceMaxFileBytes ?? readPositiveIntegerEnv3("SLOCK_DAEMON_TRACE_MAX_FILE_BYTES", 5 * 1024 * 1024),
|
|
7505
|
+
maxFiles: this.options.localTraceMaxFiles ?? readPositiveIntegerEnv3("SLOCK_DAEMON_TRACE_MAX_FILES", 8)
|
|
7506
|
+
});
|
|
7507
|
+
this.tracer = new BasicTracer({
|
|
7508
|
+
sink: this.localTraceSink
|
|
7509
|
+
});
|
|
7510
|
+
this.agentManager.setTracer(this.tracer);
|
|
7511
|
+
}
|
|
7512
|
+
installTraceBundleUploader(machineDir) {
|
|
7513
|
+
if (!this.shouldEnableLocalTrace()) return;
|
|
7514
|
+
if (this.traceBundleUploader) return;
|
|
7515
|
+
const workerUrl = process.env.SLOCK_DAEMON_TRACE_UPLOAD_URL;
|
|
7516
|
+
if (!workerUrl) return;
|
|
7517
|
+
this.traceBundleUploader = new DaemonTraceBundleUploader({
|
|
7518
|
+
machineDir,
|
|
7519
|
+
serverUrl: this.options.serverUrl,
|
|
7520
|
+
apiKey: this.options.apiKey,
|
|
7521
|
+
workerUrl,
|
|
7522
|
+
tracer: this.tracer,
|
|
7523
|
+
currentFileProvider: () => this.localTraceSink?.getCurrentFile() ?? null
|
|
7524
|
+
});
|
|
7525
|
+
this.traceBundleUploader.start();
|
|
7526
|
+
}
|
|
5597
7527
|
start() {
|
|
5598
7528
|
logger.info("[Slock Daemon] Starting...");
|
|
5599
7529
|
if (!this.machineLock) {
|
|
@@ -5603,10 +7533,24 @@ var DaemonCore = class {
|
|
|
5603
7533
|
rootDir: this.resolveMachineStateRoot()
|
|
5604
7534
|
});
|
|
5605
7535
|
logger.info(`[Slock Daemon] Acquired machine lock: ${this.machineLock.lockDir}`);
|
|
7536
|
+
this.installLocalTraceSink(this.machineLock.machineDir);
|
|
7537
|
+
this.installTraceBundleUploader(this.machineLock.machineDir);
|
|
7538
|
+
const span = this.tracer.startSpan("daemon.lifecycle.start", {
|
|
7539
|
+
surface: "daemon",
|
|
7540
|
+
kind: "internal",
|
|
7541
|
+
attrs: {
|
|
7542
|
+
machine_dir_present: true,
|
|
7543
|
+
local_trace_enabled: this.shouldEnableLocalTrace()
|
|
7544
|
+
}
|
|
7545
|
+
});
|
|
7546
|
+
span.addEvent("daemon.machine_lock.acquired", { machine_dir_present: true });
|
|
7547
|
+
span.end("ok");
|
|
5606
7548
|
}
|
|
5607
7549
|
try {
|
|
5608
7550
|
this.connection.connect();
|
|
5609
7551
|
} catch (err) {
|
|
7552
|
+
this.traceBundleUploader?.stop();
|
|
7553
|
+
this.traceBundleUploader = null;
|
|
5610
7554
|
this.machineLock.release();
|
|
5611
7555
|
this.machineLock = null;
|
|
5612
7556
|
throw err;
|
|
@@ -5614,13 +7558,24 @@ var DaemonCore = class {
|
|
|
5614
7558
|
}
|
|
5615
7559
|
async stop() {
|
|
5616
7560
|
logger.info("[Slock Daemon] Shutting down...");
|
|
7561
|
+
const span = this.tracer.startSpan("daemon.lifecycle.stop", {
|
|
7562
|
+
surface: "daemon",
|
|
7563
|
+
kind: "internal",
|
|
7564
|
+
attrs: { machine_lock_present: Boolean(this.machineLock) }
|
|
7565
|
+
});
|
|
5617
7566
|
this.reminderCache.clear();
|
|
7567
|
+
this.traceBundleUploader?.stop();
|
|
7568
|
+
this.traceBundleUploader = null;
|
|
5618
7569
|
try {
|
|
5619
7570
|
await this.agentManager.stopAll();
|
|
7571
|
+
span.addEvent("daemon.agents.stopped");
|
|
5620
7572
|
} finally {
|
|
5621
7573
|
this.connection.disconnect();
|
|
7574
|
+
span.addEvent("daemon.connection.disconnect_requested");
|
|
5622
7575
|
this.machineLock?.release();
|
|
7576
|
+
if (this.machineLock) span.addEvent("daemon.machine_lock.released");
|
|
5623
7577
|
this.machineLock = null;
|
|
7578
|
+
span.end("ok");
|
|
5624
7579
|
}
|
|
5625
7580
|
}
|
|
5626
7581
|
get connected() {
|
|
@@ -5629,6 +7584,14 @@ var DaemonCore = class {
|
|
|
5629
7584
|
getRunningAgentIds() {
|
|
5630
7585
|
return this.agentManager.getRunningAgentIds();
|
|
5631
7586
|
}
|
|
7587
|
+
recordDaemonTrace(name, attrs, status = "ok") {
|
|
7588
|
+
const span = this.tracer.startSpan(name, {
|
|
7589
|
+
surface: "daemon",
|
|
7590
|
+
kind: "internal",
|
|
7591
|
+
attrs
|
|
7592
|
+
});
|
|
7593
|
+
span.end(status);
|
|
7594
|
+
}
|
|
5632
7595
|
handleMessage(msg) {
|
|
5633
7596
|
const summary = summarizeIncomingMessage(msg);
|
|
5634
7597
|
logger.info(`[Daemon] Received ${msg.type}${summary ? ` ${summary}` : ""}`);
|
|
@@ -5658,50 +7621,88 @@ var DaemonCore = class {
|
|
|
5658
7621
|
kind: "consumer",
|
|
5659
7622
|
attrs: {
|
|
5660
7623
|
agentId: msg.agentId,
|
|
7624
|
+
deliveryId: msg.deliveryId,
|
|
7625
|
+
delivery_correlation_id: msg.deliveryId ?? msg.message.message_id,
|
|
7626
|
+
messageId: msg.message.message_id,
|
|
7627
|
+
message_id_present: Boolean(msg.message.message_id),
|
|
5661
7628
|
seq: msg.seq
|
|
5662
7629
|
}
|
|
5663
7630
|
});
|
|
5664
7631
|
logger.info(`[Agent ${msg.agentId}] Delivery received (seq=${msg.seq}, from=@${msg.message.sender_name}, target=${formatChannelTarget(msg)})`);
|
|
5665
7632
|
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");
|
|
7633
|
+
span.addEvent("daemon.receive", { seq: msg.seq, deliveryId: msg.deliveryId });
|
|
7634
|
+
const accepted = this.agentManager.deliverMessage(msg.agentId, msg.message, { deliveryId: msg.deliveryId });
|
|
7635
|
+
span.addEvent("daemon.deliver_to_agent_manager", { accepted });
|
|
7636
|
+
if (!accepted) {
|
|
7637
|
+
span.end("ok", { attrs: { outcome: "not-accepted" } });
|
|
7638
|
+
break;
|
|
7639
|
+
}
|
|
5669
7640
|
const ackSeq = msg.seq > 0 ? msg.seq : msg.message.seq ?? 0;
|
|
5670
7641
|
span.addEvent("daemon.ack.sent", { seq: ackSeq });
|
|
5671
7642
|
this.connection.send({
|
|
5672
7643
|
type: "agent:deliver:ack",
|
|
5673
7644
|
agentId: msg.agentId,
|
|
5674
7645
|
seq: ackSeq,
|
|
5675
|
-
traceparent: formatTraceparent(span.context)
|
|
7646
|
+
traceparent: formatTraceparent(span.context),
|
|
7647
|
+
deliveryId: msg.deliveryId
|
|
5676
7648
|
});
|
|
5677
|
-
span.end("ok", { attrs: { outcome: "ack-sent", ackSeq } });
|
|
7649
|
+
span.end("ok", { attrs: { outcome: "ack-sent", ackSeq, deliveryId: msg.deliveryId } });
|
|
5678
7650
|
} catch (err) {
|
|
5679
|
-
span.end("error", { attrs: {
|
|
7651
|
+
span.end("error", { attrs: { error_class: err instanceof Error ? err.name : typeof err } });
|
|
5680
7652
|
throw err;
|
|
5681
7653
|
}
|
|
5682
7654
|
break;
|
|
5683
7655
|
}
|
|
5684
|
-
case "agent:runtime_profile:migration":
|
|
7656
|
+
case "agent:runtime_profile:migration": {
|
|
7657
|
+
const span = this.tracer.startSpan("daemon.runtime_profile.control.received", {
|
|
7658
|
+
parent: parseTraceparent(msg.traceparent),
|
|
7659
|
+
surface: "daemon",
|
|
7660
|
+
kind: "consumer",
|
|
7661
|
+
attrs: {
|
|
7662
|
+
agentId: msg.agentId,
|
|
7663
|
+
control_kind: "migration",
|
|
7664
|
+
key_present: Boolean(msg.migrationKey),
|
|
7665
|
+
launchId: msg.launchId || void 0
|
|
7666
|
+
}
|
|
7667
|
+
});
|
|
5685
7668
|
logger.info(`[Agent ${msg.agentId}] Runtime profile migration received (${msg.migrationKey})`);
|
|
5686
|
-
this.agentManager.deliverRuntimeProfileNotification(msg.agentId, msg.migrationKey, "migration", msg.message);
|
|
7669
|
+
const accepted = this.agentManager.deliverRuntimeProfileNotification(msg.agentId, msg.migrationKey, "migration", msg.message, formatTraceparent(span.context));
|
|
7670
|
+
span.end("ok", { attrs: { outcome: accepted ? "accepted" : "no_injection_path" } });
|
|
5687
7671
|
break;
|
|
5688
|
-
|
|
7672
|
+
}
|
|
7673
|
+
case "agent:runtime_profile:daemon_release_notice": {
|
|
7674
|
+
const span = this.tracer.startSpan("daemon.runtime_profile.control.received", {
|
|
7675
|
+
parent: parseTraceparent(msg.traceparent),
|
|
7676
|
+
surface: "daemon",
|
|
7677
|
+
kind: "consumer",
|
|
7678
|
+
attrs: {
|
|
7679
|
+
agentId: msg.agentId,
|
|
7680
|
+
control_kind: "daemon_release_notice",
|
|
7681
|
+
key_present: Boolean(msg.noticeKey),
|
|
7682
|
+
launchId: msg.launchId || void 0
|
|
7683
|
+
}
|
|
7684
|
+
});
|
|
5689
7685
|
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);
|
|
7686
|
+
const accepted = this.agentManager.deliverRuntimeProfileNotification(msg.agentId, msg.noticeKey, "daemon_release_notice", msg.message, formatTraceparent(span.context));
|
|
7687
|
+
span.end("ok", { attrs: { outcome: accepted ? "accepted" : "no_injection_path" } });
|
|
5691
7688
|
break;
|
|
7689
|
+
}
|
|
5692
7690
|
case "agent:workspace:list":
|
|
5693
7691
|
this.agentManager.getFileTree(msg.agentId, msg.dirPath).then((files) => {
|
|
5694
7692
|
this.connection.send({ type: "agent:workspace:file_tree", agentId: msg.agentId, files, dirPath: msg.dirPath });
|
|
5695
7693
|
});
|
|
5696
7694
|
break;
|
|
5697
7695
|
case "agent:workspace:read":
|
|
5698
|
-
this.agentManager.readFile(msg.agentId, msg.path).then(({ content, binary }) => {
|
|
7696
|
+
this.agentManager.readFile(msg.agentId, msg.path).then(({ content, binary, size, mimeType, encoding }) => {
|
|
5699
7697
|
this.connection.send({
|
|
5700
7698
|
type: "agent:workspace:file_content",
|
|
5701
7699
|
agentId: msg.agentId,
|
|
5702
7700
|
requestId: msg.requestId,
|
|
5703
7701
|
content,
|
|
5704
|
-
binary
|
|
7702
|
+
binary,
|
|
7703
|
+
size,
|
|
7704
|
+
mimeType,
|
|
7705
|
+
encoding
|
|
5705
7706
|
});
|
|
5706
7707
|
}).catch(() => {
|
|
5707
7708
|
this.connection.send({
|
|
@@ -5709,7 +7710,8 @@ var DaemonCore = class {
|
|
|
5709
7710
|
agentId: msg.agentId,
|
|
5710
7711
|
requestId: msg.requestId,
|
|
5711
7712
|
content: null,
|
|
5712
|
-
binary: false
|
|
7713
|
+
binary: false,
|
|
7714
|
+
size: 0
|
|
5713
7715
|
});
|
|
5714
7716
|
});
|
|
5715
7717
|
break;
|
|
@@ -5721,6 +7723,9 @@ var DaemonCore = class {
|
|
|
5721
7723
|
this.connection.send({ type: "agent:skills:list_result", agentId: msg.agentId, global: [], workspace: [] });
|
|
5722
7724
|
});
|
|
5723
7725
|
break;
|
|
7726
|
+
case "agent:activity_probe":
|
|
7727
|
+
this.agentManager.respondToActivityProbe(msg.agentId, msg.probeId);
|
|
7728
|
+
break;
|
|
5724
7729
|
case "machine:workspace:scan":
|
|
5725
7730
|
logger.info("[Daemon] Scanning all workspace directories");
|
|
5726
7731
|
this.agentManager.scanAllWorkspaces().then((directories) => {
|
|
@@ -5738,7 +7743,12 @@ var DaemonCore = class {
|
|
|
5738
7743
|
const detect = typeof driver?.detectModels === "function" ? driver.detectModels() : Promise.resolve(null);
|
|
5739
7744
|
Promise.resolve(detect).then((result) => {
|
|
5740
7745
|
if (result) {
|
|
5741
|
-
|
|
7746
|
+
const verified = driver.model.detectedModelsVerifiedAs;
|
|
7747
|
+
const models = result.models.map((model) => ({
|
|
7748
|
+
...model,
|
|
7749
|
+
verified: model.verified ?? verified
|
|
7750
|
+
}));
|
|
7751
|
+
this.connection.send({ type: "machine:runtime_models:result", requestId: msg.requestId, models, default: result.default });
|
|
5742
7752
|
} else {
|
|
5743
7753
|
this.connection.send({ type: "machine:runtime_models:result", requestId: msg.requestId, error: "unsupported" });
|
|
5744
7754
|
}
|
|
@@ -5777,35 +7787,59 @@ var DaemonCore = class {
|
|
|
5777
7787
|
const { ids: runtimes, versions: runtimeVersions } = this.runtimeDetector();
|
|
5778
7788
|
const runtimeInfo = runtimes.map((id) => runtimeVersions[id] ? `${id} (${runtimeVersions[id]})` : id);
|
|
5779
7789
|
logger.info(`[Daemon] Detected runtimes: ${runtimeInfo.join(", ") || "none"}`);
|
|
7790
|
+
const runningAgentIds = this.agentManager.getRunningAgentIds();
|
|
7791
|
+
const idleAgentSessions = this.agentManager.getIdleAgentSessionIds();
|
|
7792
|
+
const runtimeProfileReports = this.agentManager.getAgentRuntimeProfileReports();
|
|
5780
7793
|
this.connection.send({
|
|
5781
7794
|
type: "ready",
|
|
5782
7795
|
capabilities: ["agent:start", "agent:stop", "agent:deliver", "workspace:files"],
|
|
5783
7796
|
runtimes,
|
|
5784
|
-
runningAgents:
|
|
5785
|
-
hostname: this.options.hostname ??
|
|
5786
|
-
os: this.options.osDescription ?? `${
|
|
7797
|
+
runningAgents: runningAgentIds,
|
|
7798
|
+
hostname: this.options.hostname ?? os7.hostname(),
|
|
7799
|
+
os: this.options.osDescription ?? `${os7.platform()} ${os7.arch()}`,
|
|
5787
7800
|
daemonVersion: this.daemonVersion
|
|
5788
7801
|
});
|
|
5789
|
-
|
|
7802
|
+
this.recordDaemonTrace("daemon.ready.sent", {
|
|
7803
|
+
runtimes_count: runtimes.length,
|
|
7804
|
+
running_agents_count: runningAgentIds.length,
|
|
7805
|
+
idle_agents_count: idleAgentSessions.length,
|
|
7806
|
+
runtime_profile_reports_count: runtimeProfileReports.length,
|
|
7807
|
+
daemon_version_present: Boolean(this.daemonVersion)
|
|
7808
|
+
});
|
|
7809
|
+
for (const agentId of runningAgentIds) {
|
|
5790
7810
|
const sessionId = this.agentManager.getAgentSessionId(agentId);
|
|
5791
7811
|
const launchId = this.agentManager.getAgentLaunchId(agentId);
|
|
5792
7812
|
if (sessionId) {
|
|
5793
7813
|
this.connection.send({ type: "agent:session", agentId, sessionId, launchId: launchId || void 0 });
|
|
5794
7814
|
}
|
|
5795
7815
|
}
|
|
5796
|
-
for (const { agentId, sessionId, launchId } of
|
|
7816
|
+
for (const { agentId, sessionId, launchId } of idleAgentSessions) {
|
|
5797
7817
|
this.connection.send({ type: "agent:session", agentId, sessionId, launchId: launchId || void 0 });
|
|
5798
7818
|
}
|
|
5799
|
-
for (const report of
|
|
7819
|
+
for (const report of runtimeProfileReports) {
|
|
7820
|
+
const span = this.tracer.startSpan("daemon.runtime_profile.report.sent", {
|
|
7821
|
+
surface: "daemon",
|
|
7822
|
+
kind: "producer",
|
|
7823
|
+
attrs: {
|
|
7824
|
+
agentId: report.agentId,
|
|
7825
|
+
launchId: report.launchId || void 0,
|
|
7826
|
+
runtime: report.facts.runtime,
|
|
7827
|
+
model_present: Boolean(report.facts.model),
|
|
7828
|
+
session_ref_present: Boolean(report.facts.sessionRef),
|
|
7829
|
+
workspace_ref_present: Boolean(report.facts.workspaceRef || report.facts.workspacePathRef)
|
|
7830
|
+
}
|
|
7831
|
+
});
|
|
5800
7832
|
this.connection.send({
|
|
5801
7833
|
type: "agent:runtime_profile",
|
|
5802
7834
|
agentId: report.agentId,
|
|
5803
7835
|
facts: report.facts,
|
|
5804
|
-
launchId: report.launchId || void 0
|
|
7836
|
+
launchId: report.launchId || void 0,
|
|
7837
|
+
traceparent: formatTraceparent(span.context)
|
|
5805
7838
|
});
|
|
7839
|
+
span.end("ok");
|
|
5806
7840
|
}
|
|
5807
|
-
const agentsForSnapshot = new Set(
|
|
5808
|
-
for (const { agentId } of
|
|
7841
|
+
const agentsForSnapshot = new Set(runningAgentIds);
|
|
7842
|
+
for (const { agentId } of idleAgentSessions) {
|
|
5809
7843
|
agentsForSnapshot.add(agentId);
|
|
5810
7844
|
}
|
|
5811
7845
|
for (const agentId of agentsForSnapshot) {
|
|
@@ -5815,6 +7849,10 @@ var DaemonCore = class {
|
|
|
5815
7849
|
}
|
|
5816
7850
|
handleDisconnect() {
|
|
5817
7851
|
logger.warn("[Daemon] Lost connection \u2014 agents continue running locally");
|
|
7852
|
+
this.recordDaemonTrace("daemon.connection.local_disconnect_observed", {
|
|
7853
|
+
running_agents_count: this.agentManager.getRunningAgentIds().length,
|
|
7854
|
+
idle_agents_count: this.agentManager.getIdleAgentSessionIds().length
|
|
7855
|
+
}, "cancelled");
|
|
5818
7856
|
this.options.lifecycleHooks?.onDisconnect?.();
|
|
5819
7857
|
}
|
|
5820
7858
|
};
|