@slock-ai/daemon 0.42.0 → 0.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-RNEIFBXW.js → chunk-37O7EHYE.js} +564 -107
- package/dist/cli/index.js +68 -18
- 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
|
|
|
@@ -2240,6 +2305,17 @@ var CursorDriver = class {
|
|
|
2240
2305
|
import { spawn as spawn5 } from "child_process";
|
|
2241
2306
|
import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync3, existsSync as existsSync4 } from "fs";
|
|
2242
2307
|
import path7 from "path";
|
|
2308
|
+
function buildGeminiSpawnEnv(ctx) {
|
|
2309
|
+
return {
|
|
2310
|
+
...process.env,
|
|
2311
|
+
FORCE_COLOR: "0",
|
|
2312
|
+
NO_COLOR: "1",
|
|
2313
|
+
// Gemini CLI's trusted-workspace gate breaks our managed headless flow
|
|
2314
|
+
// unless we explicitly trust the daemon-owned agent workspace.
|
|
2315
|
+
GEMINI_CLI_TRUST_WORKSPACE: "true",
|
|
2316
|
+
...ctx.config.envVars || {}
|
|
2317
|
+
};
|
|
2318
|
+
}
|
|
2243
2319
|
var GeminiDriver = class {
|
|
2244
2320
|
id = "gemini";
|
|
2245
2321
|
supportsStdinNotification = false;
|
|
@@ -2279,7 +2355,7 @@ var GeminiDriver = class {
|
|
|
2279
2355
|
if (ctx.config.sessionId) {
|
|
2280
2356
|
args.push("--resume", ctx.config.sessionId);
|
|
2281
2357
|
}
|
|
2282
|
-
const spawnEnv =
|
|
2358
|
+
const spawnEnv = buildGeminiSpawnEnv(ctx);
|
|
2283
2359
|
const proc = spawn5("gemini", args, {
|
|
2284
2360
|
cwd: ctx.workingDirectory,
|
|
2285
2361
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -2581,6 +2657,279 @@ function detectKimiModels(home = os2.homedir()) {
|
|
|
2581
2657
|
return { models, default: defaultModel };
|
|
2582
2658
|
}
|
|
2583
2659
|
|
|
2660
|
+
// src/drivers/opencode.ts
|
|
2661
|
+
import { spawn as spawn7 } from "child_process";
|
|
2662
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
2663
|
+
import os3 from "os";
|
|
2664
|
+
import path9 from "path";
|
|
2665
|
+
var CHAT_MCP_SERVER_NAME = "chat";
|
|
2666
|
+
var CHAT_MCP_TOOL_PREFIX = `${CHAT_MCP_SERVER_NAME}_`;
|
|
2667
|
+
var SLOCK_AGENT_NAME = "slock";
|
|
2668
|
+
var NO_MESSAGE_PROMPT = "No new messages are pending. Stop now.";
|
|
2669
|
+
var FIRST_MESSAGE_TASK_PREFIX = "First message task (system-triggered):";
|
|
2670
|
+
var MIN_SUPPORTED_OPENCODE_VERSION = "1.14.30";
|
|
2671
|
+
function buildChatBridgeCommand(ctx) {
|
|
2672
|
+
const isTsSource = ctx.chatBridgePath.endsWith(".ts");
|
|
2673
|
+
return [
|
|
2674
|
+
isTsSource ? "npx" : "node",
|
|
2675
|
+
...isTsSource ? ["tsx", ctx.chatBridgePath] : [ctx.chatBridgePath],
|
|
2676
|
+
"--agent-id",
|
|
2677
|
+
ctx.agentId,
|
|
2678
|
+
"--server-url",
|
|
2679
|
+
ctx.config.serverUrl,
|
|
2680
|
+
"--auth-token",
|
|
2681
|
+
ctx.config.authToken || ctx.daemonApiKey,
|
|
2682
|
+
"--runtime",
|
|
2683
|
+
"opencode",
|
|
2684
|
+
...ctx.launchId ? ["--launch-id", ctx.launchId] : [],
|
|
2685
|
+
"--runtime-actions-only"
|
|
2686
|
+
];
|
|
2687
|
+
}
|
|
2688
|
+
function parseOpenCodeConfigContent(raw) {
|
|
2689
|
+
if (!raw) return {};
|
|
2690
|
+
try {
|
|
2691
|
+
const parsed = JSON.parse(raw);
|
|
2692
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
2693
|
+
return parsed;
|
|
2694
|
+
}
|
|
2695
|
+
} catch {
|
|
2696
|
+
}
|
|
2697
|
+
return {};
|
|
2698
|
+
}
|
|
2699
|
+
function parseUserOpenCodeConfig(ctx) {
|
|
2700
|
+
const raw = ctx.config.envVars?.OPENCODE_CONFIG_CONTENT;
|
|
2701
|
+
return parseOpenCodeConfigContent(raw);
|
|
2702
|
+
}
|
|
2703
|
+
function readLocalOpenCodeConfig(home = os3.homedir()) {
|
|
2704
|
+
const configPath = path9.join(home, ".config", "opencode", "opencode.json");
|
|
2705
|
+
try {
|
|
2706
|
+
return parseOpenCodeConfigContent(readFileSync3(configPath, "utf8"));
|
|
2707
|
+
} catch {
|
|
2708
|
+
}
|
|
2709
|
+
return {};
|
|
2710
|
+
}
|
|
2711
|
+
function recordField(value) {
|
|
2712
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
2713
|
+
}
|
|
2714
|
+
function parseSemver(version) {
|
|
2715
|
+
const match = version.match(/(\d+)\.(\d+)\.(\d+)/);
|
|
2716
|
+
if (!match) return null;
|
|
2717
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
2718
|
+
}
|
|
2719
|
+
function isSupportedOpenCodeVersion(version) {
|
|
2720
|
+
if (!version) return true;
|
|
2721
|
+
const actual = parseSemver(version);
|
|
2722
|
+
const minimum = parseSemver(MIN_SUPPORTED_OPENCODE_VERSION);
|
|
2723
|
+
if (!actual || !minimum) return true;
|
|
2724
|
+
for (let i = 0; i < 3; i += 1) {
|
|
2725
|
+
if (actual[i] > minimum[i]) return true;
|
|
2726
|
+
if (actual[i] < minimum[i]) return false;
|
|
2727
|
+
}
|
|
2728
|
+
return true;
|
|
2729
|
+
}
|
|
2730
|
+
function unsupportedOpenCodeVersionMessage(version) {
|
|
2731
|
+
if (!version || isSupportedOpenCodeVersion(version)) return null;
|
|
2732
|
+
return `OpenCode CLI ${version} is unsupported; requires OpenCode >= ${MIN_SUPPORTED_OPENCODE_VERSION}. Upgrade opencode before starting this runtime.`;
|
|
2733
|
+
}
|
|
2734
|
+
function mergeOpenCodeConfigs(localConfig, envConfig) {
|
|
2735
|
+
return {
|
|
2736
|
+
...localConfig,
|
|
2737
|
+
...envConfig,
|
|
2738
|
+
provider: {
|
|
2739
|
+
...recordField(localConfig.provider),
|
|
2740
|
+
...recordField(envConfig.provider)
|
|
2741
|
+
},
|
|
2742
|
+
agent: {
|
|
2743
|
+
...recordField(localConfig.agent),
|
|
2744
|
+
...recordField(envConfig.agent)
|
|
2745
|
+
},
|
|
2746
|
+
mcp: {
|
|
2747
|
+
...recordField(localConfig.mcp),
|
|
2748
|
+
...recordField(envConfig.mcp)
|
|
2749
|
+
}
|
|
2750
|
+
};
|
|
2751
|
+
}
|
|
2752
|
+
function buildOpenCodeConfig(ctx, home = os3.homedir()) {
|
|
2753
|
+
const userConfig = mergeOpenCodeConfigs(readLocalOpenCodeConfig(home), parseUserOpenCodeConfig(ctx));
|
|
2754
|
+
const userAgents = recordField(userConfig.agent);
|
|
2755
|
+
const userSlockAgent = recordField(userAgents[SLOCK_AGENT_NAME]);
|
|
2756
|
+
return {
|
|
2757
|
+
...userConfig,
|
|
2758
|
+
$schema: "https://opencode.ai/config.json",
|
|
2759
|
+
agent: {
|
|
2760
|
+
...userAgents,
|
|
2761
|
+
[SLOCK_AGENT_NAME]: {
|
|
2762
|
+
...userSlockAgent,
|
|
2763
|
+
description: "Slock agent runtime",
|
|
2764
|
+
prompt: ctx.standingPrompt
|
|
2765
|
+
}
|
|
2766
|
+
},
|
|
2767
|
+
mcp: {
|
|
2768
|
+
...recordField(userConfig.mcp),
|
|
2769
|
+
[CHAT_MCP_SERVER_NAME]: {
|
|
2770
|
+
type: "local",
|
|
2771
|
+
command: buildChatBridgeCommand(ctx),
|
|
2772
|
+
enabled: true
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
};
|
|
2776
|
+
}
|
|
2777
|
+
function buildOpenCodeLaunchOptions(ctx, home = os3.homedir()) {
|
|
2778
|
+
const slock = prepareCliTransport(ctx, { NO_COLOR: "1" });
|
|
2779
|
+
const config = buildOpenCodeConfig(ctx, home);
|
|
2780
|
+
const env = {
|
|
2781
|
+
...slock.spawnEnv,
|
|
2782
|
+
OPENCODE_CONFIG_CONTENT: JSON.stringify(config)
|
|
2783
|
+
};
|
|
2784
|
+
const args = [
|
|
2785
|
+
"run",
|
|
2786
|
+
"--format",
|
|
2787
|
+
"json",
|
|
2788
|
+
"--dangerously-skip-permissions",
|
|
2789
|
+
"--pure",
|
|
2790
|
+
"--dir",
|
|
2791
|
+
ctx.workingDirectory
|
|
2792
|
+
];
|
|
2793
|
+
if (ctx.config.model && ctx.config.model !== "default") {
|
|
2794
|
+
args.push("--model", ctx.config.model);
|
|
2795
|
+
}
|
|
2796
|
+
args.push("--agent", SLOCK_AGENT_NAME);
|
|
2797
|
+
if (ctx.config.sessionId) {
|
|
2798
|
+
args.push("--session", ctx.config.sessionId);
|
|
2799
|
+
}
|
|
2800
|
+
const turnPrompt = ctx.prompt === ctx.standingPrompt ? NO_MESSAGE_PROMPT : ctx.prompt;
|
|
2801
|
+
args.push("--", turnPrompt);
|
|
2802
|
+
return { args, env, config };
|
|
2803
|
+
}
|
|
2804
|
+
function detectOpenCodeModels(home = os3.homedir()) {
|
|
2805
|
+
const models = [...RUNTIME_MODELS.opencode || []];
|
|
2806
|
+
const providers = recordField(readLocalOpenCodeConfig(home).provider);
|
|
2807
|
+
for (const [providerId, providerConfig] of Object.entries(providers)) {
|
|
2808
|
+
const providerModels = recordField(recordField(providerConfig).models);
|
|
2809
|
+
for (const [modelId, modelConfig] of Object.entries(providerModels)) {
|
|
2810
|
+
const fullId = `${providerId}/${modelId}`;
|
|
2811
|
+
if (models.some((model2) => model2.id === fullId)) continue;
|
|
2812
|
+
const model = recordField(modelConfig);
|
|
2813
|
+
const name = typeof model.name === "string" && model.name.length > 0 ? model.name : fullId;
|
|
2814
|
+
models.push({ id: fullId, label: name });
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2817
|
+
return models.length > 0 ? { models } : null;
|
|
2818
|
+
}
|
|
2819
|
+
function isSystemFirstMessageTask(message) {
|
|
2820
|
+
return message.sender_id === "system" && message.channel_type === "channel" && message.channel_name === "all" && message.content.trimStart().startsWith(FIRST_MESSAGE_TASK_PREFIX);
|
|
2821
|
+
}
|
|
2822
|
+
function buildOpenCodeSystemPrompt(config) {
|
|
2823
|
+
return buildCliTransportSystemPrompt(config, {
|
|
2824
|
+
toolPrefix: CHAT_MCP_TOOL_PREFIX,
|
|
2825
|
+
extraCriticalRules: [
|
|
2826
|
+
"- 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."
|
|
2827
|
+
],
|
|
2828
|
+
postStartupNotes: [
|
|
2829
|
+
"**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."
|
|
2830
|
+
],
|
|
2831
|
+
includeStdinNotificationSection: false,
|
|
2832
|
+
messageNotificationStyle: "poll"
|
|
2833
|
+
});
|
|
2834
|
+
}
|
|
2835
|
+
var OpenCodeDriver = class {
|
|
2836
|
+
id = "opencode";
|
|
2837
|
+
supportsStdinNotification = false;
|
|
2838
|
+
mcpToolPrefix = CHAT_MCP_TOOL_PREFIX;
|
|
2839
|
+
busyDeliveryMode = "none";
|
|
2840
|
+
terminateProcessOnTurnEnd = true;
|
|
2841
|
+
deferSpawnUntilMessage = true;
|
|
2842
|
+
usesSlockCliForCommunication = true;
|
|
2843
|
+
shouldDeferWakeMessage(message) {
|
|
2844
|
+
return isSystemFirstMessageTask(message);
|
|
2845
|
+
}
|
|
2846
|
+
sessionId = null;
|
|
2847
|
+
sessionAnnounced = false;
|
|
2848
|
+
probe() {
|
|
2849
|
+
if (!resolveCommandOnPath("opencode")) return { available: false };
|
|
2850
|
+
const version = readCommandVersion("opencode") || void 0;
|
|
2851
|
+
const unsupportedMessage = unsupportedOpenCodeVersionMessage(version);
|
|
2852
|
+
if (unsupportedMessage) {
|
|
2853
|
+
return {
|
|
2854
|
+
available: false,
|
|
2855
|
+
version: `${version} (requires >= ${MIN_SUPPORTED_OPENCODE_VERSION})`
|
|
2856
|
+
};
|
|
2857
|
+
}
|
|
2858
|
+
return { available: true, version };
|
|
2859
|
+
}
|
|
2860
|
+
async detectModels() {
|
|
2861
|
+
return detectOpenCodeModels();
|
|
2862
|
+
}
|
|
2863
|
+
spawn(ctx) {
|
|
2864
|
+
this.sessionId = ctx.config.sessionId || null;
|
|
2865
|
+
this.sessionAnnounced = false;
|
|
2866
|
+
const unsupportedMessage = unsupportedOpenCodeVersionMessage(readCommandVersion("opencode"));
|
|
2867
|
+
if (unsupportedMessage) {
|
|
2868
|
+
throw new Error(unsupportedMessage);
|
|
2869
|
+
}
|
|
2870
|
+
const launch = buildOpenCodeLaunchOptions(ctx);
|
|
2871
|
+
const proc = spawn7("opencode", launch.args, {
|
|
2872
|
+
cwd: ctx.workingDirectory,
|
|
2873
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
2874
|
+
env: launch.env,
|
|
2875
|
+
shell: process.platform === "win32"
|
|
2876
|
+
});
|
|
2877
|
+
proc.stdin?.end();
|
|
2878
|
+
return { process: proc };
|
|
2879
|
+
}
|
|
2880
|
+
parseLine(line) {
|
|
2881
|
+
let event;
|
|
2882
|
+
try {
|
|
2883
|
+
event = JSON.parse(line);
|
|
2884
|
+
} catch {
|
|
2885
|
+
return [];
|
|
2886
|
+
}
|
|
2887
|
+
const events = [];
|
|
2888
|
+
if (event.sessionID && event.sessionID !== this.sessionId) {
|
|
2889
|
+
this.sessionId = event.sessionID;
|
|
2890
|
+
}
|
|
2891
|
+
if (!this.sessionAnnounced && this.sessionId) {
|
|
2892
|
+
events.push({ kind: "session_init", sessionId: this.sessionId });
|
|
2893
|
+
this.sessionAnnounced = true;
|
|
2894
|
+
}
|
|
2895
|
+
switch (event.type) {
|
|
2896
|
+
case "step_start":
|
|
2897
|
+
events.push({ kind: "thinking", text: "" });
|
|
2898
|
+
break;
|
|
2899
|
+
case "text":
|
|
2900
|
+
if (typeof event.part?.text === "string" && event.part.text.length > 0) {
|
|
2901
|
+
events.push({ kind: "text", text: event.part.text });
|
|
2902
|
+
}
|
|
2903
|
+
break;
|
|
2904
|
+
case "tool_use":
|
|
2905
|
+
events.push({
|
|
2906
|
+
kind: "tool_call",
|
|
2907
|
+
name: event.part?.tool || "unknown_tool",
|
|
2908
|
+
input: event.part?.state?.input
|
|
2909
|
+
});
|
|
2910
|
+
break;
|
|
2911
|
+
case "step_finish":
|
|
2912
|
+
if (event.part?.reason !== "tool-calls") {
|
|
2913
|
+
events.push({ kind: "turn_end", sessionId: this.sessionId || void 0 });
|
|
2914
|
+
}
|
|
2915
|
+
break;
|
|
2916
|
+
case "error": {
|
|
2917
|
+
const message = event.error?.data?.message || event.error?.message || (event.error?.name ? `${event.error.name} (no message)` : null) || "Unknown OpenCode error";
|
|
2918
|
+
events.push({ kind: "error", message });
|
|
2919
|
+
events.push({ kind: "turn_end", sessionId: this.sessionId || void 0 });
|
|
2920
|
+
break;
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2923
|
+
return events;
|
|
2924
|
+
}
|
|
2925
|
+
encodeStdinMessage(_text, _sessionId, _opts) {
|
|
2926
|
+
return null;
|
|
2927
|
+
}
|
|
2928
|
+
buildSystemPrompt(config, _agentId) {
|
|
2929
|
+
return buildOpenCodeSystemPrompt(config);
|
|
2930
|
+
}
|
|
2931
|
+
};
|
|
2932
|
+
|
|
2584
2933
|
// src/drivers/index.ts
|
|
2585
2934
|
var driverFactories = {
|
|
2586
2935
|
claude: () => new ClaudeDriver(),
|
|
@@ -2588,7 +2937,8 @@ var driverFactories = {
|
|
|
2588
2937
|
copilot: () => new CopilotDriver(),
|
|
2589
2938
|
cursor: () => new CursorDriver(),
|
|
2590
2939
|
gemini: () => new GeminiDriver(),
|
|
2591
|
-
kimi: () => new KimiDriver()
|
|
2940
|
+
kimi: () => new KimiDriver(),
|
|
2941
|
+
opencode: () => new OpenCodeDriver()
|
|
2592
2942
|
};
|
|
2593
2943
|
function getDriver(runtimeId) {
|
|
2594
2944
|
const createDriver = driverFactories[runtimeId];
|
|
@@ -2601,7 +2951,7 @@ function getDriver(runtimeId) {
|
|
|
2601
2951
|
|
|
2602
2952
|
// src/workspaces.ts
|
|
2603
2953
|
import { readdir, rm, stat } from "fs/promises";
|
|
2604
|
-
import
|
|
2954
|
+
import path10 from "path";
|
|
2605
2955
|
function isValidWorkspaceDirectoryName(directoryName) {
|
|
2606
2956
|
return !directoryName.includes("/") && !directoryName.includes("\\") && !directoryName.includes("..");
|
|
2607
2957
|
}
|
|
@@ -2609,7 +2959,7 @@ function resolveWorkspaceDirectoryPath(dataDir, directoryName) {
|
|
|
2609
2959
|
if (!isValidWorkspaceDirectoryName(directoryName)) {
|
|
2610
2960
|
return null;
|
|
2611
2961
|
}
|
|
2612
|
-
return
|
|
2962
|
+
return path10.join(dataDir, directoryName);
|
|
2613
2963
|
}
|
|
2614
2964
|
function emptyWorkspaceDirectorySummary(latestMtime = /* @__PURE__ */ new Date(0)) {
|
|
2615
2965
|
return {
|
|
@@ -2658,7 +3008,7 @@ async function summarizeWorkspaceDirectory(dirPath) {
|
|
|
2658
3008
|
return summary;
|
|
2659
3009
|
}
|
|
2660
3010
|
const childSummaries = await Promise.all(
|
|
2661
|
-
entries.map((entry) => summarizeWorkspaceEntry(
|
|
3011
|
+
entries.map((entry) => summarizeWorkspaceEntry(path10.join(dirPath, entry.name), entry))
|
|
2662
3012
|
);
|
|
2663
3013
|
for (const childSummary of childSummaries) {
|
|
2664
3014
|
summary = mergeWorkspaceDirectorySummaries(summary, childSummary);
|
|
@@ -2677,7 +3027,7 @@ async function scanWorkspaceDirectories(dataDir) {
|
|
|
2677
3027
|
if (!entry.isDirectory()) {
|
|
2678
3028
|
return null;
|
|
2679
3029
|
}
|
|
2680
|
-
const dirPath =
|
|
3030
|
+
const dirPath = path10.join(dataDir, entry.name);
|
|
2681
3031
|
try {
|
|
2682
3032
|
const summary = await summarizeWorkspaceDirectory(dirPath);
|
|
2683
3033
|
return {
|
|
@@ -2709,7 +3059,7 @@ async function deleteWorkspaceDirectory(dataDir, directoryName) {
|
|
|
2709
3059
|
}
|
|
2710
3060
|
|
|
2711
3061
|
// src/agentProcessManager.ts
|
|
2712
|
-
var DATA_DIR =
|
|
3062
|
+
var DATA_DIR = path11.join(os4.homedir(), ".slock", "agents");
|
|
2713
3063
|
function toLocalTime(iso) {
|
|
2714
3064
|
const d = new Date(iso);
|
|
2715
3065
|
if (isNaN(d.getTime())) return iso;
|
|
@@ -2750,12 +3100,12 @@ function findSessionJsonl(root, predicate) {
|
|
|
2750
3100
|
for (const entry of entries) {
|
|
2751
3101
|
if (++visited > maxEntries) return null;
|
|
2752
3102
|
if (!entry.isFile() || !predicate(entry.name)) continue;
|
|
2753
|
-
return
|
|
3103
|
+
return path11.join(dir, entry.name);
|
|
2754
3104
|
}
|
|
2755
3105
|
for (const entry of entries) {
|
|
2756
3106
|
if (++visited > maxEntries) return null;
|
|
2757
3107
|
if (!entry.isDirectory()) continue;
|
|
2758
|
-
const found = visit(
|
|
3108
|
+
const found = visit(path11.join(dir, entry.name), depth - 1);
|
|
2759
3109
|
if (found) return found;
|
|
2760
3110
|
}
|
|
2761
3111
|
return null;
|
|
@@ -2768,9 +3118,9 @@ function safeSessionFilename(value) {
|
|
|
2768
3118
|
}
|
|
2769
3119
|
function writeRuntimeSessionHandoff(runtime, sessionId, fallbackDir) {
|
|
2770
3120
|
try {
|
|
2771
|
-
const dir =
|
|
3121
|
+
const dir = path11.join(fallbackDir, ".slock", "runtime-sessions");
|
|
2772
3122
|
mkdirSync4(dir, { recursive: true });
|
|
2773
|
-
const filePath =
|
|
3123
|
+
const filePath = path11.join(dir, `${runtime}-${safeSessionFilename(sessionId)}.jsonl`);
|
|
2774
3124
|
writeFileSync7(filePath, JSON.stringify({
|
|
2775
3125
|
type: "runtime_session_handoff",
|
|
2776
3126
|
runtime,
|
|
@@ -2789,8 +3139,8 @@ function writeRuntimeSessionHandoff(runtime, sessionId, fallbackDir) {
|
|
|
2789
3139
|
return null;
|
|
2790
3140
|
}
|
|
2791
3141
|
}
|
|
2792
|
-
function resolveRuntimeSessionRef(runtime, sessionId, homeDir =
|
|
2793
|
-
const directPath =
|
|
3142
|
+
function resolveRuntimeSessionRef(runtime, sessionId, homeDir = os4.homedir(), fallbackDir) {
|
|
3143
|
+
const directPath = path11.isAbsolute(sessionId) ? sessionId : null;
|
|
2794
3144
|
if (directPath) {
|
|
2795
3145
|
try {
|
|
2796
3146
|
if (statSync(directPath).isFile()) {
|
|
@@ -2799,7 +3149,7 @@ function resolveRuntimeSessionRef(runtime, sessionId, homeDir = os3.homedir(), f
|
|
|
2799
3149
|
} catch {
|
|
2800
3150
|
}
|
|
2801
3151
|
}
|
|
2802
|
-
const resolvedPath = runtime === "claude" ? findSessionJsonl(
|
|
3152
|
+
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
3153
|
if (!resolvedPath && fallbackDir) {
|
|
2804
3154
|
const fallback = writeRuntimeSessionHandoff(runtime, sessionId, fallbackDir);
|
|
2805
3155
|
if (fallback) return fallback;
|
|
@@ -2830,12 +3180,38 @@ function formatThreadContextMessage(message) {
|
|
|
2830
3180
|
const senderType = formatVisibleActorType(message.sender_type);
|
|
2831
3181
|
return `- [msg=${msgId} time=${time}${senderType}] ${formatSenderHandle(message)}: ${message.content}`;
|
|
2832
3182
|
}
|
|
2833
|
-
function
|
|
3183
|
+
function mcpToolName(driver, name) {
|
|
3184
|
+
return `${driver?.mcpToolPrefix ?? ""}${name}`;
|
|
3185
|
+
}
|
|
3186
|
+
function communicationCommand(driver, name) {
|
|
3187
|
+
if (!driver?.usesSlockCliForCommunication) {
|
|
3188
|
+
return mcpToolName(driver, name);
|
|
3189
|
+
}
|
|
3190
|
+
switch (name) {
|
|
3191
|
+
case "send_message":
|
|
3192
|
+
return "slock message send";
|
|
3193
|
+
case "read_history":
|
|
3194
|
+
return "slock message read";
|
|
3195
|
+
case "check_messages":
|
|
3196
|
+
return "slock message check";
|
|
3197
|
+
case "claim_tasks":
|
|
3198
|
+
return "slock task claim";
|
|
3199
|
+
default:
|
|
3200
|
+
return `slock ${name.replace(/_/g, " ")}`;
|
|
3201
|
+
}
|
|
3202
|
+
}
|
|
3203
|
+
function dynamicReplyInstruction(driver) {
|
|
3204
|
+
return driver?.usesSlockCliForCommunication ? "reply using `slock message send --target <exact target>`" : `reply with ${communicationCommand(driver, "send_message")}`;
|
|
3205
|
+
}
|
|
3206
|
+
function dynamicClaimInstruction(driver) {
|
|
3207
|
+
return driver?.usesSlockCliForCommunication ? "claim the relevant task with `slock task claim`" : `claim the relevant task with ${communicationCommand(driver, "claim_tasks")}`;
|
|
3208
|
+
}
|
|
3209
|
+
function formatIncomingMessage(message, driver) {
|
|
2834
3210
|
const threadJoinPrefix = message.thread_join_context ? [
|
|
2835
3211
|
`[System: You were added to a new thread via @mention. Read this context before replying.]`,
|
|
2836
3212
|
`parent: ${message.thread_join_context.parent_target}`,
|
|
2837
3213
|
`thread: ${message.thread_join_context.thread_target}`,
|
|
2838
|
-
`suggested next step: read_history(channel="${message.thread_join_context.suggested_read_history_target}")`,
|
|
3214
|
+
`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
3215
|
"",
|
|
2840
3216
|
"Parent message:",
|
|
2841
3217
|
formatThreadContextMessage(message.thread_join_context.parent_message),
|
|
@@ -3369,9 +3745,20 @@ function classifyTerminalFailure(ap) {
|
|
|
3369
3745
|
return null;
|
|
3370
3746
|
}
|
|
3371
3747
|
function isMissingResumeSession(ap) {
|
|
3372
|
-
if (ap.driver.id !== "claude") return false;
|
|
3373
3748
|
if (!ap.sessionId) return false;
|
|
3374
|
-
|
|
3749
|
+
const candidates = [
|
|
3750
|
+
ap.lastRuntimeError,
|
|
3751
|
+
...ap.recentStderr
|
|
3752
|
+
].filter((value) => !!value);
|
|
3753
|
+
if (ap.driver.id === "claude") {
|
|
3754
|
+
return candidates.some((text) => /No conversation found with session ID/i.test(text));
|
|
3755
|
+
}
|
|
3756
|
+
if (ap.driver.id === "opencode") {
|
|
3757
|
+
return candidates.some(
|
|
3758
|
+
(text) => /Session not found/i.test(text) && text.includes(ap.sessionId)
|
|
3759
|
+
);
|
|
3760
|
+
}
|
|
3761
|
+
return false;
|
|
3375
3762
|
}
|
|
3376
3763
|
function getMessageDeliveryText(driver) {
|
|
3377
3764
|
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.";
|
|
@@ -3416,7 +3803,7 @@ var AgentProcessManager = class _AgentProcessManager {
|
|
|
3416
3803
|
this.daemonApiKey = daemonApiKey;
|
|
3417
3804
|
this.serverUrl = opts.serverUrl;
|
|
3418
3805
|
this.dataDir = opts.dataDir || DATA_DIR;
|
|
3419
|
-
this.runtimeSessionHomeDir = opts.runtimeSessionHomeDir ||
|
|
3806
|
+
this.runtimeSessionHomeDir = opts.runtimeSessionHomeDir || os4.homedir();
|
|
3420
3807
|
this.driverResolver = opts.driverResolver || getDriver;
|
|
3421
3808
|
this.defaultAgentEnvVarsProvider = opts.defaultAgentEnvVarsProvider || null;
|
|
3422
3809
|
this.tracer = opts.tracer ?? noopTracer;
|
|
@@ -3433,26 +3820,26 @@ var AgentProcessManager = class _AgentProcessManager {
|
|
|
3433
3820
|
this.agentsStarting.add(agentId);
|
|
3434
3821
|
try {
|
|
3435
3822
|
const driver = this.driverResolver(config.runtime || "claude");
|
|
3436
|
-
const agentDataDir =
|
|
3823
|
+
const agentDataDir = path11.join(this.dataDir, agentId);
|
|
3437
3824
|
await mkdir(agentDataDir, { recursive: true });
|
|
3438
3825
|
const runtimeConfig = withLocalRuntimeContext(config, agentId, agentDataDir);
|
|
3439
|
-
const memoryMdPath =
|
|
3826
|
+
const memoryMdPath = path11.join(agentDataDir, "MEMORY.md");
|
|
3440
3827
|
try {
|
|
3441
3828
|
await access(memoryMdPath);
|
|
3442
3829
|
} catch {
|
|
3443
3830
|
const initialMemoryMd = buildInitialMemoryMd(runtimeConfig);
|
|
3444
3831
|
await writeFile(memoryMdPath, initialMemoryMd);
|
|
3445
3832
|
}
|
|
3446
|
-
const notesDir =
|
|
3833
|
+
const notesDir = path11.join(agentDataDir, "notes");
|
|
3447
3834
|
await mkdir(notesDir, { recursive: true });
|
|
3448
3835
|
if (getOnboardingSeedMode(config) === FIRST_CINDY_SEED_MODE) {
|
|
3449
3836
|
const seedFiles = buildOnboardingSeedFiles();
|
|
3450
3837
|
for (const { relativePath, content } of seedFiles) {
|
|
3451
|
-
const fullPath =
|
|
3838
|
+
const fullPath = path11.join(agentDataDir, relativePath);
|
|
3452
3839
|
try {
|
|
3453
3840
|
await access(fullPath);
|
|
3454
3841
|
} catch {
|
|
3455
|
-
await mkdir(
|
|
3842
|
+
await mkdir(path11.dirname(fullPath), { recursive: true });
|
|
3456
3843
|
await writeFile(fullPath, content);
|
|
3457
3844
|
}
|
|
3458
3845
|
}
|
|
@@ -3470,7 +3857,7 @@ var AgentProcessManager = class _AgentProcessManager {
|
|
|
3470
3857
|
const channelLabel = formatChannelLabel(wakeMessage);
|
|
3471
3858
|
prompt = runtimeProfileControlPrompt ?? `New message received:
|
|
3472
3859
|
|
|
3473
|
-
${formatIncomingMessage(wakeMessage)}`;
|
|
3860
|
+
${formatIncomingMessage(wakeMessage, driver)}`;
|
|
3474
3861
|
if (!runtimeProfileControlPrompt && unreadSummary && Object.keys(unreadSummary).length > 0) {
|
|
3475
3862
|
const otherUnread = Object.entries(unreadSummary).filter(([key]) => key !== channelLabel);
|
|
3476
3863
|
if (otherUnread.length > 0) {
|
|
@@ -3483,13 +3870,13 @@ You also have unread messages in other channels:`;
|
|
|
3483
3870
|
}
|
|
3484
3871
|
prompt += `
|
|
3485
3872
|
|
|
3486
|
-
Use read_history to catch up, or respond to the message above first.`;
|
|
3873
|
+
Use ${communicationCommand(driver, "read_history")} to catch up, or respond to the message above first.`;
|
|
3487
3874
|
}
|
|
3488
3875
|
}
|
|
3489
3876
|
if (!runtimeProfileControlPrompt) {
|
|
3490
3877
|
prompt += `
|
|
3491
3878
|
|
|
3492
|
-
Respond as appropriate \u2014
|
|
3879
|
+
Respond as appropriate \u2014 ${dynamicReplyInstruction(driver)}, or take action as needed. Complete ALL your work before stopping.
|
|
3493
3880
|
|
|
3494
3881
|
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
3882
|
prompt += getBusyDeliveryNote(driver);
|
|
@@ -3502,7 +3889,7 @@ IMPORTANT: If the message requires multi-step work (e.g. research, code changes,
|
|
|
3502
3889
|
}
|
|
3503
3890
|
prompt += `
|
|
3504
3891
|
|
|
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:
|
|
3892
|
+
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
3893
|
} else if (isResume) {
|
|
3507
3894
|
prompt = `No new messages while you were away. Nothing to do \u2014 just stop. ${getMessageDeliveryText(driver)}`;
|
|
3508
3895
|
prompt += getBusyDeliveryNote(driver);
|
|
@@ -3510,6 +3897,24 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
3510
3897
|
prompt = driver.supportsNativeStandingPrompt ? NATIVE_STANDING_PROMPT_STARTUP_INPUT : standingPrompt;
|
|
3511
3898
|
}
|
|
3512
3899
|
const effectiveConfig = await this.buildSpawnConfig(agentId, runtimeConfig);
|
|
3900
|
+
const canDeferEmptyStart = driver.deferSpawnUntilMessage === true && !wakeMessage && !runtimeConfig.runtimeProfileControl && (!unreadSummary || Object.keys(unreadSummary).length === 0);
|
|
3901
|
+
if (canDeferEmptyStart) {
|
|
3902
|
+
const pendingMessages = this.startingInboxes.get(agentId) || [];
|
|
3903
|
+
this.startingInboxes.delete(agentId);
|
|
3904
|
+
this.agentsStarting.delete(agentId);
|
|
3905
|
+
this.idleAgentConfigs.set(agentId, {
|
|
3906
|
+
config: effectiveConfig,
|
|
3907
|
+
sessionId: effectiveConfig.sessionId || null,
|
|
3908
|
+
launchId: launchId || null
|
|
3909
|
+
});
|
|
3910
|
+
this.sendAgentStatus(agentId, "active", launchId || null);
|
|
3911
|
+
this.broadcastActivity(agentId, "online", "Process idle");
|
|
3912
|
+
logger.info(`[Agent ${agentId}] Deferred ${driver.id} spawn until first concrete message`);
|
|
3913
|
+
for (const message of pendingMessages) {
|
|
3914
|
+
this.deliverMessage(agentId, message);
|
|
3915
|
+
}
|
|
3916
|
+
return;
|
|
3917
|
+
}
|
|
3513
3918
|
const { process: proc } = driver.spawn({
|
|
3514
3919
|
agentId,
|
|
3515
3920
|
config: effectiveConfig,
|
|
@@ -3528,6 +3933,9 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
3528
3933
|
config: runtimeConfig,
|
|
3529
3934
|
sessionId: runtimeConfig.sessionId || null,
|
|
3530
3935
|
launchId: launchId || null,
|
|
3936
|
+
startupWakeMessage: wakeMessage,
|
|
3937
|
+
startupUnreadSummary: unreadSummary,
|
|
3938
|
+
startupResumePrompt: resumePrompt,
|
|
3531
3939
|
isIdle: false,
|
|
3532
3940
|
notificationTimer: null,
|
|
3533
3941
|
pendingNotificationCount: 0,
|
|
@@ -3545,6 +3953,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
3545
3953
|
spawnError: null,
|
|
3546
3954
|
exitCode: null,
|
|
3547
3955
|
exitSignal: null,
|
|
3956
|
+
expectedTerminationReason: null,
|
|
3548
3957
|
pendingTrajectory: null,
|
|
3549
3958
|
gatedSteering: createGatedSteeringState()
|
|
3550
3959
|
};
|
|
@@ -3614,20 +4023,60 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
3614
4023
|
}
|
|
3615
4024
|
const finalCode = ap.exitCode ?? code;
|
|
3616
4025
|
const finalSignal = ap.exitSignal ?? signal;
|
|
3617
|
-
const
|
|
3618
|
-
|
|
3619
|
-
|
|
4026
|
+
const expectedTermination = Boolean(ap.expectedTerminationReason);
|
|
4027
|
+
const processEndedCleanly = finalCode === 0 || expectedTermination && !ap.lastRuntimeError;
|
|
4028
|
+
const terminalFailureDetail = processEndedCleanly ? null : classifyTerminalFailure(ap);
|
|
4029
|
+
const missingResumeSession = isMissingResumeSession(ap);
|
|
4030
|
+
const summary = summarizeCrash(finalCode, finalSignal);
|
|
4031
|
+
this.endRuntimeTrace(ap, processEndedCleanly ? "ok" : "error", {
|
|
4032
|
+
outcome: processEndedCleanly ? "process-exit" : "process-crash",
|
|
4033
|
+
expectedTerminationReason: ap.expectedTerminationReason || void 0,
|
|
3620
4034
|
exitCode: finalCode,
|
|
3621
4035
|
exitSignal: finalSignal
|
|
3622
4036
|
});
|
|
3623
|
-
if (
|
|
4037
|
+
if (processEndedCleanly) {
|
|
3624
4038
|
this.finishCompactionIfActive(agentId, "Context compaction finished (inferred from process exit)");
|
|
3625
4039
|
} else {
|
|
3626
4040
|
this.clearCompactionWatchdog(ap);
|
|
3627
4041
|
}
|
|
3628
4042
|
this.agents.delete(agentId);
|
|
3629
|
-
if (
|
|
3630
|
-
const
|
|
4043
|
+
if (missingResumeSession) {
|
|
4044
|
+
const staleSessionId = ap.sessionId;
|
|
4045
|
+
const runtimeLabel = ap.driver.id === "opencode" ? "OpenCode" : "Claude";
|
|
4046
|
+
const restartConfig = { ...ap.config, sessionId: null };
|
|
4047
|
+
logger.warn(
|
|
4048
|
+
`[Agent ${agentId}] Stored ${runtimeLabel} session ${staleSessionId} is unavailable locally; falling back to cold start`
|
|
4049
|
+
);
|
|
4050
|
+
this.broadcastActivity(
|
|
4051
|
+
agentId,
|
|
4052
|
+
"working",
|
|
4053
|
+
`Stored ${runtimeLabel} session missing; cold-starting a new session\u2026`,
|
|
4054
|
+
[{ kind: "text", text: `Stored ${runtimeLabel} session ${staleSessionId} was not found locally. Falling back to a cold start.` }]
|
|
4055
|
+
);
|
|
4056
|
+
this.startAgent(
|
|
4057
|
+
agentId,
|
|
4058
|
+
restartConfig,
|
|
4059
|
+
ap.startupWakeMessage,
|
|
4060
|
+
ap.startupUnreadSummary,
|
|
4061
|
+
ap.startupResumePrompt,
|
|
4062
|
+
ap.launchId || void 0
|
|
4063
|
+
).catch((err) => {
|
|
4064
|
+
logger.error(`[Agent ${agentId}] Cold start recovery failed`, err);
|
|
4065
|
+
this.sendAgentStatus(agentId, "inactive", ap.launchId);
|
|
4066
|
+
this.broadcastActivity(agentId, "offline", `Crashed (${summary})`, [], ap.launchId);
|
|
4067
|
+
});
|
|
4068
|
+
return;
|
|
4069
|
+
}
|
|
4070
|
+
if (processEndedCleanly) {
|
|
4071
|
+
let queuedWakeMessage;
|
|
4072
|
+
if (!ap.driver.supportsStdinNotification) {
|
|
4073
|
+
while (ap.inbox.length > 0) {
|
|
4074
|
+
const candidate = ap.inbox.shift();
|
|
4075
|
+
if (this.shouldDeferWakeMessage(agentId, ap.driver, candidate)) continue;
|
|
4076
|
+
queuedWakeMessage = candidate;
|
|
4077
|
+
break;
|
|
4078
|
+
}
|
|
4079
|
+
}
|
|
3631
4080
|
const unreadSummary2 = queuedWakeMessage ? buildUnreadSummary(ap.inbox, formatChannelLabel(queuedWakeMessage)) : void 0;
|
|
3632
4081
|
if (queuedWakeMessage) {
|
|
3633
4082
|
logger.info(`[Agent ${agentId}] Turn completed; restarting immediately for queued message`);
|
|
@@ -3662,26 +4111,6 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
3662
4111
|
} else {
|
|
3663
4112
|
this.idleAgentConfigs.delete(agentId);
|
|
3664
4113
|
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
4114
|
logger.error(`[Agent ${agentId}] Process crashed (${reason}) \u2014 marking inactive`);
|
|
3686
4115
|
this.sendAgentStatus(agentId, "inactive", ap.launchId);
|
|
3687
4116
|
if (terminalFailureDetail) {
|
|
@@ -3804,6 +4233,10 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
3804
4233
|
}
|
|
3805
4234
|
const cached = this.idleAgentConfigs.get(agentId);
|
|
3806
4235
|
if (cached) {
|
|
4236
|
+
const driver = this.driverResolver(cached.config.runtime || "claude");
|
|
4237
|
+
if (this.shouldDeferWakeMessage(agentId, driver, message)) {
|
|
4238
|
+
return;
|
|
4239
|
+
}
|
|
3807
4240
|
logger.info(`[Agent ${agentId}] Starting from idle state for new message`);
|
|
3808
4241
|
this.idleAgentConfigs.delete(agentId);
|
|
3809
4242
|
this.startAgent(agentId, cached.config, message, void 0, void 0, cached.launchId || void 0).catch((err) => {
|
|
@@ -3812,6 +4245,9 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
3812
4245
|
}
|
|
3813
4246
|
return;
|
|
3814
4247
|
}
|
|
4248
|
+
if (this.shouldDeferWakeMessage(agentId, ap.driver, message)) {
|
|
4249
|
+
return;
|
|
4250
|
+
}
|
|
3815
4251
|
if (ap.isIdle && ap.driver.supportsStdinNotification && ap.sessionId) {
|
|
3816
4252
|
const nextMessages = ap.inbox.splice(0, ap.inbox.length);
|
|
3817
4253
|
nextMessages.push(message);
|
|
@@ -3840,7 +4276,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
3840
4276
|
}
|
|
3841
4277
|
}
|
|
3842
4278
|
async resetWorkspace(agentId) {
|
|
3843
|
-
const agentDataDir =
|
|
4279
|
+
const agentDataDir = path11.join(this.dataDir, agentId);
|
|
3844
4280
|
try {
|
|
3845
4281
|
await rm2(agentDataDir, { recursive: true, force: true });
|
|
3846
4282
|
logger.info(`[Agent ${agentId}] Workspace reset complete (${agentDataDir})`);
|
|
@@ -3856,6 +4292,11 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
3856
4292
|
getRunningAgentIds() {
|
|
3857
4293
|
return [...this.agents.keys()];
|
|
3858
4294
|
}
|
|
4295
|
+
shouldDeferWakeMessage(agentId, driver, message) {
|
|
4296
|
+
if (!driver.shouldDeferWakeMessage?.(message)) return false;
|
|
4297
|
+
logger.info(`[Agent ${agentId}] Deferred non-concrete wake message for ${driver.id}`);
|
|
4298
|
+
return true;
|
|
4299
|
+
}
|
|
3859
4300
|
getAgentSessionId(agentId) {
|
|
3860
4301
|
return this.agents.get(agentId)?.sessionId ?? null;
|
|
3861
4302
|
}
|
|
@@ -3870,7 +4311,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
3870
4311
|
return result;
|
|
3871
4312
|
}
|
|
3872
4313
|
buildRuntimeProfileReport(agentId, config, sessionId, launchId) {
|
|
3873
|
-
const workspacePath =
|
|
4314
|
+
const workspacePath = path11.join(this.dataDir, agentId);
|
|
3874
4315
|
return {
|
|
3875
4316
|
agentId,
|
|
3876
4317
|
launchId,
|
|
@@ -4019,7 +4460,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
4019
4460
|
}
|
|
4020
4461
|
// Workspace file browsing
|
|
4021
4462
|
async getFileTree(agentId, dirPath) {
|
|
4022
|
-
const agentDir =
|
|
4463
|
+
const agentDir = path11.join(this.dataDir, agentId);
|
|
4023
4464
|
try {
|
|
4024
4465
|
await stat2(agentDir);
|
|
4025
4466
|
} catch {
|
|
@@ -4027,8 +4468,8 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
4027
4468
|
}
|
|
4028
4469
|
let targetDir = agentDir;
|
|
4029
4470
|
if (dirPath) {
|
|
4030
|
-
const resolved =
|
|
4031
|
-
if (!resolved.startsWith(agentDir +
|
|
4471
|
+
const resolved = path11.resolve(agentDir, dirPath);
|
|
4472
|
+
if (!resolved.startsWith(agentDir + path11.sep) && resolved !== agentDir) {
|
|
4032
4473
|
return [];
|
|
4033
4474
|
}
|
|
4034
4475
|
targetDir = resolved;
|
|
@@ -4036,9 +4477,9 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
4036
4477
|
return this.listDirectoryChildren(targetDir, agentDir);
|
|
4037
4478
|
}
|
|
4038
4479
|
async readFile(agentId, filePath) {
|
|
4039
|
-
const agentDir =
|
|
4040
|
-
const resolved =
|
|
4041
|
-
if (!resolved.startsWith(agentDir +
|
|
4480
|
+
const agentDir = path11.join(this.dataDir, agentId);
|
|
4481
|
+
const resolved = path11.resolve(agentDir, filePath);
|
|
4482
|
+
if (!resolved.startsWith(agentDir + path11.sep) && resolved !== agentDir) {
|
|
4042
4483
|
throw new Error("Access denied");
|
|
4043
4484
|
}
|
|
4044
4485
|
const info = await stat2(resolved);
|
|
@@ -4062,7 +4503,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
4062
4503
|
".sh",
|
|
4063
4504
|
".py"
|
|
4064
4505
|
]);
|
|
4065
|
-
const ext =
|
|
4506
|
+
const ext = path11.extname(resolved).toLowerCase();
|
|
4066
4507
|
if (!TEXT_EXTENSIONS.has(ext) && ext !== "") {
|
|
4067
4508
|
return { content: null, binary: true };
|
|
4068
4509
|
}
|
|
@@ -4088,14 +4529,14 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
4088
4529
|
async listSkills(agentId, runtimeHint) {
|
|
4089
4530
|
const agent = this.agents.get(agentId);
|
|
4090
4531
|
const runtime = runtimeHint || agent?.config.runtime || "claude";
|
|
4091
|
-
const home =
|
|
4092
|
-
const workspaceDir =
|
|
4532
|
+
const home = os4.homedir();
|
|
4533
|
+
const workspaceDir = path11.join(this.dataDir, agentId);
|
|
4093
4534
|
const paths = _AgentProcessManager.SKILL_PATHS[runtime] || _AgentProcessManager.SKILL_PATHS.claude;
|
|
4094
4535
|
const globalResults = await Promise.all(
|
|
4095
|
-
paths.global.map((p) => this.scanSkillsDir(
|
|
4536
|
+
paths.global.map((p) => this.scanSkillsDir(path11.join(home, p)))
|
|
4096
4537
|
);
|
|
4097
4538
|
const workspaceResults = await Promise.all(
|
|
4098
|
-
paths.workspace.map((p) => this.scanSkillsDir(
|
|
4539
|
+
paths.workspace.map((p) => this.scanSkillsDir(path11.join(workspaceDir, p)))
|
|
4099
4540
|
);
|
|
4100
4541
|
const dedup = (skills) => {
|
|
4101
4542
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -4124,7 +4565,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
4124
4565
|
const skills = [];
|
|
4125
4566
|
for (const entry of entries) {
|
|
4126
4567
|
if (entry.isDirectory() || entry.isSymbolicLink()) {
|
|
4127
|
-
const skillMd =
|
|
4568
|
+
const skillMd = path11.join(dir, entry.name, "SKILL.md");
|
|
4128
4569
|
try {
|
|
4129
4570
|
const content = await readFile(skillMd, "utf-8");
|
|
4130
4571
|
const skill = this.parseSkillMd(entry.name, content);
|
|
@@ -4135,7 +4576,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
4135
4576
|
} else if (entry.name.endsWith(".md")) {
|
|
4136
4577
|
const cmdName = entry.name.replace(/\.md$/, "");
|
|
4137
4578
|
try {
|
|
4138
|
-
const content = await readFile(
|
|
4579
|
+
const content = await readFile(path11.join(dir, entry.name), "utf-8");
|
|
4139
4580
|
const skill = this.parseSkillMd(cmdName, content);
|
|
4140
4581
|
skill.sourcePath = dir;
|
|
4141
4582
|
skills.push(skill);
|
|
@@ -4350,7 +4791,9 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
4350
4791
|
logger.info(
|
|
4351
4792
|
`[Agent ${agentId}] Claude gated steering flush reason=${reason} messages=${nextMessages.length}`
|
|
4352
4793
|
);
|
|
4353
|
-
|
|
4794
|
+
if (reason === "turn_end") {
|
|
4795
|
+
this.broadcastActivity(agentId, "working", "Message received");
|
|
4796
|
+
}
|
|
4354
4797
|
if (this.deliverMessagesViaStdin(agentId, ap, nextMessages, reason === "turn_end" ? "idle" : "busy")) {
|
|
4355
4798
|
return true;
|
|
4356
4799
|
}
|
|
@@ -4527,6 +4970,16 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
4527
4970
|
this.broadcastActivity(agentId, "online", "Idle");
|
|
4528
4971
|
}
|
|
4529
4972
|
this.endRuntimeTrace(ap, "ok", { outcome: "turn-completed" });
|
|
4973
|
+
if (ap.driver.terminateProcessOnTurnEnd) {
|
|
4974
|
+
ap.expectedTerminationReason = "turn_end";
|
|
4975
|
+
logger.info(`[Agent ${agentId}] Turn completed; terminating ${ap.driver.id} process`);
|
|
4976
|
+
try {
|
|
4977
|
+
ap.process.kill("SIGTERM");
|
|
4978
|
+
} catch (err) {
|
|
4979
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
4980
|
+
logger.warn(`[Agent ${agentId}] Failed to terminate ${ap.driver.id} after turn_end: ${reason}`);
|
|
4981
|
+
}
|
|
4982
|
+
}
|
|
4530
4983
|
}
|
|
4531
4984
|
if (event.sessionId) {
|
|
4532
4985
|
this.sendToServer({ type: "agent:session", agentId, sessionId: event.sessionId, launchId: ap?.launchId || void 0 });
|
|
@@ -4588,7 +5041,6 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
4588
5041
|
if (ap.driver.busyDeliveryMode === "direct" && ap.inbox.length > 0) {
|
|
4589
5042
|
const queuedMessages = ap.inbox.splice(0, ap.inbox.length);
|
|
4590
5043
|
console.log(`[Agent ${agentId}] Delivering queued message via stdin while busy`);
|
|
4591
|
-
this.broadcastActivity(agentId, "working", "Message received");
|
|
4592
5044
|
if (this.deliverMessagesViaStdin(agentId, ap, queuedMessages, "busy")) {
|
|
4593
5045
|
return;
|
|
4594
5046
|
}
|
|
@@ -4607,11 +5059,11 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
4607
5059
|
if (messages.length === 0) return true;
|
|
4608
5060
|
const prompt = formatRuntimeProfileControlPrompt(messages) ?? (messages.length === 1 ? `New message received:
|
|
4609
5061
|
|
|
4610
|
-
${formatIncomingMessage(messages[0])}
|
|
5062
|
+
${formatIncomingMessage(messages[0], ap.driver)}
|
|
4611
5063
|
|
|
4612
5064
|
Respond as appropriate. Complete all your work before stopping.` : `New messages received:
|
|
4613
5065
|
|
|
4614
|
-
${messages.map((message) => formatIncomingMessage(message)).join("\n")}
|
|
5066
|
+
${messages.map((message) => formatIncomingMessage(message, ap.driver)).join("\n")}
|
|
4615
5067
|
|
|
4616
5068
|
Respond as appropriate. Complete all your work before stopping.`);
|
|
4617
5069
|
const encoded = ap.driver.encodeStdinMessage(prompt, ap.sessionId, { mode });
|
|
@@ -4649,8 +5101,8 @@ Respond as appropriate. Complete all your work before stopping.`);
|
|
|
4649
5101
|
const nodes = [];
|
|
4650
5102
|
for (const entry of entries) {
|
|
4651
5103
|
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
4652
|
-
const fullPath =
|
|
4653
|
-
const relativePath =
|
|
5104
|
+
const fullPath = path11.join(dir, entry.name);
|
|
5105
|
+
const relativePath = path11.relative(rootDir, fullPath);
|
|
4654
5106
|
let info;
|
|
4655
5107
|
try {
|
|
4656
5108
|
info = await stat2(fullPath);
|
|
@@ -4881,10 +5333,10 @@ var ReminderCache = class {
|
|
|
4881
5333
|
|
|
4882
5334
|
// src/machineLock.ts
|
|
4883
5335
|
import { createHash, randomUUID as randomUUID2 } from "crypto";
|
|
4884
|
-
import { mkdirSync as mkdirSync5, readFileSync as
|
|
4885
|
-
import
|
|
4886
|
-
import
|
|
4887
|
-
var DEFAULT_MACHINE_STATE_ROOT =
|
|
5336
|
+
import { mkdirSync as mkdirSync5, readFileSync as readFileSync4, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync8 } from "fs";
|
|
5337
|
+
import os5 from "os";
|
|
5338
|
+
import path12 from "path";
|
|
5339
|
+
var DEFAULT_MACHINE_STATE_ROOT = path12.join(os5.homedir(), ".slock", "machines");
|
|
4888
5340
|
var INCOMPLETE_LOCK_STALE_MS = 3e4;
|
|
4889
5341
|
var DaemonMachineLockConflictError = class extends Error {
|
|
4890
5342
|
code = "DAEMON_MACHINE_LOCK_HELD";
|
|
@@ -4903,11 +5355,11 @@ function getDaemonMachineLockId(apiKey) {
|
|
|
4903
5355
|
return `machine-${apiKeyFingerprint(apiKey).slice(0, 16)}`;
|
|
4904
5356
|
}
|
|
4905
5357
|
function ownerPath(lockDir) {
|
|
4906
|
-
return
|
|
5358
|
+
return path12.join(lockDir, "owner.json");
|
|
4907
5359
|
}
|
|
4908
5360
|
function readOwner(lockDir) {
|
|
4909
5361
|
try {
|
|
4910
|
-
return JSON.parse(
|
|
5362
|
+
return JSON.parse(readFileSync4(ownerPath(lockDir), "utf8"));
|
|
4911
5363
|
} catch {
|
|
4912
5364
|
return null;
|
|
4913
5365
|
}
|
|
@@ -4933,8 +5385,8 @@ function acquireDaemonMachineLock(options) {
|
|
|
4933
5385
|
const rootDir = options.rootDir ?? DEFAULT_MACHINE_STATE_ROOT;
|
|
4934
5386
|
const fingerprint = apiKeyFingerprint(options.apiKey);
|
|
4935
5387
|
const lockId = getDaemonMachineLockId(options.apiKey);
|
|
4936
|
-
const machineDir =
|
|
4937
|
-
const lockDir =
|
|
5388
|
+
const machineDir = path12.join(rootDir, lockId);
|
|
5389
|
+
const lockDir = path12.join(machineDir, "daemon.lock");
|
|
4938
5390
|
const token = randomUUID2();
|
|
4939
5391
|
mkdirSync5(machineDir, { recursive: true });
|
|
4940
5392
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
@@ -4943,7 +5395,7 @@ function acquireDaemonMachineLock(options) {
|
|
|
4943
5395
|
const owner = {
|
|
4944
5396
|
pid: process.pid,
|
|
4945
5397
|
token,
|
|
4946
|
-
hostname:
|
|
5398
|
+
hostname: os5.hostname(),
|
|
4947
5399
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4948
5400
|
serverUrl: options.serverUrl,
|
|
4949
5401
|
apiKeyFingerprint: fingerprint.slice(0, 16)
|
|
@@ -5006,23 +5458,23 @@ function readDaemonVersion(moduleUrl = import.meta.url) {
|
|
|
5006
5458
|
}
|
|
5007
5459
|
}
|
|
5008
5460
|
function resolveChatBridgePath(moduleUrl = import.meta.url) {
|
|
5009
|
-
const dirname =
|
|
5010
|
-
const jsPath =
|
|
5461
|
+
const dirname = path13.dirname(fileURLToPath(moduleUrl));
|
|
5462
|
+
const jsPath = path13.resolve(dirname, "chat-bridge.js");
|
|
5011
5463
|
try {
|
|
5012
5464
|
accessSync(jsPath);
|
|
5013
5465
|
return jsPath;
|
|
5014
5466
|
} catch {
|
|
5015
|
-
return
|
|
5467
|
+
return path13.resolve(dirname, "chat-bridge.ts");
|
|
5016
5468
|
}
|
|
5017
5469
|
}
|
|
5018
5470
|
function resolveSlockCliPath(moduleUrl = import.meta.url) {
|
|
5019
|
-
const thisDir =
|
|
5020
|
-
const bundledDistPath =
|
|
5471
|
+
const thisDir = path13.dirname(fileURLToPath(moduleUrl));
|
|
5472
|
+
const bundledDistPath = path13.resolve(thisDir, "cli", "index.js");
|
|
5021
5473
|
try {
|
|
5022
5474
|
accessSync(bundledDistPath);
|
|
5023
5475
|
return bundledDistPath;
|
|
5024
5476
|
} catch {
|
|
5025
|
-
const workspaceDistPath =
|
|
5477
|
+
const workspaceDistPath = path13.resolve(thisDir, "..", "..", "cli", "dist", "index.js");
|
|
5026
5478
|
accessSync(workspaceDistPath);
|
|
5027
5479
|
return workspaceDistPath;
|
|
5028
5480
|
}
|
|
@@ -5031,9 +5483,14 @@ function detectRuntimes() {
|
|
|
5031
5483
|
const ids = [];
|
|
5032
5484
|
const versions = {};
|
|
5033
5485
|
for (const runtime of RUNTIMES) {
|
|
5486
|
+
const driver = getDriver(runtime.id);
|
|
5034
5487
|
try {
|
|
5035
|
-
|
|
5036
|
-
|
|
5488
|
+
if (driver.probe) {
|
|
5489
|
+
const probe = driver.probe();
|
|
5490
|
+
if (!probe.available) {
|
|
5491
|
+
if (probe.version) versions[runtime.id] = probe.version;
|
|
5492
|
+
continue;
|
|
5493
|
+
}
|
|
5037
5494
|
ids.push(runtime.id);
|
|
5038
5495
|
if (probe.version) versions[runtime.id] = probe.version;
|
|
5039
5496
|
continue;
|
|
@@ -5134,7 +5591,7 @@ var DaemonCore = class {
|
|
|
5134
5591
|
}
|
|
5135
5592
|
resolveMachineStateRoot() {
|
|
5136
5593
|
if (this.options.machineStateDir) return this.options.machineStateDir;
|
|
5137
|
-
if (this.options.dataDir) return
|
|
5594
|
+
if (this.options.dataDir) return path13.join(path13.dirname(this.options.dataDir), "machines");
|
|
5138
5595
|
return DEFAULT_MACHINE_STATE_ROOT;
|
|
5139
5596
|
}
|
|
5140
5597
|
start() {
|
|
@@ -5325,8 +5782,8 @@ var DaemonCore = class {
|
|
|
5325
5782
|
capabilities: ["agent:start", "agent:stop", "agent:deliver", "workspace:files"],
|
|
5326
5783
|
runtimes,
|
|
5327
5784
|
runningAgents: this.agentManager.getRunningAgentIds(),
|
|
5328
|
-
hostname: this.options.hostname ??
|
|
5329
|
-
os: this.options.osDescription ?? `${
|
|
5785
|
+
hostname: this.options.hostname ?? os6.hostname(),
|
|
5786
|
+
os: this.options.osDescription ?? `${os6.platform()} ${os6.arch()}`,
|
|
5330
5787
|
daemonVersion: this.daemonVersion
|
|
5331
5788
|
});
|
|
5332
5789
|
for (const agentId of this.agentManager.getRunningAgentIds()) {
|
package/dist/cli/index.js
CHANGED
|
@@ -1397,7 +1397,8 @@ var RUNTIMES = [
|
|
|
1397
1397
|
{ id: "kimi", displayName: "Kimi CLI", binary: "kimi", supported: true },
|
|
1398
1398
|
{ id: "copilot", displayName: "Copilot CLI", binary: "copilot", supported: true },
|
|
1399
1399
|
{ id: "cursor", displayName: "Cursor CLI", binary: "cursor-agent", supported: true },
|
|
1400
|
-
{ id: "gemini", displayName: "Gemini CLI", binary: "gemini", supported: true }
|
|
1400
|
+
{ id: "gemini", displayName: "Gemini CLI", binary: "gemini", supported: true },
|
|
1401
|
+
{ id: "opencode", displayName: "OpenCode", binary: "opencode", supported: true }
|
|
1401
1402
|
];
|
|
1402
1403
|
function getRuntimeDisplayName(id) {
|
|
1403
1404
|
return RUNTIMES.find((r) => r.id === id)?.displayName ?? id;
|
|
@@ -1552,6 +1553,8 @@ var FILENAME_MIME_MAP2 = {
|
|
|
1552
1553
|
".gif": "image/gif",
|
|
1553
1554
|
".webp": "image/webp"
|
|
1554
1555
|
};
|
|
1556
|
+
var MAX_PROFILE_DESCRIPTION_LENGTH = 3e3;
|
|
1557
|
+
var MAX_PROFILE_DISPLAY_NAME_LENGTH = 80;
|
|
1555
1558
|
function inferImageMimeType(filename, buffer) {
|
|
1556
1559
|
const lowerFilename = filename.toLowerCase();
|
|
1557
1560
|
if (buffer.length >= 8 && buffer.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))) {
|
|
@@ -1596,7 +1599,7 @@ function readAvatarFile(avatarFile) {
|
|
|
1596
1599
|
return { filename, buffer, mimeType };
|
|
1597
1600
|
}
|
|
1598
1601
|
function registerProfileUpdateCommand(parent) {
|
|
1599
|
-
parent.command("update").description("Update your own profile").
|
|
1602
|
+
parent.command("update").description("Update your own profile").option("--avatar-file <path>", "Path to a local image file to use as your avatar").option("--display-name <name>", "Set your display name (non-empty)").option("--description <text>", "Set your profile description (non-empty)").option("--json", "Emit machine-readable JSON").action(async (opts) => {
|
|
1600
1603
|
let ctx;
|
|
1601
1604
|
try {
|
|
1602
1605
|
ctx = loadAgentContext();
|
|
@@ -1604,28 +1607,75 @@ function registerProfileUpdateCommand(parent) {
|
|
|
1604
1607
|
if (err instanceof AgentBootstrapError) fail(err.code, err.message);
|
|
1605
1608
|
throw err;
|
|
1606
1609
|
}
|
|
1607
|
-
|
|
1608
|
-
|
|
1610
|
+
const hasAvatar = opts.avatarFile !== void 0;
|
|
1611
|
+
const hasDisplayName = opts.displayName !== void 0;
|
|
1612
|
+
const hasDescription = opts.description !== void 0;
|
|
1613
|
+
if (!hasAvatar && !hasDisplayName && !hasDescription) {
|
|
1614
|
+
fail("INVALID_ARG", "Provide at least one of --avatar-file, --display-name, or --description");
|
|
1615
|
+
}
|
|
1616
|
+
let trimmedDisplayName;
|
|
1617
|
+
if (hasDisplayName) {
|
|
1618
|
+
trimmedDisplayName = opts.displayName.trim();
|
|
1619
|
+
if (trimmedDisplayName.length === 0) {
|
|
1620
|
+
fail("INVALID_ARG", "--display-name must not be empty");
|
|
1621
|
+
}
|
|
1622
|
+
if (trimmedDisplayName.length > MAX_PROFILE_DISPLAY_NAME_LENGTH) {
|
|
1623
|
+
fail("INVALID_ARG", `--display-name must be at most ${MAX_PROFILE_DISPLAY_NAME_LENGTH} characters`);
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
if (hasDescription) {
|
|
1627
|
+
if (opts.description.length === 0) {
|
|
1628
|
+
fail("INVALID_ARG", "--description must not be empty");
|
|
1629
|
+
}
|
|
1630
|
+
if (opts.description.length > MAX_PROFILE_DESCRIPTION_LENGTH) {
|
|
1631
|
+
fail("INVALID_ARG", `--description must be at most ${MAX_PROFILE_DESCRIPTION_LENGTH} characters`);
|
|
1632
|
+
}
|
|
1609
1633
|
}
|
|
1610
|
-
const avatar = readAvatarFile(opts.avatarFile);
|
|
1611
|
-
const form = new FormData();
|
|
1612
|
-
const avatarBytes = Uint8Array.from(avatar.buffer);
|
|
1613
|
-
form.append("avatar", new Blob([avatarBytes], { type: avatar.mimeType }), avatar.filename);
|
|
1614
1634
|
const client = new ApiClient(ctx);
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1635
|
+
let latestProfile = null;
|
|
1636
|
+
if (hasDisplayName || hasDescription) {
|
|
1637
|
+
const body = {};
|
|
1638
|
+
if (hasDisplayName) {
|
|
1639
|
+
body.displayName = trimmedDisplayName;
|
|
1640
|
+
}
|
|
1641
|
+
if (hasDescription) {
|
|
1642
|
+
body.description = opts.description;
|
|
1643
|
+
}
|
|
1644
|
+
const res = await client.request(
|
|
1645
|
+
"POST",
|
|
1646
|
+
`/internal/agent/${encodeURIComponent(ctx.agentId)}/profile`,
|
|
1647
|
+
body
|
|
1648
|
+
);
|
|
1649
|
+
if (!res.ok || !res.data) {
|
|
1650
|
+
const code = res.errorCode ?? (res.status >= 500 ? "SERVER_5XX" : "PROFILE_UPDATE_FAILED");
|
|
1651
|
+
fail(code, res.error ?? `HTTP ${res.status}`);
|
|
1652
|
+
}
|
|
1653
|
+
latestProfile = res.data;
|
|
1654
|
+
}
|
|
1655
|
+
if (hasAvatar) {
|
|
1656
|
+
const avatar = readAvatarFile(opts.avatarFile);
|
|
1657
|
+
const form = new FormData();
|
|
1658
|
+
const avatarBytes = Uint8Array.from(avatar.buffer);
|
|
1659
|
+
form.append("avatar", new Blob([avatarBytes], { type: avatar.mimeType }), avatar.filename);
|
|
1660
|
+
const res = await client.requestMultipart(
|
|
1661
|
+
"POST",
|
|
1662
|
+
`/internal/agent/${encodeURIComponent(ctx.agentId)}/profile/avatar`,
|
|
1663
|
+
form
|
|
1664
|
+
);
|
|
1665
|
+
if (!res.ok || !res.data) {
|
|
1666
|
+
const code = res.errorCode ?? (res.status >= 500 ? "SERVER_5XX" : "PROFILE_UPDATE_FAILED");
|
|
1667
|
+
fail(code, res.error ?? `HTTP ${res.status}`);
|
|
1668
|
+
}
|
|
1669
|
+
latestProfile = res.data;
|
|
1670
|
+
}
|
|
1671
|
+
if (!latestProfile) {
|
|
1672
|
+
fail("PROFILE_UPDATE_FAILED", "No profile returned from server");
|
|
1623
1673
|
}
|
|
1624
1674
|
if (opts.json) {
|
|
1625
|
-
emit({ ok: true, data:
|
|
1675
|
+
emit({ ok: true, data: latestProfile });
|
|
1626
1676
|
return;
|
|
1627
1677
|
}
|
|
1628
|
-
process.stdout.write(`${formatProfile(
|
|
1678
|
+
process.stdout.write(`${formatProfile(latestProfile)}
|
|
1629
1679
|
`);
|
|
1630
1680
|
});
|
|
1631
1681
|
}
|
package/dist/core.js
CHANGED
package/dist/index.js
CHANGED