@slock-ai/daemon 0.42.0 → 0.44.2
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 +1 -1
- package/dist/{chunk-RNEIFBXW.js → chunk-NM2MFLQ7.js} +959 -114
- package/dist/cli/index.js +269 -53
- package/dist/core.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
|
@@ -4,8 +4,8 @@ import {
|
|
|
4
4
|
} from "./chunk-JG7ONJZ6.js";
|
|
5
5
|
|
|
6
6
|
// src/core.ts
|
|
7
|
-
import
|
|
8
|
-
import
|
|
7
|
+
import path13 from "path";
|
|
8
|
+
import os6 from "os";
|
|
9
9
|
import { createRequire } from "module";
|
|
10
10
|
import { accessSync } from "fs";
|
|
11
11
|
import { fileURLToPath } from "url";
|
|
@@ -545,8 +545,57 @@ var RUNTIMES = [
|
|
|
545
545
|
{ id: "kimi", displayName: "Kimi CLI", binary: "kimi", supported: true },
|
|
546
546
|
{ id: "copilot", displayName: "Copilot CLI", binary: "copilot", supported: true },
|
|
547
547
|
{ id: "cursor", displayName: "Cursor CLI", binary: "cursor-agent", supported: true },
|
|
548
|
-
{ id: "gemini", displayName: "Gemini CLI", binary: "gemini", supported: true }
|
|
548
|
+
{ id: "gemini", displayName: "Gemini CLI", binary: "gemini", supported: true },
|
|
549
|
+
{ id: "opencode", displayName: "OpenCode", binary: "opencode", supported: true }
|
|
549
550
|
];
|
|
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
|
+
};
|
|
550
599
|
var PLAN_CONFIG = {
|
|
551
600
|
free: {
|
|
552
601
|
displayName: "Hobby",
|
|
@@ -584,8 +633,8 @@ var DISPLAY_PLAN_CONFIG = {
|
|
|
584
633
|
// src/agentProcessManager.ts
|
|
585
634
|
import { mkdirSync as mkdirSync4, readdirSync, statSync, writeFileSync as writeFileSync7 } from "fs";
|
|
586
635
|
import { mkdir, writeFile, access, readdir as readdir2, stat as stat2, readFile, rm as rm2 } from "fs/promises";
|
|
587
|
-
import
|
|
588
|
-
import
|
|
636
|
+
import path11 from "path";
|
|
637
|
+
import os4 from "os";
|
|
589
638
|
|
|
590
639
|
// src/drivers/claude.ts
|
|
591
640
|
import { spawn } from "child_process";
|
|
@@ -631,6 +680,9 @@ function buildPrompt(config, variant, opts) {
|
|
|
631
680
|
const taskCreateCmd = isCli ? "`slock task create`" : `\`${t("create_tasks")}\``;
|
|
632
681
|
const taskUpdateCmd = isCli ? "`slock task update`" : `\`${t("update_task_status")}\``;
|
|
633
682
|
const serverInfoCmd = isCli ? "`slock server info`" : `\`${t("list_server")}\``;
|
|
683
|
+
const scheduleReminderCmd = isCli ? "`slock reminder schedule`" : `\`${t("schedule_reminder")}\``;
|
|
684
|
+
const listRemindersCmd = isCli ? "`slock reminder list`" : `\`${t("list_reminders")}\``;
|
|
685
|
+
const cancelReminderCmd = isCli ? "`slock reminder cancel`" : `\`${t("cancel_reminder")}\``;
|
|
634
686
|
const messageDeliveryText = opts.includeStdinNotificationSection ? "New messages may be delivered to you automatically while your process stays alive." : "The daemon will automatically restart you when new messages arrive.";
|
|
635
687
|
const criticalRules = isCli ? [
|
|
636
688
|
"- Always communicate through `slock` CLI commands. This is your only output channel.",
|
|
@@ -681,15 +733,11 @@ Use the \`slock\` CLI for chat / task / attachment operations. The daemon inject
|
|
|
681
733
|
14. **\`slock attachment upload\`** \u2014 Upload a file to attach to a message. Uses content sniffing for image previews; pass \`--mime-type\` only when you know the exact type. Returns an attachment ID to pass to \`slock message send\`.
|
|
682
734
|
15. **\`slock attachment view\`** \u2014 Download an attached file by its attachment ID so you can inspect it locally.
|
|
683
735
|
16. **\`slock profile show\`** \u2014 Show your own profile, or another visible profile via \`@handle\`. Mirrors the canonical Slock profile view.
|
|
684
|
-
17. **\`slock profile update\`** \u2014 Update your own profile.
|
|
736
|
+
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.
|
|
685
737
|
18. **\`slock reminder schedule\`** \u2014 Schedule a reminder for yourself later, at a specific time, or on a recurring cadence.
|
|
686
738
|
19. **\`slock reminder list\`** \u2014 List your reminders.
|
|
687
739
|
20. **\`slock reminder cancel\`** \u2014 Cancel one of your reminders by ID.
|
|
688
740
|
|
|
689
|
-
When a user asks you to remind them later, at a specific time, or on a recurring schedule, prefer the reminder commands instead of relying on MEMORY or manual follow-up.
|
|
690
|
-
Do not use runtime-native wake or cron tools such as ScheduleWakeup or CronCreate for user-visible reminders; use \`slock reminder schedule\` so reminders stay anchored, observable, and cancelable in Slock.
|
|
691
|
-
For agent-created reminders, first resolve the anchor message from the current conversation and pass its \`msgId\` explicitly. If you cannot resolve a message id, do not create the reminder.
|
|
692
|
-
|
|
693
741
|
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:
|
|
694
742
|
- failure \u2192 stderr \`{"ok":false,"code":"...","message":"..."}\` with non-zero exit
|
|
695
743
|
|
|
@@ -712,7 +760,15 @@ You have MCP tools from the "chat" server. Use ONLY these for communication:
|
|
|
712
760
|
10. **\`${t("unclaim_task")}\`** \u2014 Release your claim on a task.
|
|
713
761
|
11. **${taskUpdateCmd}** \u2014 Change a task's status (e.g. to in_review or done).
|
|
714
762
|
12. **\`${t("upload_file")}\`** \u2014 Upload a file to attach to a message. Returns an attachment ID to pass to ${sendCmd}.
|
|
715
|
-
13. **\`${t("view_file")}\`** \u2014 Download an attached file by its attachment ID so you can inspect it locally
|
|
763
|
+
13. **\`${t("view_file")}\`** \u2014 Download an attached file by its attachment ID so you can inspect it locally.
|
|
764
|
+
14. **${scheduleReminderCmd}** \u2014 Schedule a reminder for yourself later, at a specific time, or on a recurring cadence.
|
|
765
|
+
15. **${listRemindersCmd}** \u2014 List your reminders.
|
|
766
|
+
16. **${cancelReminderCmd}** \u2014 Cancel one of your reminders by ID.`;
|
|
767
|
+
const reminderSection = `### Reminders
|
|
768
|
+
|
|
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
|
+
Use ${scheduleReminderCmd} rather than runtime-native wake or cron tools such as ScheduleWakeup or CronCreate for user-visible reminders, so reminders stay anchored, observable, and cancelable in Slock.
|
|
771
|
+
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.`;
|
|
716
772
|
const sendingMessagesSection = isCli ? `### Sending messages
|
|
717
773
|
|
|
718
774
|
- **Reply to a channel**: \`slock message send --target "#channel-name" <<'EOF'\` followed by the message body and \`EOF\`
|
|
@@ -782,6 +838,11 @@ To jump directly to a specific hit with nearby context, use \`slock message read
|
|
|
782
838
|
Use ${readCmd} with the \`channel\` parameter set to \`"#channel-name"\`, \`"dm:@peer-name"\`, or a thread target like \`"#channel:shortid"\`.
|
|
783
839
|
|
|
784
840
|
To jump directly to a specific hit with nearby context, pass \`around\` set to a message ID or seq number.`;
|
|
841
|
+
const historicalReferenceSection = isCli ? `### Historical references
|
|
842
|
+
|
|
843
|
+
When a user refers to prior Slock discussion and the relevant context is not already available, first use \`slock message search\` and \`slock message read\` to find the original thread, decision, or owner before answering. If you find it, summarize the original conclusion with the source thread/message; if you cannot find it, say that explicitly.` : `### Historical references
|
|
844
|
+
|
|
845
|
+
When a user refers to prior Slock discussion and the relevant context is not already available, first use \`${t("search_messages")}\` and ${readCmd} to find the original thread, decision, or owner before answering. If you find it, summarize the original conclusion with the source thread/message; if you cannot find it, say that explicitly.`;
|
|
785
846
|
const tasksSection = isCli ? `### Tasks
|
|
786
847
|
|
|
787
848
|
When someone sends a message that asks you to do something \u2014 fix a bug, write code, review a PR, deploy, investigate an issue \u2014 that is work. Claim it before you start.
|
|
@@ -931,6 +992,8 @@ Header fields:
|
|
|
931
992
|
|
|
932
993
|
${sendingMessagesSection}
|
|
933
994
|
|
|
995
|
+
${reminderSection}
|
|
996
|
+
|
|
934
997
|
${threadsSection}
|
|
935
998
|
|
|
936
999
|
${discoverySection}
|
|
@@ -939,6 +1002,8 @@ ${channelAwarenessSection}
|
|
|
939
1002
|
|
|
940
1003
|
${readingHistorySection}
|
|
941
1004
|
|
|
1005
|
+
${historicalReferenceSection}
|
|
1006
|
+
|
|
942
1007
|
${tasksSection}
|
|
943
1008
|
|
|
944
1009
|
### Splitting tasks for parallel execution
|
|
@@ -996,7 +1061,7 @@ When writing a URL next to non-ASCII punctuation (Chinese, Japanese, etc.), alwa
|
|
|
996
1061
|
|
|
997
1062
|
## Workspace & Memory
|
|
998
1063
|
|
|
999
|
-
Your working directory (cwd) is your **persistent workspace
|
|
1064
|
+
Your working directory (cwd) is your **persistent, agent-owned workspace**; files you create here survive across sessions. Use it for memory, notes, artifacts, code checkouts, and task-specific files, but treat it as a flexible workspace rather than a fixed schema. Keep **MEMORY.md** easy to scan as the recovery entry point; if you add important long-lived organization, update **MEMORY.md** or a note index so future sessions can find it. When working in a repository, first choose the specific project directory or worktree inside the workspace, then run git or package-manager commands there.
|
|
1000
1065
|
|
|
1001
1066
|
### MEMORY.md \u2014 Your Memory Index (CRITICAL)
|
|
1002
1067
|
|
|
@@ -1263,8 +1328,25 @@ function probeClaude(deps = {}) {
|
|
|
1263
1328
|
}
|
|
1264
1329
|
var ClaudeDriver = class {
|
|
1265
1330
|
id = "claude";
|
|
1331
|
+
lifecycle = {
|
|
1332
|
+
kind: "persistent",
|
|
1333
|
+
stdin: "gated",
|
|
1334
|
+
inFlightWake: "queue"
|
|
1335
|
+
};
|
|
1336
|
+
communication = {
|
|
1337
|
+
chat: "slock_cli",
|
|
1338
|
+
runtimeControl: "mcp_runtime_actions"
|
|
1339
|
+
};
|
|
1340
|
+
session = {
|
|
1341
|
+
recovery: "resume_or_fresh"
|
|
1342
|
+
};
|
|
1343
|
+
model = {
|
|
1344
|
+
detectedModelsVerifiedAs: "launchable",
|
|
1345
|
+
toLaunchSpec: (modelId) => ({ args: ["--model", modelId] })
|
|
1346
|
+
};
|
|
1266
1347
|
supportsStdinNotification = true;
|
|
1267
1348
|
mcpToolPrefix = "mcp__chat__";
|
|
1349
|
+
usesSlockCliForCommunication = true;
|
|
1268
1350
|
// Claude Code supports same-turn steering, but raw stdin injection at an
|
|
1269
1351
|
// arbitrary busy instant can collide with active signed thinking blocks. The
|
|
1270
1352
|
// daemon therefore gates busy delivery on Claude stream-json boundaries.
|
|
@@ -1566,8 +1648,25 @@ function joinReasoningText(item) {
|
|
|
1566
1648
|
}
|
|
1567
1649
|
var CodexDriver = class {
|
|
1568
1650
|
id = "codex";
|
|
1651
|
+
lifecycle = {
|
|
1652
|
+
kind: "persistent",
|
|
1653
|
+
stdin: "direct",
|
|
1654
|
+
inFlightWake: "steer"
|
|
1655
|
+
};
|
|
1656
|
+
communication = {
|
|
1657
|
+
chat: "slock_cli",
|
|
1658
|
+
runtimeControl: "mcp_runtime_actions"
|
|
1659
|
+
};
|
|
1660
|
+
session = {
|
|
1661
|
+
recovery: "resume_or_fresh"
|
|
1662
|
+
};
|
|
1663
|
+
model = {
|
|
1664
|
+
detectedModelsVerifiedAs: "launchable",
|
|
1665
|
+
toLaunchSpec: (modelId) => ({ params: { model: modelId } })
|
|
1666
|
+
};
|
|
1569
1667
|
supportsStdinNotification = true;
|
|
1570
1668
|
mcpToolPrefix = "mcp_chat_";
|
|
1669
|
+
usesSlockCliForCommunication = true;
|
|
1571
1670
|
busyDeliveryMode = "direct";
|
|
1572
1671
|
supportsNativeStandingPrompt = true;
|
|
1573
1672
|
probe() {
|
|
@@ -1962,7 +2061,7 @@ function detectCodexModels(home = os.homedir()) {
|
|
|
1962
2061
|
if (entry?.visibility && entry.visibility !== "public") continue;
|
|
1963
2062
|
if (entry?.supported_in_api === false) continue;
|
|
1964
2063
|
const label = typeof entry?.display_name === "string" && entry.display_name.length > 0 ? entry.display_name : slug;
|
|
1965
|
-
models.push({ id: slug, label });
|
|
2064
|
+
models.push({ id: slug, label, verified: "launchable" });
|
|
1966
2065
|
}
|
|
1967
2066
|
} catch {
|
|
1968
2067
|
return null;
|
|
@@ -1984,6 +2083,23 @@ import path5 from "path";
|
|
|
1984
2083
|
import { writeFileSync as writeFileSync3 } from "fs";
|
|
1985
2084
|
var CopilotDriver = class {
|
|
1986
2085
|
id = "copilot";
|
|
2086
|
+
lifecycle = {
|
|
2087
|
+
kind: "per_turn",
|
|
2088
|
+
start: "immediate",
|
|
2089
|
+
exit: "natural",
|
|
2090
|
+
inFlightWake: "spawn_new"
|
|
2091
|
+
};
|
|
2092
|
+
communication = {
|
|
2093
|
+
chat: "mcp_chat_bridge",
|
|
2094
|
+
runtimeControl: "mcp_runtime_actions"
|
|
2095
|
+
};
|
|
2096
|
+
session = {
|
|
2097
|
+
recovery: "resume_or_fresh"
|
|
2098
|
+
};
|
|
2099
|
+
model = {
|
|
2100
|
+
detectedModelsVerifiedAs: "launchable",
|
|
2101
|
+
toLaunchSpec: (modelId) => ({ args: ["--model", modelId] })
|
|
2102
|
+
};
|
|
1987
2103
|
supportsStdinNotification = false;
|
|
1988
2104
|
mcpToolPrefix = "";
|
|
1989
2105
|
busyDeliveryMode = "none";
|
|
@@ -2117,11 +2233,28 @@ var CopilotDriver = class {
|
|
|
2117
2233
|
};
|
|
2118
2234
|
|
|
2119
2235
|
// src/drivers/cursor.ts
|
|
2120
|
-
import { spawn as spawn4 } from "child_process";
|
|
2236
|
+
import { spawn as spawn4, spawnSync } from "child_process";
|
|
2121
2237
|
import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync2, existsSync as existsSync3 } from "fs";
|
|
2122
2238
|
import path6 from "path";
|
|
2123
2239
|
var CursorDriver = class {
|
|
2124
2240
|
id = "cursor";
|
|
2241
|
+
lifecycle = {
|
|
2242
|
+
kind: "per_turn",
|
|
2243
|
+
start: "immediate",
|
|
2244
|
+
exit: "natural",
|
|
2245
|
+
inFlightWake: "spawn_new"
|
|
2246
|
+
};
|
|
2247
|
+
communication = {
|
|
2248
|
+
chat: "mcp_chat_bridge",
|
|
2249
|
+
runtimeControl: "mcp_runtime_actions"
|
|
2250
|
+
};
|
|
2251
|
+
session = {
|
|
2252
|
+
recovery: "resume_or_fresh"
|
|
2253
|
+
};
|
|
2254
|
+
model = {
|
|
2255
|
+
detectedModelsVerifiedAs: "launchable",
|
|
2256
|
+
toLaunchSpec: (modelId) => ({ args: ["--model", modelId] })
|
|
2257
|
+
};
|
|
2125
2258
|
supportsStdinNotification = false;
|
|
2126
2259
|
mcpToolPrefix = "mcp__chat__";
|
|
2127
2260
|
busyDeliveryMode = "none";
|
|
@@ -2234,14 +2367,83 @@ var CursorDriver = class {
|
|
|
2234
2367
|
messageNotificationStyle: "poll"
|
|
2235
2368
|
});
|
|
2236
2369
|
}
|
|
2370
|
+
async detectModels() {
|
|
2371
|
+
return detectCursorModels();
|
|
2372
|
+
}
|
|
2237
2373
|
};
|
|
2374
|
+
function parseCursorModelsOutput(output) {
|
|
2375
|
+
const stripAnsi = (value) => value.replace(/\u001b\[[0-9;]*m/g, "");
|
|
2376
|
+
const models = [];
|
|
2377
|
+
let defaultModel;
|
|
2378
|
+
for (const rawLine of stripAnsi(output).split(/\r?\n/)) {
|
|
2379
|
+
const line = rawLine.trim();
|
|
2380
|
+
if (!line || /^available models$/i.test(line) || /^tip:/i.test(line)) continue;
|
|
2381
|
+
if (/^no models available/i.test(line) || /^failed to load models:/i.test(line)) continue;
|
|
2382
|
+
let modelLine = line;
|
|
2383
|
+
const markerMatch = modelLine.match(/\s+\(([^)]+)\)$/);
|
|
2384
|
+
const markers = markerMatch?.[1]?.split(",").map((part) => part.trim().toLowerCase()) ?? [];
|
|
2385
|
+
if (markers.length > 0 && markers.every((part) => part === "current" || part === "default")) {
|
|
2386
|
+
const markerStart = markerMatch?.index ?? modelLine.length;
|
|
2387
|
+
modelLine = modelLine.slice(0, markerStart).trim();
|
|
2388
|
+
}
|
|
2389
|
+
const match = modelLine.match(/^(\S+)(?:\s+-\s+(.+))?$/);
|
|
2390
|
+
if (!match) continue;
|
|
2391
|
+
const id = match[1]?.trim();
|
|
2392
|
+
if (!id || id.startsWith("-")) continue;
|
|
2393
|
+
const label = match[2]?.trim() || id;
|
|
2394
|
+
models.push({ id, label, verified: "launchable" });
|
|
2395
|
+
if (markers.includes("default")) defaultModel = id;
|
|
2396
|
+
}
|
|
2397
|
+
if (models.length === 0) return null;
|
|
2398
|
+
return { models, default: defaultModel };
|
|
2399
|
+
}
|
|
2400
|
+
function detectCursorModels(runCommand = runCursorModelsCommand) {
|
|
2401
|
+
const result = runCommand();
|
|
2402
|
+
if (result.error || result.status !== 0) return null;
|
|
2403
|
+
return parseCursorModelsOutput(String(result.stdout || ""));
|
|
2404
|
+
}
|
|
2405
|
+
function runCursorModelsCommand() {
|
|
2406
|
+
return spawnSync("cursor-agent", ["models"], {
|
|
2407
|
+
env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" },
|
|
2408
|
+
encoding: "utf8",
|
|
2409
|
+
timeout: 5e3
|
|
2410
|
+
});
|
|
2411
|
+
}
|
|
2238
2412
|
|
|
2239
2413
|
// src/drivers/gemini.ts
|
|
2240
2414
|
import { spawn as spawn5 } from "child_process";
|
|
2241
2415
|
import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync3, existsSync as existsSync4 } from "fs";
|
|
2242
2416
|
import path7 from "path";
|
|
2417
|
+
function buildGeminiSpawnEnv(ctx) {
|
|
2418
|
+
return {
|
|
2419
|
+
...process.env,
|
|
2420
|
+
FORCE_COLOR: "0",
|
|
2421
|
+
NO_COLOR: "1",
|
|
2422
|
+
// Gemini CLI's trusted-workspace gate breaks our managed headless flow
|
|
2423
|
+
// unless we explicitly trust the daemon-owned agent workspace.
|
|
2424
|
+
GEMINI_CLI_TRUST_WORKSPACE: "true",
|
|
2425
|
+
...ctx.config.envVars || {}
|
|
2426
|
+
};
|
|
2427
|
+
}
|
|
2243
2428
|
var GeminiDriver = class {
|
|
2244
2429
|
id = "gemini";
|
|
2430
|
+
lifecycle = {
|
|
2431
|
+
kind: "per_turn",
|
|
2432
|
+
start: "immediate",
|
|
2433
|
+
exit: "natural",
|
|
2434
|
+
inFlightWake: "spawn_new"
|
|
2435
|
+
};
|
|
2436
|
+
communication = {
|
|
2437
|
+
chat: "mcp_chat_bridge",
|
|
2438
|
+
runtimeControl: "mcp_runtime_actions"
|
|
2439
|
+
};
|
|
2440
|
+
session = {
|
|
2441
|
+
recovery: "resume_or_fresh"
|
|
2442
|
+
};
|
|
2443
|
+
model = {
|
|
2444
|
+
detectedModelsVerifiedAs: "launchable",
|
|
2445
|
+
toLaunchSpec: (modelId) => ({ args: ["--model", modelId] })
|
|
2446
|
+
};
|
|
2245
2447
|
supportsStdinNotification = false;
|
|
2246
2448
|
mcpToolPrefix = "";
|
|
2247
2449
|
busyDeliveryMode = "none";
|
|
@@ -2279,7 +2481,7 @@ var GeminiDriver = class {
|
|
|
2279
2481
|
if (ctx.config.sessionId) {
|
|
2280
2482
|
args.push("--resume", ctx.config.sessionId);
|
|
2281
2483
|
}
|
|
2282
|
-
const spawnEnv =
|
|
2484
|
+
const spawnEnv = buildGeminiSpawnEnv(ctx);
|
|
2283
2485
|
const proc = spawn5("gemini", args, {
|
|
2284
2486
|
cwd: ctx.workingDirectory,
|
|
2285
2487
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -2371,6 +2573,22 @@ function parseToolArguments(raw) {
|
|
|
2371
2573
|
}
|
|
2372
2574
|
var KimiDriver = class {
|
|
2373
2575
|
id = "kimi";
|
|
2576
|
+
lifecycle = {
|
|
2577
|
+
kind: "persistent",
|
|
2578
|
+
stdin: "direct",
|
|
2579
|
+
inFlightWake: "steer"
|
|
2580
|
+
};
|
|
2581
|
+
communication = {
|
|
2582
|
+
chat: "mcp_chat_bridge",
|
|
2583
|
+
runtimeControl: "mcp_runtime_actions"
|
|
2584
|
+
};
|
|
2585
|
+
session = {
|
|
2586
|
+
recovery: "resume_or_fresh"
|
|
2587
|
+
};
|
|
2588
|
+
model = {
|
|
2589
|
+
detectedModelsVerifiedAs: "launchable",
|
|
2590
|
+
toLaunchSpec: (modelId) => ({ args: ["--model", modelId] })
|
|
2591
|
+
};
|
|
2374
2592
|
supportsStdinNotification = true;
|
|
2375
2593
|
mcpToolPrefix = "";
|
|
2376
2594
|
busyDeliveryMode = "direct";
|
|
@@ -2571,7 +2789,7 @@ function detectKimiModels(home = os2.homedir()) {
|
|
|
2571
2789
|
let key = match[1].trim();
|
|
2572
2790
|
if (key.startsWith('"') && key.endsWith('"')) key = key.slice(1, -1);
|
|
2573
2791
|
if (!key) continue;
|
|
2574
|
-
models.push({ id: key, label: key });
|
|
2792
|
+
models.push({ id: key, label: key, verified: "launchable" });
|
|
2575
2793
|
}
|
|
2576
2794
|
void sectionRe;
|
|
2577
2795
|
if (models.length === 0) return null;
|
|
@@ -2581,6 +2799,314 @@ function detectKimiModels(home = os2.homedir()) {
|
|
|
2581
2799
|
return { models, default: defaultModel };
|
|
2582
2800
|
}
|
|
2583
2801
|
|
|
2802
|
+
// src/drivers/opencode.ts
|
|
2803
|
+
import { spawn as spawn7 } from "child_process";
|
|
2804
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
2805
|
+
import os3 from "os";
|
|
2806
|
+
import path9 from "path";
|
|
2807
|
+
var CHAT_MCP_SERVER_NAME = "chat";
|
|
2808
|
+
var CHAT_MCP_TOOL_PREFIX = `${CHAT_MCP_SERVER_NAME}_`;
|
|
2809
|
+
var SLOCK_AGENT_NAME = "slock";
|
|
2810
|
+
var NO_MESSAGE_PROMPT = "No new messages are pending. Stop now.";
|
|
2811
|
+
var FIRST_MESSAGE_TASK_PREFIX = "First message task (system-triggered):";
|
|
2812
|
+
var MIN_SUPPORTED_OPENCODE_VERSION = "1.14.30";
|
|
2813
|
+
function buildChatBridgeCommand(ctx) {
|
|
2814
|
+
const isTsSource = ctx.chatBridgePath.endsWith(".ts");
|
|
2815
|
+
return [
|
|
2816
|
+
isTsSource ? "npx" : "node",
|
|
2817
|
+
...isTsSource ? ["tsx", ctx.chatBridgePath] : [ctx.chatBridgePath],
|
|
2818
|
+
"--agent-id",
|
|
2819
|
+
ctx.agentId,
|
|
2820
|
+
"--server-url",
|
|
2821
|
+
ctx.config.serverUrl,
|
|
2822
|
+
"--auth-token",
|
|
2823
|
+
ctx.config.authToken || ctx.daemonApiKey,
|
|
2824
|
+
"--runtime",
|
|
2825
|
+
"opencode",
|
|
2826
|
+
...ctx.launchId ? ["--launch-id", ctx.launchId] : [],
|
|
2827
|
+
"--runtime-actions-only"
|
|
2828
|
+
];
|
|
2829
|
+
}
|
|
2830
|
+
function parseOpenCodeConfigContent(raw) {
|
|
2831
|
+
if (!raw) return {};
|
|
2832
|
+
try {
|
|
2833
|
+
const parsed = JSON.parse(raw);
|
|
2834
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
2835
|
+
return parsed;
|
|
2836
|
+
}
|
|
2837
|
+
} catch {
|
|
2838
|
+
}
|
|
2839
|
+
return {};
|
|
2840
|
+
}
|
|
2841
|
+
function parseUserOpenCodeConfig(ctx) {
|
|
2842
|
+
const raw = ctx.config.envVars?.OPENCODE_CONFIG_CONTENT;
|
|
2843
|
+
return parseOpenCodeConfigContent(raw);
|
|
2844
|
+
}
|
|
2845
|
+
function readLocalOpenCodeConfig(home = os3.homedir()) {
|
|
2846
|
+
const configPath = path9.join(home, ".config", "opencode", "opencode.json");
|
|
2847
|
+
try {
|
|
2848
|
+
return parseOpenCodeConfigContent(readFileSync3(configPath, "utf8"));
|
|
2849
|
+
} catch {
|
|
2850
|
+
}
|
|
2851
|
+
return {};
|
|
2852
|
+
}
|
|
2853
|
+
function recordField(value) {
|
|
2854
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
2855
|
+
}
|
|
2856
|
+
function parseSemver(version) {
|
|
2857
|
+
const match = version.match(/(\d+)\.(\d+)\.(\d+)/);
|
|
2858
|
+
if (!match) return null;
|
|
2859
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
2860
|
+
}
|
|
2861
|
+
function isSupportedOpenCodeVersion(version) {
|
|
2862
|
+
if (!version) return true;
|
|
2863
|
+
const actual = parseSemver(version);
|
|
2864
|
+
const minimum = parseSemver(MIN_SUPPORTED_OPENCODE_VERSION);
|
|
2865
|
+
if (!actual || !minimum) return true;
|
|
2866
|
+
for (let i = 0; i < 3; i += 1) {
|
|
2867
|
+
if (actual[i] > minimum[i]) return true;
|
|
2868
|
+
if (actual[i] < minimum[i]) return false;
|
|
2869
|
+
}
|
|
2870
|
+
return true;
|
|
2871
|
+
}
|
|
2872
|
+
function unsupportedOpenCodeVersionMessage(version) {
|
|
2873
|
+
if (!version || isSupportedOpenCodeVersion(version)) return null;
|
|
2874
|
+
return `OpenCode CLI ${version} is unsupported; requires OpenCode >= ${MIN_SUPPORTED_OPENCODE_VERSION}. Upgrade opencode before starting this runtime.`;
|
|
2875
|
+
}
|
|
2876
|
+
function mergeOpenCodeConfigs(localConfig, envConfig) {
|
|
2877
|
+
return {
|
|
2878
|
+
...localConfig,
|
|
2879
|
+
...envConfig,
|
|
2880
|
+
provider: {
|
|
2881
|
+
...recordField(localConfig.provider),
|
|
2882
|
+
...recordField(envConfig.provider)
|
|
2883
|
+
},
|
|
2884
|
+
agent: {
|
|
2885
|
+
...recordField(localConfig.agent),
|
|
2886
|
+
...recordField(envConfig.agent)
|
|
2887
|
+
},
|
|
2888
|
+
mcp: {
|
|
2889
|
+
...recordField(localConfig.mcp),
|
|
2890
|
+
...recordField(envConfig.mcp)
|
|
2891
|
+
}
|
|
2892
|
+
};
|
|
2893
|
+
}
|
|
2894
|
+
function buildOpenCodeConfig(ctx, home = os3.homedir()) {
|
|
2895
|
+
const userConfig = mergeOpenCodeConfigs(readLocalOpenCodeConfig(home), parseUserOpenCodeConfig(ctx));
|
|
2896
|
+
const userAgents = recordField(userConfig.agent);
|
|
2897
|
+
const userSlockAgent = recordField(userAgents[SLOCK_AGENT_NAME]);
|
|
2898
|
+
return {
|
|
2899
|
+
...userConfig,
|
|
2900
|
+
$schema: "https://opencode.ai/config.json",
|
|
2901
|
+
agent: {
|
|
2902
|
+
...userAgents,
|
|
2903
|
+
[SLOCK_AGENT_NAME]: {
|
|
2904
|
+
...userSlockAgent,
|
|
2905
|
+
description: "Slock agent runtime",
|
|
2906
|
+
prompt: ctx.standingPrompt
|
|
2907
|
+
}
|
|
2908
|
+
},
|
|
2909
|
+
mcp: {
|
|
2910
|
+
...recordField(userConfig.mcp),
|
|
2911
|
+
[CHAT_MCP_SERVER_NAME]: {
|
|
2912
|
+
type: "local",
|
|
2913
|
+
command: buildChatBridgeCommand(ctx),
|
|
2914
|
+
enabled: true
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
};
|
|
2918
|
+
}
|
|
2919
|
+
function buildOpenCodeLaunchOptions(ctx, home = os3.homedir()) {
|
|
2920
|
+
const slock = prepareCliTransport(ctx, { NO_COLOR: "1" });
|
|
2921
|
+
const config = buildOpenCodeConfig(ctx, home);
|
|
2922
|
+
const env = {
|
|
2923
|
+
...slock.spawnEnv,
|
|
2924
|
+
OPENCODE_CONFIG_CONTENT: JSON.stringify(config)
|
|
2925
|
+
};
|
|
2926
|
+
const args = [
|
|
2927
|
+
"run",
|
|
2928
|
+
"--format",
|
|
2929
|
+
"json",
|
|
2930
|
+
"--dangerously-skip-permissions",
|
|
2931
|
+
"--pure",
|
|
2932
|
+
"--dir",
|
|
2933
|
+
ctx.workingDirectory
|
|
2934
|
+
];
|
|
2935
|
+
if (ctx.config.model && ctx.config.model !== "default") {
|
|
2936
|
+
args.push("--model", ctx.config.model);
|
|
2937
|
+
}
|
|
2938
|
+
args.push("--agent", SLOCK_AGENT_NAME);
|
|
2939
|
+
if (ctx.config.sessionId) {
|
|
2940
|
+
args.push("--session", ctx.config.sessionId);
|
|
2941
|
+
}
|
|
2942
|
+
const turnPrompt = ctx.prompt === ctx.standingPrompt ? NO_MESSAGE_PROMPT : ctx.prompt;
|
|
2943
|
+
args.push("--", turnPrompt);
|
|
2944
|
+
return { args, env, config };
|
|
2945
|
+
}
|
|
2946
|
+
function detectOpenCodeModels(home = os3.homedir()) {
|
|
2947
|
+
const models = (RUNTIME_MODELS.opencode || []).map((model) => ({
|
|
2948
|
+
...model,
|
|
2949
|
+
verified: "suggestion_only"
|
|
2950
|
+
}));
|
|
2951
|
+
const providers = recordField(readLocalOpenCodeConfig(home).provider);
|
|
2952
|
+
for (const [providerId, providerConfig] of Object.entries(providers)) {
|
|
2953
|
+
const providerModels = recordField(recordField(providerConfig).models);
|
|
2954
|
+
for (const [modelId, modelConfig] of Object.entries(providerModels)) {
|
|
2955
|
+
const fullId = `${providerId}/${modelId}`;
|
|
2956
|
+
if (models.some((model2) => model2.id === fullId)) continue;
|
|
2957
|
+
const model = recordField(modelConfig);
|
|
2958
|
+
const name = typeof model.name === "string" && model.name.length > 0 ? model.name : fullId;
|
|
2959
|
+
models.push({ id: fullId, label: name, verified: "launchable" });
|
|
2960
|
+
}
|
|
2961
|
+
}
|
|
2962
|
+
return models.length > 0 ? { models } : null;
|
|
2963
|
+
}
|
|
2964
|
+
function isSystemFirstMessageTask(message) {
|
|
2965
|
+
return message.sender_id === "system" && message.channel_type === "channel" && message.channel_name === "all" && message.content.trimStart().startsWith(FIRST_MESSAGE_TASK_PREFIX);
|
|
2966
|
+
}
|
|
2967
|
+
function buildOpenCodeSystemPrompt(config) {
|
|
2968
|
+
return buildCliTransportSystemPrompt(config, {
|
|
2969
|
+
toolPrefix: CHAT_MCP_TOOL_PREFIX,
|
|
2970
|
+
extraCriticalRules: [
|
|
2971
|
+
"- 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 `chat_runtime_profile_migration_done` tool with the exact `migration_key`; do not use `slock` CLI or reply in chat as the acknowledgment."
|
|
2972
|
+
],
|
|
2973
|
+
postStartupNotes: [
|
|
2974
|
+
"**OpenCode 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."
|
|
2975
|
+
],
|
|
2976
|
+
includeStdinNotificationSection: false,
|
|
2977
|
+
messageNotificationStyle: "poll"
|
|
2978
|
+
});
|
|
2979
|
+
}
|
|
2980
|
+
var OpenCodeDriver = class {
|
|
2981
|
+
id = "opencode";
|
|
2982
|
+
lifecycle = {
|
|
2983
|
+
kind: "per_turn",
|
|
2984
|
+
start: "defer_until_concrete_message",
|
|
2985
|
+
exit: "terminate_on_turn_end",
|
|
2986
|
+
inFlightWake: "coalesce_into_pending"
|
|
2987
|
+
};
|
|
2988
|
+
communication = {
|
|
2989
|
+
chat: "slock_cli",
|
|
2990
|
+
runtimeControl: "mcp_runtime_actions"
|
|
2991
|
+
};
|
|
2992
|
+
session = {
|
|
2993
|
+
recovery: "resume_or_fresh"
|
|
2994
|
+
};
|
|
2995
|
+
model = {
|
|
2996
|
+
detectedModelsVerifiedAs: "launchable",
|
|
2997
|
+
toLaunchSpec: (modelId, ctx, opts) => {
|
|
2998
|
+
if (!ctx) return { args: ["--model", modelId] };
|
|
2999
|
+
const launchCtx = {
|
|
3000
|
+
...ctx,
|
|
3001
|
+
config: {
|
|
3002
|
+
...ctx.config,
|
|
3003
|
+
model: modelId
|
|
3004
|
+
}
|
|
3005
|
+
};
|
|
3006
|
+
const launch = buildOpenCodeLaunchOptions(launchCtx, opts?.home);
|
|
3007
|
+
return {
|
|
3008
|
+
args: launch.args,
|
|
3009
|
+
env: launch.env,
|
|
3010
|
+
config: launch.config
|
|
3011
|
+
};
|
|
3012
|
+
}
|
|
3013
|
+
};
|
|
3014
|
+
supportsStdinNotification = false;
|
|
3015
|
+
mcpToolPrefix = CHAT_MCP_TOOL_PREFIX;
|
|
3016
|
+
busyDeliveryMode = "none";
|
|
3017
|
+
terminateProcessOnTurnEnd = true;
|
|
3018
|
+
deferSpawnUntilMessage = true;
|
|
3019
|
+
usesSlockCliForCommunication = true;
|
|
3020
|
+
shouldDeferWakeMessage(message) {
|
|
3021
|
+
return isSystemFirstMessageTask(message);
|
|
3022
|
+
}
|
|
3023
|
+
sessionId = null;
|
|
3024
|
+
sessionAnnounced = false;
|
|
3025
|
+
probe() {
|
|
3026
|
+
if (!resolveCommandOnPath("opencode")) return { available: false };
|
|
3027
|
+
const version = readCommandVersion("opencode") || void 0;
|
|
3028
|
+
const unsupportedMessage = unsupportedOpenCodeVersionMessage(version);
|
|
3029
|
+
if (unsupportedMessage) {
|
|
3030
|
+
return {
|
|
3031
|
+
available: false,
|
|
3032
|
+
version: `${version} (requires >= ${MIN_SUPPORTED_OPENCODE_VERSION})`
|
|
3033
|
+
};
|
|
3034
|
+
}
|
|
3035
|
+
return { available: true, version };
|
|
3036
|
+
}
|
|
3037
|
+
async detectModels() {
|
|
3038
|
+
return detectOpenCodeModels();
|
|
3039
|
+
}
|
|
3040
|
+
spawn(ctx) {
|
|
3041
|
+
this.sessionId = ctx.config.sessionId || null;
|
|
3042
|
+
this.sessionAnnounced = false;
|
|
3043
|
+
const unsupportedMessage = unsupportedOpenCodeVersionMessage(readCommandVersion("opencode"));
|
|
3044
|
+
if (unsupportedMessage) {
|
|
3045
|
+
throw new Error(unsupportedMessage);
|
|
3046
|
+
}
|
|
3047
|
+
const launch = buildOpenCodeLaunchOptions(ctx);
|
|
3048
|
+
const proc = spawn7("opencode", launch.args, {
|
|
3049
|
+
cwd: ctx.workingDirectory,
|
|
3050
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
3051
|
+
env: launch.env,
|
|
3052
|
+
shell: process.platform === "win32"
|
|
3053
|
+
});
|
|
3054
|
+
proc.stdin?.end();
|
|
3055
|
+
return { process: proc };
|
|
3056
|
+
}
|
|
3057
|
+
parseLine(line) {
|
|
3058
|
+
let event;
|
|
3059
|
+
try {
|
|
3060
|
+
event = JSON.parse(line);
|
|
3061
|
+
} catch {
|
|
3062
|
+
return [];
|
|
3063
|
+
}
|
|
3064
|
+
const events = [];
|
|
3065
|
+
if (event.sessionID && event.sessionID !== this.sessionId) {
|
|
3066
|
+
this.sessionId = event.sessionID;
|
|
3067
|
+
}
|
|
3068
|
+
if (!this.sessionAnnounced && this.sessionId) {
|
|
3069
|
+
events.push({ kind: "session_init", sessionId: this.sessionId });
|
|
3070
|
+
this.sessionAnnounced = true;
|
|
3071
|
+
}
|
|
3072
|
+
switch (event.type) {
|
|
3073
|
+
case "step_start":
|
|
3074
|
+
events.push({ kind: "thinking", text: "" });
|
|
3075
|
+
break;
|
|
3076
|
+
case "text":
|
|
3077
|
+
if (typeof event.part?.text === "string" && event.part.text.length > 0) {
|
|
3078
|
+
events.push({ kind: "text", text: event.part.text });
|
|
3079
|
+
}
|
|
3080
|
+
break;
|
|
3081
|
+
case "tool_use":
|
|
3082
|
+
events.push({
|
|
3083
|
+
kind: "tool_call",
|
|
3084
|
+
name: event.part?.tool || "unknown_tool",
|
|
3085
|
+
input: event.part?.state?.input
|
|
3086
|
+
});
|
|
3087
|
+
break;
|
|
3088
|
+
case "step_finish":
|
|
3089
|
+
if (event.part?.reason !== "tool-calls") {
|
|
3090
|
+
events.push({ kind: "turn_end", sessionId: this.sessionId || void 0 });
|
|
3091
|
+
}
|
|
3092
|
+
break;
|
|
3093
|
+
case "error": {
|
|
3094
|
+
const message = event.error?.data?.message || event.error?.message || (event.error?.name ? `${event.error.name} (no message)` : null) || "Unknown OpenCode error";
|
|
3095
|
+
events.push({ kind: "error", message });
|
|
3096
|
+
events.push({ kind: "turn_end", sessionId: this.sessionId || void 0 });
|
|
3097
|
+
break;
|
|
3098
|
+
}
|
|
3099
|
+
}
|
|
3100
|
+
return events;
|
|
3101
|
+
}
|
|
3102
|
+
encodeStdinMessage(_text, _sessionId, _opts) {
|
|
3103
|
+
return null;
|
|
3104
|
+
}
|
|
3105
|
+
buildSystemPrompt(config, _agentId) {
|
|
3106
|
+
return buildOpenCodeSystemPrompt(config);
|
|
3107
|
+
}
|
|
3108
|
+
};
|
|
3109
|
+
|
|
2584
3110
|
// src/drivers/index.ts
|
|
2585
3111
|
var driverFactories = {
|
|
2586
3112
|
claude: () => new ClaudeDriver(),
|
|
@@ -2588,7 +3114,8 @@ var driverFactories = {
|
|
|
2588
3114
|
copilot: () => new CopilotDriver(),
|
|
2589
3115
|
cursor: () => new CursorDriver(),
|
|
2590
3116
|
gemini: () => new GeminiDriver(),
|
|
2591
|
-
kimi: () => new KimiDriver()
|
|
3117
|
+
kimi: () => new KimiDriver(),
|
|
3118
|
+
opencode: () => new OpenCodeDriver()
|
|
2592
3119
|
};
|
|
2593
3120
|
function getDriver(runtimeId) {
|
|
2594
3121
|
const createDriver = driverFactories[runtimeId];
|
|
@@ -2601,7 +3128,7 @@ function getDriver(runtimeId) {
|
|
|
2601
3128
|
|
|
2602
3129
|
// src/workspaces.ts
|
|
2603
3130
|
import { readdir, rm, stat } from "fs/promises";
|
|
2604
|
-
import
|
|
3131
|
+
import path10 from "path";
|
|
2605
3132
|
function isValidWorkspaceDirectoryName(directoryName) {
|
|
2606
3133
|
return !directoryName.includes("/") && !directoryName.includes("\\") && !directoryName.includes("..");
|
|
2607
3134
|
}
|
|
@@ -2609,7 +3136,7 @@ function resolveWorkspaceDirectoryPath(dataDir, directoryName) {
|
|
|
2609
3136
|
if (!isValidWorkspaceDirectoryName(directoryName)) {
|
|
2610
3137
|
return null;
|
|
2611
3138
|
}
|
|
2612
|
-
return
|
|
3139
|
+
return path10.join(dataDir, directoryName);
|
|
2613
3140
|
}
|
|
2614
3141
|
function emptyWorkspaceDirectorySummary(latestMtime = /* @__PURE__ */ new Date(0)) {
|
|
2615
3142
|
return {
|
|
@@ -2658,7 +3185,7 @@ async function summarizeWorkspaceDirectory(dirPath) {
|
|
|
2658
3185
|
return summary;
|
|
2659
3186
|
}
|
|
2660
3187
|
const childSummaries = await Promise.all(
|
|
2661
|
-
entries.map((entry) => summarizeWorkspaceEntry(
|
|
3188
|
+
entries.map((entry) => summarizeWorkspaceEntry(path10.join(dirPath, entry.name), entry))
|
|
2662
3189
|
);
|
|
2663
3190
|
for (const childSummary of childSummaries) {
|
|
2664
3191
|
summary = mergeWorkspaceDirectorySummaries(summary, childSummary);
|
|
@@ -2677,7 +3204,7 @@ async function scanWorkspaceDirectories(dataDir) {
|
|
|
2677
3204
|
if (!entry.isDirectory()) {
|
|
2678
3205
|
return null;
|
|
2679
3206
|
}
|
|
2680
|
-
const dirPath =
|
|
3207
|
+
const dirPath = path10.join(dataDir, entry.name);
|
|
2681
3208
|
try {
|
|
2682
3209
|
const summary = await summarizeWorkspaceDirectory(dirPath);
|
|
2683
3210
|
return {
|
|
@@ -2709,7 +3236,23 @@ async function deleteWorkspaceDirectory(dataDir, directoryName) {
|
|
|
2709
3236
|
}
|
|
2710
3237
|
|
|
2711
3238
|
// src/agentProcessManager.ts
|
|
2712
|
-
var DATA_DIR =
|
|
3239
|
+
var DATA_DIR = path11.join(os4.homedir(), ".slock", "agents");
|
|
3240
|
+
var DEFAULT_MAX_CONCURRENT_AGENT_STARTS = 1;
|
|
3241
|
+
var DEFAULT_AGENT_START_INTERVAL_MS = 500;
|
|
3242
|
+
function readPositiveIntegerEnv(name, fallback) {
|
|
3243
|
+
const raw = process.env[name];
|
|
3244
|
+
if (!raw) return fallback;
|
|
3245
|
+
const parsed = Number(raw);
|
|
3246
|
+
if (!Number.isFinite(parsed) || parsed < 1) return fallback;
|
|
3247
|
+
return Math.floor(parsed);
|
|
3248
|
+
}
|
|
3249
|
+
function readNonNegativeIntegerEnv(name, fallback) {
|
|
3250
|
+
const raw = process.env[name];
|
|
3251
|
+
if (!raw) return fallback;
|
|
3252
|
+
const parsed = Number(raw);
|
|
3253
|
+
if (!Number.isFinite(parsed) || parsed < 0) return fallback;
|
|
3254
|
+
return Math.floor(parsed);
|
|
3255
|
+
}
|
|
2713
3256
|
function toLocalTime(iso) {
|
|
2714
3257
|
const d = new Date(iso);
|
|
2715
3258
|
if (isNaN(d.getTime())) return iso;
|
|
@@ -2750,12 +3293,12 @@ function findSessionJsonl(root, predicate) {
|
|
|
2750
3293
|
for (const entry of entries) {
|
|
2751
3294
|
if (++visited > maxEntries) return null;
|
|
2752
3295
|
if (!entry.isFile() || !predicate(entry.name)) continue;
|
|
2753
|
-
return
|
|
3296
|
+
return path11.join(dir, entry.name);
|
|
2754
3297
|
}
|
|
2755
3298
|
for (const entry of entries) {
|
|
2756
3299
|
if (++visited > maxEntries) return null;
|
|
2757
3300
|
if (!entry.isDirectory()) continue;
|
|
2758
|
-
const found = visit(
|
|
3301
|
+
const found = visit(path11.join(dir, entry.name), depth - 1);
|
|
2759
3302
|
if (found) return found;
|
|
2760
3303
|
}
|
|
2761
3304
|
return null;
|
|
@@ -2768,9 +3311,9 @@ function safeSessionFilename(value) {
|
|
|
2768
3311
|
}
|
|
2769
3312
|
function writeRuntimeSessionHandoff(runtime, sessionId, fallbackDir) {
|
|
2770
3313
|
try {
|
|
2771
|
-
const dir =
|
|
3314
|
+
const dir = path11.join(fallbackDir, ".slock", "runtime-sessions");
|
|
2772
3315
|
mkdirSync4(dir, { recursive: true });
|
|
2773
|
-
const filePath =
|
|
3316
|
+
const filePath = path11.join(dir, `${runtime}-${safeSessionFilename(sessionId)}.jsonl`);
|
|
2774
3317
|
writeFileSync7(filePath, JSON.stringify({
|
|
2775
3318
|
type: "runtime_session_handoff",
|
|
2776
3319
|
runtime,
|
|
@@ -2789,8 +3332,8 @@ function writeRuntimeSessionHandoff(runtime, sessionId, fallbackDir) {
|
|
|
2789
3332
|
return null;
|
|
2790
3333
|
}
|
|
2791
3334
|
}
|
|
2792
|
-
function resolveRuntimeSessionRef(runtime, sessionId, homeDir =
|
|
2793
|
-
const directPath =
|
|
3335
|
+
function resolveRuntimeSessionRef(runtime, sessionId, homeDir = os4.homedir(), fallbackDir) {
|
|
3336
|
+
const directPath = path11.isAbsolute(sessionId) ? sessionId : null;
|
|
2794
3337
|
if (directPath) {
|
|
2795
3338
|
try {
|
|
2796
3339
|
if (statSync(directPath).isFile()) {
|
|
@@ -2799,7 +3342,7 @@ function resolveRuntimeSessionRef(runtime, sessionId, homeDir = os3.homedir(), f
|
|
|
2799
3342
|
} catch {
|
|
2800
3343
|
}
|
|
2801
3344
|
}
|
|
2802
|
-
const resolvedPath = runtime === "claude" ? findSessionJsonl(
|
|
3345
|
+
const resolvedPath = runtime === "claude" ? findSessionJsonl(path11.join(homeDir, ".claude", "projects"), (filename) => filename === `${sessionId}.jsonl`) : runtime === "codex" ? findSessionJsonl(path11.join(homeDir, ".codex", "sessions"), (filename) => filename.endsWith(".jsonl") && filename.includes(sessionId)) : null;
|
|
2803
3346
|
if (!resolvedPath && fallbackDir) {
|
|
2804
3347
|
const fallback = writeRuntimeSessionHandoff(runtime, sessionId, fallbackDir);
|
|
2805
3348
|
if (fallback) return fallback;
|
|
@@ -2830,12 +3373,38 @@ function formatThreadContextMessage(message) {
|
|
|
2830
3373
|
const senderType = formatVisibleActorType(message.sender_type);
|
|
2831
3374
|
return `- [msg=${msgId} time=${time}${senderType}] ${formatSenderHandle(message)}: ${message.content}`;
|
|
2832
3375
|
}
|
|
2833
|
-
function
|
|
3376
|
+
function mcpToolName(driver, name) {
|
|
3377
|
+
return `${driver?.mcpToolPrefix ?? ""}${name}`;
|
|
3378
|
+
}
|
|
3379
|
+
function communicationCommand(driver, name) {
|
|
3380
|
+
if (!driver?.usesSlockCliForCommunication) {
|
|
3381
|
+
return mcpToolName(driver, name);
|
|
3382
|
+
}
|
|
3383
|
+
switch (name) {
|
|
3384
|
+
case "send_message":
|
|
3385
|
+
return "slock message send";
|
|
3386
|
+
case "read_history":
|
|
3387
|
+
return "slock message read";
|
|
3388
|
+
case "check_messages":
|
|
3389
|
+
return "slock message check";
|
|
3390
|
+
case "claim_tasks":
|
|
3391
|
+
return "slock task claim";
|
|
3392
|
+
default:
|
|
3393
|
+
return `slock ${name.replace(/_/g, " ")}`;
|
|
3394
|
+
}
|
|
3395
|
+
}
|
|
3396
|
+
function dynamicReplyInstruction(driver) {
|
|
3397
|
+
return driver?.usesSlockCliForCommunication ? "reply using `slock message send --target <exact target>`" : `reply with ${communicationCommand(driver, "send_message")}`;
|
|
3398
|
+
}
|
|
3399
|
+
function dynamicClaimInstruction(driver) {
|
|
3400
|
+
return driver?.usesSlockCliForCommunication ? "claim the relevant task with `slock task claim`" : `claim the relevant task with ${communicationCommand(driver, "claim_tasks")}`;
|
|
3401
|
+
}
|
|
3402
|
+
function formatIncomingMessage(message, driver) {
|
|
2834
3403
|
const threadJoinPrefix = message.thread_join_context ? [
|
|
2835
3404
|
`[System: You were added to a new thread via @mention. Read this context before replying.]`,
|
|
2836
3405
|
`parent: ${message.thread_join_context.parent_target}`,
|
|
2837
3406
|
`thread: ${message.thread_join_context.thread_target}`,
|
|
2838
|
-
`suggested next step: read_history(channel="${message.thread_join_context.suggested_read_history_target}")`,
|
|
3407
|
+
`suggested next step: ${driver?.usesSlockCliForCommunication ? `slock message read --channel "${message.thread_join_context.suggested_read_history_target}"` : `${communicationCommand(driver, "read_history")}(channel="${message.thread_join_context.suggested_read_history_target}")`}`,
|
|
2839
3408
|
"",
|
|
2840
3409
|
"Parent message:",
|
|
2841
3410
|
formatThreadContextMessage(message.thread_join_context.parent_message),
|
|
@@ -3369,9 +3938,20 @@ function classifyTerminalFailure(ap) {
|
|
|
3369
3938
|
return null;
|
|
3370
3939
|
}
|
|
3371
3940
|
function isMissingResumeSession(ap) {
|
|
3372
|
-
if (ap.driver.id !== "claude") return false;
|
|
3373
3941
|
if (!ap.sessionId) return false;
|
|
3374
|
-
|
|
3942
|
+
const candidates = [
|
|
3943
|
+
ap.lastRuntimeError,
|
|
3944
|
+
...ap.recentStderr
|
|
3945
|
+
].filter((value) => !!value);
|
|
3946
|
+
if (ap.driver.id === "claude") {
|
|
3947
|
+
return candidates.some((text) => /No conversation found with session ID/i.test(text));
|
|
3948
|
+
}
|
|
3949
|
+
if (ap.driver.id === "opencode") {
|
|
3950
|
+
return candidates.some(
|
|
3951
|
+
(text) => /Session not found/i.test(text) && text.includes(ap.sessionId)
|
|
3952
|
+
);
|
|
3953
|
+
}
|
|
3954
|
+
return false;
|
|
3375
3955
|
}
|
|
3376
3956
|
function getMessageDeliveryText(driver) {
|
|
3377
3957
|
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.";
|
|
@@ -3396,6 +3976,15 @@ var AgentProcessManager = class _AgentProcessManager {
|
|
|
3396
3976
|
agents = /* @__PURE__ */ new Map();
|
|
3397
3977
|
agentsStarting = /* @__PURE__ */ new Set();
|
|
3398
3978
|
// Prevent concurrent starts of same agent
|
|
3979
|
+
queuedAgentStarts = /* @__PURE__ */ new Map();
|
|
3980
|
+
agentStartQueue = [];
|
|
3981
|
+
activeAgentStartPermits = /* @__PURE__ */ new Set();
|
|
3982
|
+
activeAgentStartCount = 0;
|
|
3983
|
+
agentStartPumpTimer = null;
|
|
3984
|
+
lastAgentStartAt = 0;
|
|
3985
|
+
lastAgentStartAgentId = null;
|
|
3986
|
+
maxConcurrentAgentStarts;
|
|
3987
|
+
agentStartIntervalMs;
|
|
3399
3988
|
startingInboxes = /* @__PURE__ */ new Map();
|
|
3400
3989
|
/** Cached configs for agents whose process exited normally — enables auto-restart on next message */
|
|
3401
3990
|
idleAgentConfigs = /* @__PURE__ */ new Map();
|
|
@@ -3416,12 +4005,141 @@ var AgentProcessManager = class _AgentProcessManager {
|
|
|
3416
4005
|
this.daemonApiKey = daemonApiKey;
|
|
3417
4006
|
this.serverUrl = opts.serverUrl;
|
|
3418
4007
|
this.dataDir = opts.dataDir || DATA_DIR;
|
|
3419
|
-
this.runtimeSessionHomeDir = opts.runtimeSessionHomeDir ||
|
|
4008
|
+
this.runtimeSessionHomeDir = opts.runtimeSessionHomeDir || os4.homedir();
|
|
3420
4009
|
this.driverResolver = opts.driverResolver || getDriver;
|
|
3421
4010
|
this.defaultAgentEnvVarsProvider = opts.defaultAgentEnvVarsProvider || null;
|
|
3422
4011
|
this.tracer = opts.tracer ?? noopTracer;
|
|
4012
|
+
this.maxConcurrentAgentStarts = Math.max(
|
|
4013
|
+
1,
|
|
4014
|
+
Math.floor(
|
|
4015
|
+
opts.runtimeStartScheduler?.maxConcurrentStarts ?? readPositiveIntegerEnv("SLOCK_DAEMON_MAX_CONCURRENT_AGENT_STARTS", DEFAULT_MAX_CONCURRENT_AGENT_STARTS)
|
|
4016
|
+
)
|
|
4017
|
+
);
|
|
4018
|
+
this.agentStartIntervalMs = Math.max(
|
|
4019
|
+
0,
|
|
4020
|
+
Math.floor(
|
|
4021
|
+
opts.runtimeStartScheduler?.minStartIntervalMs ?? readNonNegativeIntegerEnv("SLOCK_DAEMON_AGENT_START_INTERVAL_MS", DEFAULT_AGENT_START_INTERVAL_MS)
|
|
4022
|
+
)
|
|
4023
|
+
);
|
|
3423
4024
|
}
|
|
3424
4025
|
async startAgent(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId) {
|
|
4026
|
+
if (this.agents.has(agentId)) {
|
|
4027
|
+
logger.info(`[Agent ${agentId}] Start ignored (already running)`);
|
|
4028
|
+
return;
|
|
4029
|
+
}
|
|
4030
|
+
if (this.agentsStarting.has(agentId)) {
|
|
4031
|
+
logger.info(`[Agent ${agentId}] Start ignored (startup in progress)`);
|
|
4032
|
+
return;
|
|
4033
|
+
}
|
|
4034
|
+
if (this.queuedAgentStarts.has(agentId)) {
|
|
4035
|
+
logger.info(`[Agent ${agentId}] Start ignored (startup already queued)`);
|
|
4036
|
+
return;
|
|
4037
|
+
}
|
|
4038
|
+
return new Promise((resolve, reject) => {
|
|
4039
|
+
const item = {
|
|
4040
|
+
agentId,
|
|
4041
|
+
config,
|
|
4042
|
+
wakeMessage,
|
|
4043
|
+
unreadSummary,
|
|
4044
|
+
resumePrompt,
|
|
4045
|
+
launchId,
|
|
4046
|
+
resolve,
|
|
4047
|
+
reject
|
|
4048
|
+
};
|
|
4049
|
+
this.agentStartQueue.push(item);
|
|
4050
|
+
this.queuedAgentStarts.set(agentId, item);
|
|
4051
|
+
logger.info(
|
|
4052
|
+
`[Agent ${agentId}] Start queued (queue=${this.agentStartQueue.length}, active=${this.activeAgentStartCount}, max=${this.maxConcurrentAgentStarts}, interval=${this.agentStartIntervalMs}ms)`
|
|
4053
|
+
);
|
|
4054
|
+
this.pumpAgentStartQueue();
|
|
4055
|
+
});
|
|
4056
|
+
}
|
|
4057
|
+
pumpAgentStartQueue() {
|
|
4058
|
+
if (this.agentStartPumpTimer) return;
|
|
4059
|
+
if (this.agentStartQueue.length === 0) return;
|
|
4060
|
+
if (this.activeAgentStartCount >= this.maxConcurrentAgentStarts) return;
|
|
4061
|
+
const next = this.agentStartQueue[0];
|
|
4062
|
+
const shouldRateLimit = next ? next.agentId !== this.lastAgentStartAgentId : true;
|
|
4063
|
+
const elapsed = Date.now() - this.lastAgentStartAt;
|
|
4064
|
+
const waitMs = shouldRateLimit ? Math.max(0, this.agentStartIntervalMs - elapsed) : 0;
|
|
4065
|
+
if (waitMs > 0) {
|
|
4066
|
+
this.agentStartPumpTimer = setTimeout(() => {
|
|
4067
|
+
this.agentStartPumpTimer = null;
|
|
4068
|
+
this.pumpAgentStartQueue();
|
|
4069
|
+
}, waitMs);
|
|
4070
|
+
return;
|
|
4071
|
+
}
|
|
4072
|
+
const item = this.agentStartQueue.shift();
|
|
4073
|
+
if (!item) return;
|
|
4074
|
+
if (this.queuedAgentStarts.get(item.agentId) !== item) {
|
|
4075
|
+
this.pumpAgentStartQueue();
|
|
4076
|
+
return;
|
|
4077
|
+
}
|
|
4078
|
+
this.queuedAgentStarts.delete(item.agentId);
|
|
4079
|
+
if (this.agents.has(item.agentId) || this.agentsStarting.has(item.agentId)) {
|
|
4080
|
+
logger.info(`[Agent ${item.agentId}] Queued start skipped (already running or starting)`);
|
|
4081
|
+
item.resolve();
|
|
4082
|
+
this.pumpAgentStartQueue();
|
|
4083
|
+
return;
|
|
4084
|
+
}
|
|
4085
|
+
this.activeAgentStartCount++;
|
|
4086
|
+
this.activeAgentStartPermits.add(item.agentId);
|
|
4087
|
+
this.lastAgentStartAt = Date.now();
|
|
4088
|
+
this.lastAgentStartAgentId = item.agentId;
|
|
4089
|
+
logger.info(
|
|
4090
|
+
`[Agent ${item.agentId}] Dequeued start (remaining=${this.agentStartQueue.length}, active=${this.activeAgentStartCount})`
|
|
4091
|
+
);
|
|
4092
|
+
this.startAgentNow(
|
|
4093
|
+
item.agentId,
|
|
4094
|
+
item.config,
|
|
4095
|
+
item.wakeMessage,
|
|
4096
|
+
item.unreadSummary,
|
|
4097
|
+
item.resumePrompt,
|
|
4098
|
+
item.launchId
|
|
4099
|
+
).then(item.resolve, (err) => {
|
|
4100
|
+
this.releaseAgentStartPermit(item.agentId, "start failed");
|
|
4101
|
+
item.reject(err);
|
|
4102
|
+
});
|
|
4103
|
+
}
|
|
4104
|
+
releaseAgentStartPermit(agentId, reason) {
|
|
4105
|
+
if (!this.activeAgentStartPermits.delete(agentId)) return false;
|
|
4106
|
+
this.activeAgentStartCount = Math.max(0, this.activeAgentStartCount - 1);
|
|
4107
|
+
logger.info(
|
|
4108
|
+
`[Agent ${agentId}] Start permit released (${reason}) (active=${this.activeAgentStartCount}, queue=${this.agentStartQueue.length})`
|
|
4109
|
+
);
|
|
4110
|
+
this.pumpAgentStartQueue();
|
|
4111
|
+
return true;
|
|
4112
|
+
}
|
|
4113
|
+
cancelQueuedAgentStart(agentId, reason) {
|
|
4114
|
+
const item = this.queuedAgentStarts.get(agentId);
|
|
4115
|
+
if (!item) return false;
|
|
4116
|
+
this.queuedAgentStarts.delete(agentId);
|
|
4117
|
+
this.agentStartQueue = this.agentStartQueue.filter((candidate) => candidate !== item);
|
|
4118
|
+
this.startingInboxes.delete(agentId);
|
|
4119
|
+
if (this.agentStartQueue.length === 0 && this.agentStartPumpTimer) {
|
|
4120
|
+
clearTimeout(this.agentStartPumpTimer);
|
|
4121
|
+
this.agentStartPumpTimer = null;
|
|
4122
|
+
}
|
|
4123
|
+
logger.info(`[Agent ${agentId}] Queued start cancelled (${reason})`);
|
|
4124
|
+
item.resolve();
|
|
4125
|
+
return true;
|
|
4126
|
+
}
|
|
4127
|
+
cancelAllQueuedAgentStarts(reason) {
|
|
4128
|
+
for (const item of this.agentStartQueue) {
|
|
4129
|
+
if (this.queuedAgentStarts.get(item.agentId) === item) {
|
|
4130
|
+
logger.info(`[Agent ${item.agentId}] Queued start cancelled (${reason})`);
|
|
4131
|
+
item.resolve();
|
|
4132
|
+
}
|
|
4133
|
+
}
|
|
4134
|
+
this.agentStartQueue = [];
|
|
4135
|
+
this.queuedAgentStarts.clear();
|
|
4136
|
+
this.startingInboxes.clear();
|
|
4137
|
+
if (this.agentStartPumpTimer) {
|
|
4138
|
+
clearTimeout(this.agentStartPumpTimer);
|
|
4139
|
+
this.agentStartPumpTimer = null;
|
|
4140
|
+
}
|
|
4141
|
+
}
|
|
4142
|
+
async startAgentNow(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId) {
|
|
3425
4143
|
if (this.agents.has(agentId)) {
|
|
3426
4144
|
logger.info(`[Agent ${agentId}] Start ignored (already running)`);
|
|
3427
4145
|
return;
|
|
@@ -3433,26 +4151,26 @@ var AgentProcessManager = class _AgentProcessManager {
|
|
|
3433
4151
|
this.agentsStarting.add(agentId);
|
|
3434
4152
|
try {
|
|
3435
4153
|
const driver = this.driverResolver(config.runtime || "claude");
|
|
3436
|
-
const agentDataDir =
|
|
4154
|
+
const agentDataDir = path11.join(this.dataDir, agentId);
|
|
3437
4155
|
await mkdir(agentDataDir, { recursive: true });
|
|
3438
4156
|
const runtimeConfig = withLocalRuntimeContext(config, agentId, agentDataDir);
|
|
3439
|
-
const memoryMdPath =
|
|
4157
|
+
const memoryMdPath = path11.join(agentDataDir, "MEMORY.md");
|
|
3440
4158
|
try {
|
|
3441
4159
|
await access(memoryMdPath);
|
|
3442
4160
|
} catch {
|
|
3443
4161
|
const initialMemoryMd = buildInitialMemoryMd(runtimeConfig);
|
|
3444
4162
|
await writeFile(memoryMdPath, initialMemoryMd);
|
|
3445
4163
|
}
|
|
3446
|
-
const notesDir =
|
|
4164
|
+
const notesDir = path11.join(agentDataDir, "notes");
|
|
3447
4165
|
await mkdir(notesDir, { recursive: true });
|
|
3448
4166
|
if (getOnboardingSeedMode(config) === FIRST_CINDY_SEED_MODE) {
|
|
3449
4167
|
const seedFiles = buildOnboardingSeedFiles();
|
|
3450
4168
|
for (const { relativePath, content } of seedFiles) {
|
|
3451
|
-
const fullPath =
|
|
4169
|
+
const fullPath = path11.join(agentDataDir, relativePath);
|
|
3452
4170
|
try {
|
|
3453
4171
|
await access(fullPath);
|
|
3454
4172
|
} catch {
|
|
3455
|
-
await mkdir(
|
|
4173
|
+
await mkdir(path11.dirname(fullPath), { recursive: true });
|
|
3456
4174
|
await writeFile(fullPath, content);
|
|
3457
4175
|
}
|
|
3458
4176
|
}
|
|
@@ -3470,7 +4188,7 @@ var AgentProcessManager = class _AgentProcessManager {
|
|
|
3470
4188
|
const channelLabel = formatChannelLabel(wakeMessage);
|
|
3471
4189
|
prompt = runtimeProfileControlPrompt ?? `New message received:
|
|
3472
4190
|
|
|
3473
|
-
${formatIncomingMessage(wakeMessage)}`;
|
|
4191
|
+
${formatIncomingMessage(wakeMessage, driver)}`;
|
|
3474
4192
|
if (!runtimeProfileControlPrompt && unreadSummary && Object.keys(unreadSummary).length > 0) {
|
|
3475
4193
|
const otherUnread = Object.entries(unreadSummary).filter(([key]) => key !== channelLabel);
|
|
3476
4194
|
if (otherUnread.length > 0) {
|
|
@@ -3483,13 +4201,13 @@ You also have unread messages in other channels:`;
|
|
|
3483
4201
|
}
|
|
3484
4202
|
prompt += `
|
|
3485
4203
|
|
|
3486
|
-
Use read_history to catch up, or respond to the message above first.`;
|
|
4204
|
+
Use ${communicationCommand(driver, "read_history")} to catch up, or respond to the message above first.`;
|
|
3487
4205
|
}
|
|
3488
4206
|
}
|
|
3489
4207
|
if (!runtimeProfileControlPrompt) {
|
|
3490
4208
|
prompt += `
|
|
3491
4209
|
|
|
3492
|
-
Respond as appropriate \u2014
|
|
4210
|
+
Respond as appropriate \u2014 ${dynamicReplyInstruction(driver)}, or take action as needed. Complete ALL your work before stopping.
|
|
3493
4211
|
|
|
3494
4212
|
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)}`;
|
|
3495
4213
|
prompt += getBusyDeliveryNote(driver);
|
|
@@ -3502,7 +4220,7 @@ IMPORTANT: If the message requires multi-step work (e.g. research, code changes,
|
|
|
3502
4220
|
}
|
|
3503
4221
|
prompt += `
|
|
3504
4222
|
|
|
3505
|
-
Use read_history to catch up on the channels listed above, then stop. Read each listed channel at most once unless a read fails. Do NOT call check_messages in this mode. If the history reveals a direct request, assignment, @mention, review request, or task clearly addressed to you, switch into active handling instead of stopping:
|
|
4223
|
+
Use ${communicationCommand(driver, "read_history")} to catch up on the channels listed above, then stop. Read each listed channel at most once unless a read fails. Do NOT call ${communicationCommand(driver, "check_messages")} in this mode. If the history reveals a direct request, assignment, @mention, review request, or task clearly addressed to you, switch into active handling instead of stopping: ${dynamicReplyInstruction(driver)} and ${dynamicClaimInstruction(driver)} before starting work. Otherwise, do NOT send any message in this mode. ${getMessageDeliveryText(driver)}`;
|
|
3506
4224
|
} else if (isResume) {
|
|
3507
4225
|
prompt = `No new messages while you were away. Nothing to do \u2014 just stop. ${getMessageDeliveryText(driver)}`;
|
|
3508
4226
|
prompt += getBusyDeliveryNote(driver);
|
|
@@ -3510,6 +4228,25 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
3510
4228
|
prompt = driver.supportsNativeStandingPrompt ? NATIVE_STANDING_PROMPT_STARTUP_INPUT : standingPrompt;
|
|
3511
4229
|
}
|
|
3512
4230
|
const effectiveConfig = await this.buildSpawnConfig(agentId, runtimeConfig);
|
|
4231
|
+
const canDeferEmptyStart = driver.deferSpawnUntilMessage === true && !wakeMessage && !runtimeConfig.runtimeProfileControl && (!unreadSummary || Object.keys(unreadSummary).length === 0);
|
|
4232
|
+
if (canDeferEmptyStart) {
|
|
4233
|
+
const pendingMessages = this.startingInboxes.get(agentId) || [];
|
|
4234
|
+
this.startingInboxes.delete(agentId);
|
|
4235
|
+
this.agentsStarting.delete(agentId);
|
|
4236
|
+
this.idleAgentConfigs.set(agentId, {
|
|
4237
|
+
config: effectiveConfig,
|
|
4238
|
+
sessionId: effectiveConfig.sessionId || null,
|
|
4239
|
+
launchId: launchId || null
|
|
4240
|
+
});
|
|
4241
|
+
this.sendAgentStatus(agentId, "active", launchId || null);
|
|
4242
|
+
this.broadcastActivity(agentId, "online", "Process idle");
|
|
4243
|
+
logger.info(`[Agent ${agentId}] Deferred ${driver.id} spawn until first concrete message`);
|
|
4244
|
+
this.releaseAgentStartPermit(agentId, "spawn deferred");
|
|
4245
|
+
for (const message of pendingMessages) {
|
|
4246
|
+
this.deliverMessage(agentId, message);
|
|
4247
|
+
}
|
|
4248
|
+
return;
|
|
4249
|
+
}
|
|
3513
4250
|
const { process: proc } = driver.spawn({
|
|
3514
4251
|
agentId,
|
|
3515
4252
|
config: effectiveConfig,
|
|
@@ -3528,6 +4265,9 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
3528
4265
|
config: runtimeConfig,
|
|
3529
4266
|
sessionId: runtimeConfig.sessionId || null,
|
|
3530
4267
|
launchId: launchId || null,
|
|
4268
|
+
startupWakeMessage: wakeMessage,
|
|
4269
|
+
startupUnreadSummary: unreadSummary,
|
|
4270
|
+
startupResumePrompt: resumePrompt,
|
|
3531
4271
|
isIdle: false,
|
|
3532
4272
|
notificationTimer: null,
|
|
3533
4273
|
pendingNotificationCount: 0,
|
|
@@ -3539,12 +4279,14 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
3539
4279
|
runtimeTraceSpan: null,
|
|
3540
4280
|
lastActivity: "",
|
|
3541
4281
|
lastActivityDetail: "",
|
|
4282
|
+
activityClientSeq: 0,
|
|
3542
4283
|
recentStdout: [],
|
|
3543
4284
|
recentStderr: [],
|
|
3544
4285
|
lastRuntimeError: null,
|
|
3545
4286
|
spawnError: null,
|
|
3546
4287
|
exitCode: null,
|
|
3547
4288
|
exitSignal: null,
|
|
4289
|
+
expectedTerminationReason: null,
|
|
3548
4290
|
pendingTrajectory: null,
|
|
3549
4291
|
gatedSteering: createGatedSteeringState()
|
|
3550
4292
|
};
|
|
@@ -3603,6 +4345,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
3603
4345
|
if (this.agents.has(agentId)) {
|
|
3604
4346
|
const ap = this.agents.get(agentId);
|
|
3605
4347
|
if (ap.process !== proc) return;
|
|
4348
|
+
this.releaseAgentStartPermit(agentId, "process closed");
|
|
3606
4349
|
if (ap.notificationTimer) {
|
|
3607
4350
|
clearTimeout(ap.notificationTimer);
|
|
3608
4351
|
}
|
|
@@ -3614,20 +4357,60 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
3614
4357
|
}
|
|
3615
4358
|
const finalCode = ap.exitCode ?? code;
|
|
3616
4359
|
const finalSignal = ap.exitSignal ?? signal;
|
|
3617
|
-
const
|
|
3618
|
-
|
|
3619
|
-
|
|
4360
|
+
const expectedTermination = Boolean(ap.expectedTerminationReason);
|
|
4361
|
+
const processEndedCleanly = finalCode === 0 || expectedTermination && !ap.lastRuntimeError;
|
|
4362
|
+
const terminalFailureDetail = processEndedCleanly ? null : classifyTerminalFailure(ap);
|
|
4363
|
+
const missingResumeSession = isMissingResumeSession(ap);
|
|
4364
|
+
const summary = summarizeCrash(finalCode, finalSignal);
|
|
4365
|
+
this.endRuntimeTrace(ap, processEndedCleanly ? "ok" : "error", {
|
|
4366
|
+
outcome: processEndedCleanly ? "process-exit" : "process-crash",
|
|
4367
|
+
expectedTerminationReason: ap.expectedTerminationReason || void 0,
|
|
3620
4368
|
exitCode: finalCode,
|
|
3621
4369
|
exitSignal: finalSignal
|
|
3622
4370
|
});
|
|
3623
|
-
if (
|
|
4371
|
+
if (processEndedCleanly) {
|
|
3624
4372
|
this.finishCompactionIfActive(agentId, "Context compaction finished (inferred from process exit)");
|
|
3625
4373
|
} else {
|
|
3626
4374
|
this.clearCompactionWatchdog(ap);
|
|
3627
4375
|
}
|
|
3628
4376
|
this.agents.delete(agentId);
|
|
3629
|
-
if (
|
|
3630
|
-
const
|
|
4377
|
+
if (missingResumeSession) {
|
|
4378
|
+
const staleSessionId = ap.sessionId;
|
|
4379
|
+
const runtimeLabel = ap.driver.id === "opencode" ? "OpenCode" : "Claude";
|
|
4380
|
+
const restartConfig = { ...ap.config, sessionId: null };
|
|
4381
|
+
logger.warn(
|
|
4382
|
+
`[Agent ${agentId}] Stored ${runtimeLabel} session ${staleSessionId} is unavailable locally; falling back to cold start`
|
|
4383
|
+
);
|
|
4384
|
+
this.broadcastActivity(
|
|
4385
|
+
agentId,
|
|
4386
|
+
"working",
|
|
4387
|
+
`Stored ${runtimeLabel} session missing; cold-starting a new session\u2026`,
|
|
4388
|
+
[{ kind: "text", text: `Stored ${runtimeLabel} session ${staleSessionId} was not found locally. Falling back to a cold start.` }]
|
|
4389
|
+
);
|
|
4390
|
+
this.startAgent(
|
|
4391
|
+
agentId,
|
|
4392
|
+
restartConfig,
|
|
4393
|
+
ap.startupWakeMessage,
|
|
4394
|
+
ap.startupUnreadSummary,
|
|
4395
|
+
ap.startupResumePrompt,
|
|
4396
|
+
ap.launchId || void 0
|
|
4397
|
+
).catch((err) => {
|
|
4398
|
+
logger.error(`[Agent ${agentId}] Cold start recovery failed`, err);
|
|
4399
|
+
this.sendAgentStatus(agentId, "inactive", ap.launchId);
|
|
4400
|
+
this.broadcastActivity(agentId, "offline", `Crashed (${summary})`, [], ap.launchId);
|
|
4401
|
+
});
|
|
4402
|
+
return;
|
|
4403
|
+
}
|
|
4404
|
+
if (processEndedCleanly) {
|
|
4405
|
+
let queuedWakeMessage;
|
|
4406
|
+
if (!ap.driver.supportsStdinNotification) {
|
|
4407
|
+
while (ap.inbox.length > 0) {
|
|
4408
|
+
const candidate = ap.inbox.shift();
|
|
4409
|
+
if (this.shouldDeferWakeMessage(agentId, ap.driver, candidate)) continue;
|
|
4410
|
+
queuedWakeMessage = candidate;
|
|
4411
|
+
break;
|
|
4412
|
+
}
|
|
4413
|
+
}
|
|
3631
4414
|
const unreadSummary2 = queuedWakeMessage ? buildUnreadSummary(ap.inbox, formatChannelLabel(queuedWakeMessage)) : void 0;
|
|
3632
4415
|
if (queuedWakeMessage) {
|
|
3633
4416
|
logger.info(`[Agent ${agentId}] Turn completed; restarting immediately for queued message`);
|
|
@@ -3662,26 +4445,6 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
3662
4445
|
} else {
|
|
3663
4446
|
this.idleAgentConfigs.delete(agentId);
|
|
3664
4447
|
const reason = formatCrashReason(finalCode, finalSignal, ap);
|
|
3665
|
-
const summary = summarizeCrash(finalCode, finalSignal);
|
|
3666
|
-
if (isMissingResumeSession(ap)) {
|
|
3667
|
-
const staleSessionId = ap.sessionId;
|
|
3668
|
-
const restartConfig = { ...ap.config, sessionId: null };
|
|
3669
|
-
logger.warn(
|
|
3670
|
-
`[Agent ${agentId}] Stored Claude session ${staleSessionId} is unavailable locally; falling back to cold start`
|
|
3671
|
-
);
|
|
3672
|
-
this.broadcastActivity(
|
|
3673
|
-
agentId,
|
|
3674
|
-
"working",
|
|
3675
|
-
"Stored Claude session missing; cold-starting a new session\u2026",
|
|
3676
|
-
[{ kind: "text", text: `Stored Claude session ${staleSessionId} was not found locally. Falling back to a cold start.` }]
|
|
3677
|
-
);
|
|
3678
|
-
this.startAgent(agentId, restartConfig, void 0, void 0, void 0, ap.launchId || void 0).catch((err) => {
|
|
3679
|
-
logger.error(`[Agent ${agentId}] Cold start recovery failed`, err);
|
|
3680
|
-
this.sendAgentStatus(agentId, "inactive", ap.launchId);
|
|
3681
|
-
this.broadcastActivity(agentId, "offline", `Crashed (${summary})`, [], ap.launchId);
|
|
3682
|
-
});
|
|
3683
|
-
return;
|
|
3684
|
-
}
|
|
3685
4448
|
logger.error(`[Agent ${agentId}] Process crashed (${reason}) \u2014 marking inactive`);
|
|
3686
4449
|
this.sendAgentStatus(agentId, "inactive", ap.launchId);
|
|
3687
4450
|
if (terminalFailureDetail) {
|
|
@@ -3747,6 +4510,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
3747
4510
|
return leftKeys.every((key) => left?.[key] === right?.[key]);
|
|
3748
4511
|
}
|
|
3749
4512
|
async stopAgent(agentId, { wait = false, silent = false } = {}) {
|
|
4513
|
+
this.cancelQueuedAgentStart(agentId, "stop requested");
|
|
3750
4514
|
this.idleAgentConfigs.delete(agentId);
|
|
3751
4515
|
const ap = this.agents.get(agentId);
|
|
3752
4516
|
if (!ap) {
|
|
@@ -3755,6 +4519,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
3755
4519
|
}
|
|
3756
4520
|
return;
|
|
3757
4521
|
}
|
|
4522
|
+
this.releaseAgentStartPermit(agentId, "stop requested");
|
|
3758
4523
|
if (ap.notificationTimer) {
|
|
3759
4524
|
clearTimeout(ap.notificationTimer);
|
|
3760
4525
|
}
|
|
@@ -3796,7 +4561,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
3796
4561
|
deliverMessage(agentId, message) {
|
|
3797
4562
|
const ap = this.agents.get(agentId);
|
|
3798
4563
|
if (!ap) {
|
|
3799
|
-
if (this.agentsStarting.has(agentId)) {
|
|
4564
|
+
if (this.agentsStarting.has(agentId) || this.queuedAgentStarts.has(agentId)) {
|
|
3800
4565
|
const pending = this.startingInboxes.get(agentId) || [];
|
|
3801
4566
|
pending.push(message);
|
|
3802
4567
|
this.startingInboxes.set(agentId, pending);
|
|
@@ -3804,6 +4569,10 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
3804
4569
|
}
|
|
3805
4570
|
const cached = this.idleAgentConfigs.get(agentId);
|
|
3806
4571
|
if (cached) {
|
|
4572
|
+
const driver = this.driverResolver(cached.config.runtime || "claude");
|
|
4573
|
+
if (this.shouldDeferWakeMessage(agentId, driver, message)) {
|
|
4574
|
+
return;
|
|
4575
|
+
}
|
|
3807
4576
|
logger.info(`[Agent ${agentId}] Starting from idle state for new message`);
|
|
3808
4577
|
this.idleAgentConfigs.delete(agentId);
|
|
3809
4578
|
this.startAgent(agentId, cached.config, message, void 0, void 0, cached.launchId || void 0).catch((err) => {
|
|
@@ -3812,6 +4581,9 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
3812
4581
|
}
|
|
3813
4582
|
return;
|
|
3814
4583
|
}
|
|
4584
|
+
if (this.shouldDeferWakeMessage(agentId, ap.driver, message)) {
|
|
4585
|
+
return;
|
|
4586
|
+
}
|
|
3815
4587
|
if (ap.isIdle && ap.driver.supportsStdinNotification && ap.sessionId) {
|
|
3816
4588
|
const nextMessages = ap.inbox.splice(0, ap.inbox.length);
|
|
3817
4589
|
nextMessages.push(message);
|
|
@@ -3840,7 +4612,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
3840
4612
|
}
|
|
3841
4613
|
}
|
|
3842
4614
|
async resetWorkspace(agentId) {
|
|
3843
|
-
const agentDataDir =
|
|
4615
|
+
const agentDataDir = path11.join(this.dataDir, agentId);
|
|
3844
4616
|
try {
|
|
3845
4617
|
await rm2(agentDataDir, { recursive: true, force: true });
|
|
3846
4618
|
logger.info(`[Agent ${agentId}] Workspace reset complete (${agentDataDir})`);
|
|
@@ -3849,6 +4621,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
3849
4621
|
}
|
|
3850
4622
|
}
|
|
3851
4623
|
async stopAll() {
|
|
4624
|
+
this.cancelAllQueuedAgentStarts("daemon shutdown");
|
|
3852
4625
|
this.idleAgentConfigs.clear();
|
|
3853
4626
|
const ids = [...this.agents.keys()];
|
|
3854
4627
|
await Promise.all(ids.map((id) => this.stopAgent(id, { wait: true, silent: true })));
|
|
@@ -3856,6 +4629,11 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
3856
4629
|
getRunningAgentIds() {
|
|
3857
4630
|
return [...this.agents.keys()];
|
|
3858
4631
|
}
|
|
4632
|
+
shouldDeferWakeMessage(agentId, driver, message) {
|
|
4633
|
+
if (!driver.shouldDeferWakeMessage?.(message)) return false;
|
|
4634
|
+
logger.info(`[Agent ${agentId}] Deferred non-concrete wake message for ${driver.id}`);
|
|
4635
|
+
return true;
|
|
4636
|
+
}
|
|
3859
4637
|
getAgentSessionId(agentId) {
|
|
3860
4638
|
return this.agents.get(agentId)?.sessionId ?? null;
|
|
3861
4639
|
}
|
|
@@ -3870,7 +4648,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
3870
4648
|
return result;
|
|
3871
4649
|
}
|
|
3872
4650
|
buildRuntimeProfileReport(agentId, config, sessionId, launchId) {
|
|
3873
|
-
const workspacePath =
|
|
4651
|
+
const workspacePath = path11.join(this.dataDir, agentId);
|
|
3874
4652
|
return {
|
|
3875
4653
|
agentId,
|
|
3876
4654
|
launchId,
|
|
@@ -4019,7 +4797,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
4019
4797
|
}
|
|
4020
4798
|
// Workspace file browsing
|
|
4021
4799
|
async getFileTree(agentId, dirPath) {
|
|
4022
|
-
const agentDir =
|
|
4800
|
+
const agentDir = path11.join(this.dataDir, agentId);
|
|
4023
4801
|
try {
|
|
4024
4802
|
await stat2(agentDir);
|
|
4025
4803
|
} catch {
|
|
@@ -4027,8 +4805,8 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
4027
4805
|
}
|
|
4028
4806
|
let targetDir = agentDir;
|
|
4029
4807
|
if (dirPath) {
|
|
4030
|
-
const resolved =
|
|
4031
|
-
if (!resolved.startsWith(agentDir +
|
|
4808
|
+
const resolved = path11.resolve(agentDir, dirPath);
|
|
4809
|
+
if (!resolved.startsWith(agentDir + path11.sep) && resolved !== agentDir) {
|
|
4032
4810
|
return [];
|
|
4033
4811
|
}
|
|
4034
4812
|
targetDir = resolved;
|
|
@@ -4036,9 +4814,9 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
4036
4814
|
return this.listDirectoryChildren(targetDir, agentDir);
|
|
4037
4815
|
}
|
|
4038
4816
|
async readFile(agentId, filePath) {
|
|
4039
|
-
const agentDir =
|
|
4040
|
-
const resolved =
|
|
4041
|
-
if (!resolved.startsWith(agentDir +
|
|
4817
|
+
const agentDir = path11.join(this.dataDir, agentId);
|
|
4818
|
+
const resolved = path11.resolve(agentDir, filePath);
|
|
4819
|
+
if (!resolved.startsWith(agentDir + path11.sep) && resolved !== agentDir) {
|
|
4042
4820
|
throw new Error("Access denied");
|
|
4043
4821
|
}
|
|
4044
4822
|
const info = await stat2(resolved);
|
|
@@ -4062,7 +4840,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
4062
4840
|
".sh",
|
|
4063
4841
|
".py"
|
|
4064
4842
|
]);
|
|
4065
|
-
const ext =
|
|
4843
|
+
const ext = path11.extname(resolved).toLowerCase();
|
|
4066
4844
|
if (!TEXT_EXTENSIONS.has(ext) && ext !== "") {
|
|
4067
4845
|
return { content: null, binary: true };
|
|
4068
4846
|
}
|
|
@@ -4088,14 +4866,14 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
4088
4866
|
async listSkills(agentId, runtimeHint) {
|
|
4089
4867
|
const agent = this.agents.get(agentId);
|
|
4090
4868
|
const runtime = runtimeHint || agent?.config.runtime || "claude";
|
|
4091
|
-
const home =
|
|
4092
|
-
const workspaceDir =
|
|
4869
|
+
const home = os4.homedir();
|
|
4870
|
+
const workspaceDir = path11.join(this.dataDir, agentId);
|
|
4093
4871
|
const paths = _AgentProcessManager.SKILL_PATHS[runtime] || _AgentProcessManager.SKILL_PATHS.claude;
|
|
4094
4872
|
const globalResults = await Promise.all(
|
|
4095
|
-
paths.global.map((p) => this.scanSkillsDir(
|
|
4873
|
+
paths.global.map((p) => this.scanSkillsDir(path11.join(home, p)))
|
|
4096
4874
|
);
|
|
4097
4875
|
const workspaceResults = await Promise.all(
|
|
4098
|
-
paths.workspace.map((p) => this.scanSkillsDir(
|
|
4876
|
+
paths.workspace.map((p) => this.scanSkillsDir(path11.join(workspaceDir, p)))
|
|
4099
4877
|
);
|
|
4100
4878
|
const dedup = (skills) => {
|
|
4101
4879
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -4124,7 +4902,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
4124
4902
|
const skills = [];
|
|
4125
4903
|
for (const entry of entries) {
|
|
4126
4904
|
if (entry.isDirectory() || entry.isSymbolicLink()) {
|
|
4127
|
-
const skillMd =
|
|
4905
|
+
const skillMd = path11.join(dir, entry.name, "SKILL.md");
|
|
4128
4906
|
try {
|
|
4129
4907
|
const content = await readFile(skillMd, "utf-8");
|
|
4130
4908
|
const skill = this.parseSkillMd(entry.name, content);
|
|
@@ -4135,7 +4913,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
4135
4913
|
} else if (entry.name.endsWith(".md")) {
|
|
4136
4914
|
const cmdName = entry.name.replace(/\.md$/, "");
|
|
4137
4915
|
try {
|
|
4138
|
-
const content = await readFile(
|
|
4916
|
+
const content = await readFile(path11.join(dir, entry.name), "utf-8");
|
|
4139
4917
|
const skill = this.parseSkillMd(cmdName, content);
|
|
4140
4918
|
skill.sourcePath = dir;
|
|
4141
4919
|
skills.push(skill);
|
|
@@ -4178,13 +4956,15 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
4178
4956
|
if (!hasToolStart) {
|
|
4179
4957
|
entries.push({ kind: "status", activity, detail });
|
|
4180
4958
|
}
|
|
4959
|
+
if (ap) ap.activityClientSeq += 1;
|
|
4181
4960
|
this.sendToServer({
|
|
4182
4961
|
type: "agent:activity",
|
|
4183
4962
|
agentId,
|
|
4184
4963
|
activity,
|
|
4185
4964
|
detail,
|
|
4186
4965
|
entries,
|
|
4187
|
-
launchId: launchIdOverride || ap?.launchId || void 0
|
|
4966
|
+
launchId: launchIdOverride || ap?.launchId || void 0,
|
|
4967
|
+
clientSeq: ap?.activityClientSeq
|
|
4188
4968
|
});
|
|
4189
4969
|
if (ap) {
|
|
4190
4970
|
ap.lastActivity = activity;
|
|
@@ -4196,12 +4976,14 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
4196
4976
|
this.recordRuntimeTraceEvent(agentId, ap, "activity.heartbeat.sent", {
|
|
4197
4977
|
activity: ap.lastActivity
|
|
4198
4978
|
});
|
|
4979
|
+
ap.activityClientSeq += 1;
|
|
4199
4980
|
this.sendToServer({
|
|
4200
4981
|
type: "agent:activity",
|
|
4201
4982
|
agentId,
|
|
4202
4983
|
activity: ap.lastActivity,
|
|
4203
4984
|
detail: ap.lastActivityDetail,
|
|
4204
|
-
launchId: launchIdOverride || ap.launchId || void 0
|
|
4985
|
+
launchId: launchIdOverride || ap.launchId || void 0,
|
|
4986
|
+
clientSeq: ap.activityClientSeq
|
|
4205
4987
|
});
|
|
4206
4988
|
}, ACTIVITY_HEARTBEAT_MS);
|
|
4207
4989
|
}
|
|
@@ -4213,6 +4995,42 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
4213
4995
|
}
|
|
4214
4996
|
}
|
|
4215
4997
|
}
|
|
4998
|
+
/**
|
|
4999
|
+
* Respond to a server-issued `agent:activity_probe`. Echoes the
|
|
5000
|
+
* agent's current `lastActivity` back through the existing
|
|
5001
|
+
* `agent:activity` upstream channel with the matching `probeId`.
|
|
5002
|
+
*
|
|
5003
|
+
* Why this exists: the server's stale-activity sweep used to
|
|
5004
|
+
* synthesize `online` whenever a transient state went 90s without
|
|
5005
|
+
* an update. That invented state without consulting ground truth
|
|
5006
|
+
* and produced "agent shows green/idle but is actually working" UI
|
|
5007
|
+
* staleness (#engineering:72283cf7 task #340 RCA).
|
|
5008
|
+
*
|
|
5009
|
+
* The new flow: server sends `agent:activity_probe` for stale
|
|
5010
|
+
* agents, daemon replies here with the *real* current activity, and
|
|
5011
|
+
* the server only falls back to synth-online if the probe times out
|
|
5012
|
+
* (5s). The body is intentionally minimal — no entries, no
|
|
5013
|
+
* heartbeat side-effects, no state mutation. We just echo what we
|
|
5014
|
+
* already know.
|
|
5015
|
+
*
|
|
5016
|
+
* If the agent is no longer running locally (`ap` undefined), we
|
|
5017
|
+
* report `offline` so the server stops believing the agent is busy.
|
|
5018
|
+
*/
|
|
5019
|
+
respondToActivityProbe(agentId, probeId) {
|
|
5020
|
+
const ap = this.agents.get(agentId);
|
|
5021
|
+
const activity = ap?.lastActivity || "offline";
|
|
5022
|
+
const detail = ap?.lastActivityDetail || (ap ? "" : "Agent not running");
|
|
5023
|
+
if (ap) ap.activityClientSeq += 1;
|
|
5024
|
+
this.sendToServer({
|
|
5025
|
+
type: "agent:activity",
|
|
5026
|
+
agentId,
|
|
5027
|
+
activity,
|
|
5028
|
+
detail,
|
|
5029
|
+
launchId: ap?.launchId || void 0,
|
|
5030
|
+
probeId,
|
|
5031
|
+
clientSeq: ap?.activityClientSeq
|
|
5032
|
+
});
|
|
5033
|
+
}
|
|
4216
5034
|
flushPendingTrajectory(agentId) {
|
|
4217
5035
|
const ap = this.agents.get(agentId);
|
|
4218
5036
|
const pending = ap?.pendingTrajectory;
|
|
@@ -4350,7 +5168,9 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
4350
5168
|
logger.info(
|
|
4351
5169
|
`[Agent ${agentId}] Claude gated steering flush reason=${reason} messages=${nextMessages.length}`
|
|
4352
5170
|
);
|
|
4353
|
-
|
|
5171
|
+
if (reason === "turn_end") {
|
|
5172
|
+
this.broadcastActivity(agentId, "working", "Message received");
|
|
5173
|
+
}
|
|
4354
5174
|
if (this.deliverMessagesViaStdin(agentId, ap, nextMessages, reason === "turn_end" ? "idle" : "busy")) {
|
|
4355
5175
|
return true;
|
|
4356
5176
|
}
|
|
@@ -4502,6 +5322,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
4502
5322
|
this.finishCompactionIfActive(agentId, "Context compaction finished (inferred from turn end)");
|
|
4503
5323
|
this.flushPendingTrajectory(agentId);
|
|
4504
5324
|
if (ap) {
|
|
5325
|
+
this.releaseAgentStartPermit(agentId, "initial turn ended");
|
|
4505
5326
|
this.clearGatedInFlightBatch(agentId, ap, "turn_end");
|
|
4506
5327
|
if (event.sessionId) ap.sessionId = event.sessionId;
|
|
4507
5328
|
ap.gatedSteering.outstandingToolUses = 0;
|
|
@@ -4527,6 +5348,16 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
4527
5348
|
this.broadcastActivity(agentId, "online", "Idle");
|
|
4528
5349
|
}
|
|
4529
5350
|
this.endRuntimeTrace(ap, "ok", { outcome: "turn-completed" });
|
|
5351
|
+
if (ap.driver.terminateProcessOnTurnEnd) {
|
|
5352
|
+
ap.expectedTerminationReason = "turn_end";
|
|
5353
|
+
logger.info(`[Agent ${agentId}] Turn completed; terminating ${ap.driver.id} process`);
|
|
5354
|
+
try {
|
|
5355
|
+
ap.process.kill("SIGTERM");
|
|
5356
|
+
} catch (err) {
|
|
5357
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
5358
|
+
logger.warn(`[Agent ${agentId}] Failed to terminate ${ap.driver.id} after turn_end: ${reason}`);
|
|
5359
|
+
}
|
|
5360
|
+
}
|
|
4530
5361
|
}
|
|
4531
5362
|
if (event.sessionId) {
|
|
4532
5363
|
this.sendToServer({ type: "agent:session", agentId, sessionId: event.sessionId, launchId: ap?.launchId || void 0 });
|
|
@@ -4588,7 +5419,6 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
4588
5419
|
if (ap.driver.busyDeliveryMode === "direct" && ap.inbox.length > 0) {
|
|
4589
5420
|
const queuedMessages = ap.inbox.splice(0, ap.inbox.length);
|
|
4590
5421
|
console.log(`[Agent ${agentId}] Delivering queued message via stdin while busy`);
|
|
4591
|
-
this.broadcastActivity(agentId, "working", "Message received");
|
|
4592
5422
|
if (this.deliverMessagesViaStdin(agentId, ap, queuedMessages, "busy")) {
|
|
4593
5423
|
return;
|
|
4594
5424
|
}
|
|
@@ -4607,11 +5437,11 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
4607
5437
|
if (messages.length === 0) return true;
|
|
4608
5438
|
const prompt = formatRuntimeProfileControlPrompt(messages) ?? (messages.length === 1 ? `New message received:
|
|
4609
5439
|
|
|
4610
|
-
${formatIncomingMessage(messages[0])}
|
|
5440
|
+
${formatIncomingMessage(messages[0], ap.driver)}
|
|
4611
5441
|
|
|
4612
5442
|
Respond as appropriate. Complete all your work before stopping.` : `New messages received:
|
|
4613
5443
|
|
|
4614
|
-
${messages.map((message) => formatIncomingMessage(message)).join("\n")}
|
|
5444
|
+
${messages.map((message) => formatIncomingMessage(message, ap.driver)).join("\n")}
|
|
4615
5445
|
|
|
4616
5446
|
Respond as appropriate. Complete all your work before stopping.`);
|
|
4617
5447
|
const encoded = ap.driver.encodeStdinMessage(prompt, ap.sessionId, { mode });
|
|
@@ -4649,8 +5479,8 @@ Respond as appropriate. Complete all your work before stopping.`);
|
|
|
4649
5479
|
const nodes = [];
|
|
4650
5480
|
for (const entry of entries) {
|
|
4651
5481
|
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
4652
|
-
const fullPath =
|
|
4653
|
-
const relativePath =
|
|
5482
|
+
const fullPath = path11.join(dir, entry.name);
|
|
5483
|
+
const relativePath = path11.relative(rootDir, fullPath);
|
|
4654
5484
|
let info;
|
|
4655
5485
|
try {
|
|
4656
5486
|
info = await stat2(fullPath);
|
|
@@ -4881,10 +5711,10 @@ var ReminderCache = class {
|
|
|
4881
5711
|
|
|
4882
5712
|
// src/machineLock.ts
|
|
4883
5713
|
import { createHash, randomUUID as randomUUID2 } from "crypto";
|
|
4884
|
-
import { mkdirSync as mkdirSync5, readFileSync as
|
|
4885
|
-
import
|
|
4886
|
-
import
|
|
4887
|
-
var DEFAULT_MACHINE_STATE_ROOT =
|
|
5714
|
+
import { mkdirSync as mkdirSync5, readFileSync as readFileSync4, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync8 } from "fs";
|
|
5715
|
+
import os5 from "os";
|
|
5716
|
+
import path12 from "path";
|
|
5717
|
+
var DEFAULT_MACHINE_STATE_ROOT = path12.join(os5.homedir(), ".slock", "machines");
|
|
4888
5718
|
var INCOMPLETE_LOCK_STALE_MS = 3e4;
|
|
4889
5719
|
var DaemonMachineLockConflictError = class extends Error {
|
|
4890
5720
|
code = "DAEMON_MACHINE_LOCK_HELD";
|
|
@@ -4903,11 +5733,11 @@ function getDaemonMachineLockId(apiKey) {
|
|
|
4903
5733
|
return `machine-${apiKeyFingerprint(apiKey).slice(0, 16)}`;
|
|
4904
5734
|
}
|
|
4905
5735
|
function ownerPath(lockDir) {
|
|
4906
|
-
return
|
|
5736
|
+
return path12.join(lockDir, "owner.json");
|
|
4907
5737
|
}
|
|
4908
5738
|
function readOwner(lockDir) {
|
|
4909
5739
|
try {
|
|
4910
|
-
return JSON.parse(
|
|
5740
|
+
return JSON.parse(readFileSync4(ownerPath(lockDir), "utf8"));
|
|
4911
5741
|
} catch {
|
|
4912
5742
|
return null;
|
|
4913
5743
|
}
|
|
@@ -4933,8 +5763,8 @@ function acquireDaemonMachineLock(options) {
|
|
|
4933
5763
|
const rootDir = options.rootDir ?? DEFAULT_MACHINE_STATE_ROOT;
|
|
4934
5764
|
const fingerprint = apiKeyFingerprint(options.apiKey);
|
|
4935
5765
|
const lockId = getDaemonMachineLockId(options.apiKey);
|
|
4936
|
-
const machineDir =
|
|
4937
|
-
const lockDir =
|
|
5766
|
+
const machineDir = path12.join(rootDir, lockId);
|
|
5767
|
+
const lockDir = path12.join(machineDir, "daemon.lock");
|
|
4938
5768
|
const token = randomUUID2();
|
|
4939
5769
|
mkdirSync5(machineDir, { recursive: true });
|
|
4940
5770
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
@@ -4943,7 +5773,7 @@ function acquireDaemonMachineLock(options) {
|
|
|
4943
5773
|
const owner = {
|
|
4944
5774
|
pid: process.pid,
|
|
4945
5775
|
token,
|
|
4946
|
-
hostname:
|
|
5776
|
+
hostname: os5.hostname(),
|
|
4947
5777
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4948
5778
|
serverUrl: options.serverUrl,
|
|
4949
5779
|
apiKeyFingerprint: fingerprint.slice(0, 16)
|
|
@@ -5006,23 +5836,23 @@ function readDaemonVersion(moduleUrl = import.meta.url) {
|
|
|
5006
5836
|
}
|
|
5007
5837
|
}
|
|
5008
5838
|
function resolveChatBridgePath(moduleUrl = import.meta.url) {
|
|
5009
|
-
const dirname =
|
|
5010
|
-
const jsPath =
|
|
5839
|
+
const dirname = path13.dirname(fileURLToPath(moduleUrl));
|
|
5840
|
+
const jsPath = path13.resolve(dirname, "chat-bridge.js");
|
|
5011
5841
|
try {
|
|
5012
5842
|
accessSync(jsPath);
|
|
5013
5843
|
return jsPath;
|
|
5014
5844
|
} catch {
|
|
5015
|
-
return
|
|
5845
|
+
return path13.resolve(dirname, "chat-bridge.ts");
|
|
5016
5846
|
}
|
|
5017
5847
|
}
|
|
5018
5848
|
function resolveSlockCliPath(moduleUrl = import.meta.url) {
|
|
5019
|
-
const thisDir =
|
|
5020
|
-
const bundledDistPath =
|
|
5849
|
+
const thisDir = path13.dirname(fileURLToPath(moduleUrl));
|
|
5850
|
+
const bundledDistPath = path13.resolve(thisDir, "cli", "index.js");
|
|
5021
5851
|
try {
|
|
5022
5852
|
accessSync(bundledDistPath);
|
|
5023
5853
|
return bundledDistPath;
|
|
5024
5854
|
} catch {
|
|
5025
|
-
const workspaceDistPath =
|
|
5855
|
+
const workspaceDistPath = path13.resolve(thisDir, "..", "..", "cli", "dist", "index.js");
|
|
5026
5856
|
accessSync(workspaceDistPath);
|
|
5027
5857
|
return workspaceDistPath;
|
|
5028
5858
|
}
|
|
@@ -5031,9 +5861,14 @@ function detectRuntimes() {
|
|
|
5031
5861
|
const ids = [];
|
|
5032
5862
|
const versions = {};
|
|
5033
5863
|
for (const runtime of RUNTIMES) {
|
|
5864
|
+
const driver = getDriver(runtime.id);
|
|
5034
5865
|
try {
|
|
5035
|
-
|
|
5036
|
-
|
|
5866
|
+
if (driver.probe) {
|
|
5867
|
+
const probe = driver.probe();
|
|
5868
|
+
if (!probe.available) {
|
|
5869
|
+
if (probe.version) versions[runtime.id] = probe.version;
|
|
5870
|
+
continue;
|
|
5871
|
+
}
|
|
5037
5872
|
ids.push(runtime.id);
|
|
5038
5873
|
if (probe.version) versions[runtime.id] = probe.version;
|
|
5039
5874
|
continue;
|
|
@@ -5077,6 +5912,8 @@ function summarizeIncomingMessage(msg) {
|
|
|
5077
5912
|
return `(agent=${msg.agentId}, path=${msg.path})`;
|
|
5078
5913
|
case "agent:skills:list":
|
|
5079
5914
|
return `(agent=${msg.agentId}, runtime=${msg.runtime || "auto"})`;
|
|
5915
|
+
case "agent:activity_probe":
|
|
5916
|
+
return `(agent=${msg.agentId}, probe=${msg.probeId.slice(0, 8)}, purpose=${msg.purpose})`;
|
|
5080
5917
|
case "machine:workspace:delete":
|
|
5081
5918
|
return `(directory=${msg.directoryName})`;
|
|
5082
5919
|
case "machine:runtime_models:detect":
|
|
@@ -5134,7 +5971,7 @@ var DaemonCore = class {
|
|
|
5134
5971
|
}
|
|
5135
5972
|
resolveMachineStateRoot() {
|
|
5136
5973
|
if (this.options.machineStateDir) return this.options.machineStateDir;
|
|
5137
|
-
if (this.options.dataDir) return
|
|
5974
|
+
if (this.options.dataDir) return path13.join(path13.dirname(this.options.dataDir), "machines");
|
|
5138
5975
|
return DEFAULT_MACHINE_STATE_ROOT;
|
|
5139
5976
|
}
|
|
5140
5977
|
start() {
|
|
@@ -5264,6 +6101,9 @@ var DaemonCore = class {
|
|
|
5264
6101
|
this.connection.send({ type: "agent:skills:list_result", agentId: msg.agentId, global: [], workspace: [] });
|
|
5265
6102
|
});
|
|
5266
6103
|
break;
|
|
6104
|
+
case "agent:activity_probe":
|
|
6105
|
+
this.agentManager.respondToActivityProbe(msg.agentId, msg.probeId);
|
|
6106
|
+
break;
|
|
5267
6107
|
case "machine:workspace:scan":
|
|
5268
6108
|
logger.info("[Daemon] Scanning all workspace directories");
|
|
5269
6109
|
this.agentManager.scanAllWorkspaces().then((directories) => {
|
|
@@ -5281,7 +6121,12 @@ var DaemonCore = class {
|
|
|
5281
6121
|
const detect = typeof driver?.detectModels === "function" ? driver.detectModels() : Promise.resolve(null);
|
|
5282
6122
|
Promise.resolve(detect).then((result) => {
|
|
5283
6123
|
if (result) {
|
|
5284
|
-
|
|
6124
|
+
const verified = driver.model.detectedModelsVerifiedAs;
|
|
6125
|
+
const models = result.models.map((model) => ({
|
|
6126
|
+
...model,
|
|
6127
|
+
verified: model.verified ?? verified
|
|
6128
|
+
}));
|
|
6129
|
+
this.connection.send({ type: "machine:runtime_models:result", requestId: msg.requestId, models, default: result.default });
|
|
5285
6130
|
} else {
|
|
5286
6131
|
this.connection.send({ type: "machine:runtime_models:result", requestId: msg.requestId, error: "unsupported" });
|
|
5287
6132
|
}
|
|
@@ -5325,8 +6170,8 @@ var DaemonCore = class {
|
|
|
5325
6170
|
capabilities: ["agent:start", "agent:stop", "agent:deliver", "workspace:files"],
|
|
5326
6171
|
runtimes,
|
|
5327
6172
|
runningAgents: this.agentManager.getRunningAgentIds(),
|
|
5328
|
-
hostname: this.options.hostname ??
|
|
5329
|
-
os: this.options.osDescription ?? `${
|
|
6173
|
+
hostname: this.options.hostname ?? os6.hostname(),
|
|
6174
|
+
os: this.options.osDescription ?? `${os6.platform()} ${os6.arch()}`,
|
|
5330
6175
|
daemonVersion: this.daemonVersion
|
|
5331
6176
|
});
|
|
5332
6177
|
for (const agentId of this.agentManager.getRunningAgentIds()) {
|