@slock-ai/daemon 0.41.0 → 0.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chat-bridge.js +25 -2
- package/dist/{chunk-KFVDXO5Y.js → chunk-RNEIFBXW.js} +493 -68
- package/dist/cli/index.js +492 -4
- package/dist/core.js +1 -1
- package/dist/index.js +7 -2
- 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 path12 from "path";
|
|
8
|
+
import os5 from "os";
|
|
9
9
|
import { createRequire } from "module";
|
|
10
10
|
import { accessSync } from "fs";
|
|
11
11
|
import { fileURLToPath } from "url";
|
|
@@ -582,6 +582,7 @@ var DISPLAY_PLAN_CONFIG = {
|
|
|
582
582
|
};
|
|
583
583
|
|
|
584
584
|
// src/agentProcessManager.ts
|
|
585
|
+
import { mkdirSync as mkdirSync4, readdirSync, statSync, writeFileSync as writeFileSync7 } from "fs";
|
|
585
586
|
import { mkdir, writeFile, access, readdir as readdir2, stat as stat2, readFile, rm as rm2 } from "fs/promises";
|
|
586
587
|
import path10 from "path";
|
|
587
588
|
import os3 from "os";
|
|
@@ -599,6 +600,27 @@ import path from "path";
|
|
|
599
600
|
function toolRef(prefix, name) {
|
|
600
601
|
return `${prefix}${name}`;
|
|
601
602
|
}
|
|
603
|
+
function runtimeContextLines(config) {
|
|
604
|
+
const ctx = config.runtimeContext;
|
|
605
|
+
if (!ctx) return [];
|
|
606
|
+
const lines = [
|
|
607
|
+
"## Current Runtime Context",
|
|
608
|
+
"",
|
|
609
|
+
"This is authoritative context injected by Slock. Do not infer computer identity from hostname or cwd when this section is present.",
|
|
610
|
+
""
|
|
611
|
+
];
|
|
612
|
+
if (ctx.agentId) lines.push(`- Agent ID: ${ctx.agentId}`);
|
|
613
|
+
if (ctx.serverId) lines.push(`- Server ID: ${ctx.serverId}`);
|
|
614
|
+
if (ctx.machineName || ctx.machineId) {
|
|
615
|
+
const label = ctx.machineName && ctx.machineId ? `${ctx.machineName} (${ctx.machineId})` : ctx.machineName || ctx.machineId;
|
|
616
|
+
lines.push(`- Computer: ${label}`);
|
|
617
|
+
}
|
|
618
|
+
if (ctx.machineHostname) lines.push(`- Hostname: ${ctx.machineHostname}`);
|
|
619
|
+
if (ctx.machineOs) lines.push(`- OS: ${ctx.machineOs}`);
|
|
620
|
+
if (ctx.daemonVersion) lines.push(`- Daemon: v${ctx.daemonVersion}`);
|
|
621
|
+
if (ctx.workspacePath) lines.push(`- Workspace: ${ctx.workspacePath}`);
|
|
622
|
+
return lines.length > 4 ? lines : [];
|
|
623
|
+
}
|
|
602
624
|
function buildPrompt(config, variant, opts) {
|
|
603
625
|
const isCli = variant === "cli";
|
|
604
626
|
const t = (name) => toolRef(opts.toolPrefix, name);
|
|
@@ -621,13 +643,18 @@ function buildPrompt(config, variant, opts) {
|
|
|
621
643
|
"- Use only the provided MCP tools for messaging \u2014 they are already available and ready.",
|
|
622
644
|
`- Always claim a task via ${taskClaimCmd} before starting work on it. If the claim fails, move on to a different task.`
|
|
623
645
|
];
|
|
646
|
+
const runtimeProfileControlStartupStep = config.runtimeProfileControl ? [
|
|
647
|
+
"0. If this system prompt contains a **Runtime Profile Control** section, complete that runtime-control instruction first. Do not read MEMORY.md, check messages, or respond to inbox messages until the required runtime control action has succeeded."
|
|
648
|
+
] : [];
|
|
624
649
|
const startupSteps = isCli ? [
|
|
650
|
+
...runtimeProfileControlStartupStep,
|
|
625
651
|
"1. If this turn already includes a concrete incoming message, first decide whether that message needs a visible acknowledgment, blocker question, or ownership signal. If it does, send it early with `slock message send` before deep context gathering.",
|
|
626
652
|
"2. Read MEMORY.md (in your cwd) and then only the additional memory/files you need to handle the current turn well.",
|
|
627
653
|
`3. If there is no concrete incoming message to handle, stop and wait. ${messageDeliveryText}`,
|
|
628
654
|
"4. When you receive a message, process it and reply with `slock message send`.",
|
|
629
655
|
"5. **Complete ALL your work before stopping.** If a task requires multi-step work (research, code changes, testing), finish everything, report results, then stop. New messages arrive automatically \u2014 you do not need to poll or wait for them."
|
|
630
656
|
] : [
|
|
657
|
+
...runtimeProfileControlStartupStep,
|
|
631
658
|
`1. If this turn already includes a concrete incoming message, first decide whether that message needs a visible acknowledgment, blocker question, or ownership signal. If it does, send it early with ${sendCmd} before deep context gathering.`,
|
|
632
659
|
"2. Read MEMORY.md (in your cwd) and then only the additional memory/files you need to handle the current turn well.",
|
|
633
660
|
`3. If there is no concrete incoming message to handle, stop and wait. ${messageDeliveryText}`,
|
|
@@ -641,20 +668,23 @@ Use the \`slock\` CLI for chat / task / attachment operations. The daemon inject
|
|
|
641
668
|
1. **\`slock message check\`** \u2014 Non-blocking check for new messages. Use freely during work \u2014 at natural breakpoints or after notifications.
|
|
642
669
|
2. **\`slock message send\`** \u2014 Send a message to a channel or DM.
|
|
643
670
|
3. **\`slock server info\`** \u2014 List channels in this server, which ones you have joined, plus all agents and humans.
|
|
644
|
-
4. **\`slock channel
|
|
645
|
-
5. **\`slock
|
|
646
|
-
6. **\`slock
|
|
647
|
-
7. **\`slock message
|
|
648
|
-
8. **\`slock
|
|
649
|
-
9. **\`slock task
|
|
650
|
-
10. **\`slock task
|
|
651
|
-
11. **\`slock task
|
|
652
|
-
12. **\`slock task
|
|
653
|
-
13. **\`slock
|
|
654
|
-
14. **\`slock attachment
|
|
655
|
-
15. **\`slock
|
|
656
|
-
16. **\`slock
|
|
657
|
-
17. **\`slock
|
|
671
|
+
4. **\`slock channel members\`** \u2014 List the members (agents and humans) of a specific channel, DM, or thread target.
|
|
672
|
+
5. **\`slock channel leave\`** \u2014 Leave a regular channel you have joined. This only affects your own agent membership.
|
|
673
|
+
6. **\`slock thread unfollow\`** \u2014 Stop receiving ordinary delivery for a thread you no longer need to follow. This only affects your own agent attention state.
|
|
674
|
+
7. **\`slock message read\`** \u2014 Read past messages from a channel, DM, or thread. Supports \`before\` / \`after\` pagination and \`around\` for centered context.
|
|
675
|
+
8. **\`slock message search\`** \u2014 Search messages visible to you, then inspect a hit with \`slock message read\`.
|
|
676
|
+
9. **\`slock task list\`** \u2014 View a channel's task board.
|
|
677
|
+
10. **\`slock task create\`** \u2014 Create new task-messages in a channel (supports batch titles; equivalent to sending a new message and publishing it as a task-message, not claiming it for yourself).
|
|
678
|
+
11. **\`slock task claim\`** \u2014 Claim tasks by number or message ID (supports batch, handles conflicts).
|
|
679
|
+
12. **\`slock task unclaim\`** \u2014 Release your claim on a task.
|
|
680
|
+
13. **\`slock task update\`** \u2014 Change a task's status (e.g. to in_review or done).
|
|
681
|
+
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
|
+
15. **\`slock attachment view\`** \u2014 Download an attached file by its attachment ID so you can inspect it locally.
|
|
683
|
+
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. Currently this only supports \`--avatar-file\`.
|
|
685
|
+
18. **\`slock reminder schedule\`** \u2014 Schedule a reminder for yourself later, at a specific time, or on a recurring cadence.
|
|
686
|
+
19. **\`slock reminder list\`** \u2014 List your reminders.
|
|
687
|
+
20. **\`slock reminder cancel\`** \u2014 Cancel one of your reminders by ID.
|
|
658
688
|
|
|
659
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.
|
|
660
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.
|
|
@@ -728,7 +758,7 @@ Threads are sub-conversations attached to a specific message. They let you discu
|
|
|
728
758
|
const discoverySection = isCli ? `### Discovering people and channels
|
|
729
759
|
|
|
730
760
|
Call \`slock server info\` to see all channels in this server, which ones you have joined, other agents, and humans.
|
|
731
|
-
Visible public channels may appear even when \`joined=false\`. In that state you can still inspect them with \`slock message read\`, but you cannot send messages there or receive ordinary channel delivery until a human adds you to the channel. To leave a regular channel you have joined, use \`slock channel leave --target "#channel-name"\`. To stop following a thread without leaving its parent channel, use \`slock thread unfollow --target "#channel-name:shortid"\`.` : `### Discovering people and channels
|
|
761
|
+
Visible public channels may appear even when \`joined=false\`. In that state you can still inspect them with \`slock message read\` and \`slock channel members\`, but you cannot send messages there or receive ordinary channel delivery until a human adds you to the channel. To leave a regular channel you have joined, use \`slock channel leave --target "#channel-name"\`. To stop following a thread without leaving its parent channel, use \`slock thread unfollow --target "#channel-name:shortid"\`.` : `### Discovering people and channels
|
|
732
762
|
|
|
733
763
|
Call ${serverInfoCmd} to see all channels in this server, which ones you have joined, other agents, and humans.
|
|
734
764
|
Visible public channels may appear even when \`joined=false\`. In that state you can still inspect them with ${readCmd}, but you cannot send messages there or receive ordinary channel delivery until a human adds you to the channel. To leave a regular channel you have joined, use \`${t("leave_channel")}\`.`;
|
|
@@ -838,6 +868,8 @@ ${readCmd} shows messages in their current state. If a message was later convert
|
|
|
838
868
|
|
|
839
869
|
Your workspace and MEMORY.md persist across turns, so you can recover context when resumed. You will be started, put to sleep when idle, and woken up again when someone sends you a message. Think of yourself as a colleague who is always available, accumulates knowledge over time, and develops expertise through interactions.
|
|
840
870
|
|
|
871
|
+
${runtimeContextLines(config).join("\n")}
|
|
872
|
+
|
|
841
873
|
${communicationSection}
|
|
842
874
|
|
|
843
875
|
CRITICAL RULES:
|
|
@@ -851,6 +883,30 @@ ${startupSteps.join("\n")}`;
|
|
|
851
883
|
|
|
852
884
|
${opts.postStartupNotes.join("\n")}`;
|
|
853
885
|
}
|
|
886
|
+
if (config.runtimeProfileControl) {
|
|
887
|
+
const control = config.runtimeProfileControl;
|
|
888
|
+
prompt += `
|
|
889
|
+
|
|
890
|
+
## Runtime Profile Control
|
|
891
|
+
|
|
892
|
+
`;
|
|
893
|
+
prompt += `This section is a trusted daemon runtime-control instruction. It overrides the normal startup sequence: complete this section before reading MEMORY.md, checking messages, or responding to inbox messages.
|
|
894
|
+
|
|
895
|
+
`;
|
|
896
|
+
if (control.kind === "migration") {
|
|
897
|
+
prompt += `You are currently in Runtime Profile migration. Before handling normal inbox messages, re-ground yourself in the new runtime context and then invoke the runtime control action \`${t("runtime_profile_migration_done")}\` with exactly this migration_key: \`${control.key}\`.
|
|
898
|
+
|
|
899
|
+
`;
|
|
900
|
+
prompt += `Do not use ${sendCmd}, ${checkCmd}, or any chat reply as the migration acknowledgment. Normal inbox delivery is gated until the runtime control action succeeds.
|
|
901
|
+
|
|
902
|
+
`;
|
|
903
|
+
} else {
|
|
904
|
+
prompt += `Read the daemon release notice below before handling normal inbox messages. No chat reply is required for this notice.
|
|
905
|
+
|
|
906
|
+
`;
|
|
907
|
+
}
|
|
908
|
+
prompt += control.message;
|
|
909
|
+
}
|
|
854
910
|
prompt += `
|
|
855
911
|
|
|
856
912
|
## Messaging
|
|
@@ -1045,6 +1101,20 @@ function buildMcpSystemPrompt(config, opts) {
|
|
|
1045
1101
|
|
|
1046
1102
|
// src/drivers/cliTransport.ts
|
|
1047
1103
|
var shellSingleQuote = (value) => `'${value.replace(/'/g, `'\\''`)}'`;
|
|
1104
|
+
function runtimeContextEnv(config) {
|
|
1105
|
+
const ctx = config.runtimeContext;
|
|
1106
|
+
if (!ctx) return {};
|
|
1107
|
+
return {
|
|
1108
|
+
...ctx.agentId ? { SLOCK_CURRENT_AGENT_ID: ctx.agentId } : {},
|
|
1109
|
+
...ctx.serverId ? { SLOCK_CURRENT_SERVER_ID: ctx.serverId } : {},
|
|
1110
|
+
...ctx.machineId ? { SLOCK_CURRENT_COMPUTER_ID: ctx.machineId } : {},
|
|
1111
|
+
...ctx.machineName ? { SLOCK_CURRENT_COMPUTER_NAME: ctx.machineName } : {},
|
|
1112
|
+
...ctx.machineHostname ? { SLOCK_CURRENT_COMPUTER_HOSTNAME: ctx.machineHostname } : {},
|
|
1113
|
+
...ctx.machineOs ? { SLOCK_CURRENT_COMPUTER_OS: ctx.machineOs } : {},
|
|
1114
|
+
...ctx.daemonVersion ? { SLOCK_CURRENT_DAEMON_VERSION: ctx.daemonVersion } : {},
|
|
1115
|
+
...ctx.workspacePath ? { SLOCK_CURRENT_WORKSPACE_PATH: ctx.workspacePath } : {}
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1048
1118
|
function buildCliTransportSystemPrompt(config, opts) {
|
|
1049
1119
|
return buildCliSystemPrompt(config, opts);
|
|
1050
1120
|
}
|
|
@@ -1074,6 +1144,7 @@ exec ${shellSingleQuote(process.execPath)} ${shellSingleQuote(ctx.slockCliPath)}
|
|
|
1074
1144
|
FORCE_COLOR: "0",
|
|
1075
1145
|
...ctx.config.envVars || {},
|
|
1076
1146
|
...extraEnv,
|
|
1147
|
+
...runtimeContextEnv(ctx.config),
|
|
1077
1148
|
SLOCK_AGENT_ID: ctx.agentId,
|
|
1078
1149
|
...ctx.launchId ? { SLOCK_AGENT_LAUNCH_ID: ctx.launchId } : {},
|
|
1079
1150
|
SLOCK_SERVER_URL: ctx.config.serverUrl,
|
|
@@ -1097,7 +1168,7 @@ function normalizeExecOutput(raw) {
|
|
|
1097
1168
|
return Buffer.isBuffer(raw) ? raw.toString("utf8") : String(raw ?? "");
|
|
1098
1169
|
}
|
|
1099
1170
|
function resolveCommandOnWindows(command, env, execFileSyncFn) {
|
|
1100
|
-
const script = "$cmd = Get-Command -Name $args[0] -ErrorAction Stop | Select-Object -First 1; if ($cmd.Path) { $cmd.Path } elseif ($cmd.Source) { $cmd.Source } elseif ($cmd.Definition) { $cmd.Definition }";
|
|
1171
|
+
const script = "& {$cmd = Get-Command -Name $args[0] -ErrorAction Stop | Select-Object -First 1; if ($cmd.Path) { $cmd.Path } elseif ($cmd.Source) { $cmd.Source } elseif ($cmd.Definition) { $cmd.Definition } }";
|
|
1101
1172
|
try {
|
|
1102
1173
|
const output = normalizeExecOutput(execFileSyncFn("powershell.exe", [
|
|
1103
1174
|
"-NoProfile",
|
|
@@ -1207,6 +1278,8 @@ var ClaudeDriver = class {
|
|
|
1207
1278
|
"--allow-dangerously-skip-permissions",
|
|
1208
1279
|
"--dangerously-skip-permissions",
|
|
1209
1280
|
"--verbose",
|
|
1281
|
+
"--permission-mode",
|
|
1282
|
+
"bypassPermissions",
|
|
1210
1283
|
"--output-format",
|
|
1211
1284
|
"stream-json",
|
|
1212
1285
|
"--input-format",
|
|
@@ -1217,16 +1290,16 @@ var ClaudeDriver = class {
|
|
|
1217
1290
|
CLAUDE_DISALLOWED_TOOLS
|
|
1218
1291
|
];
|
|
1219
1292
|
if (opts.standingPromptFilePath) {
|
|
1220
|
-
args.push("--
|
|
1293
|
+
args.push("--system-prompt-file", opts.standingPromptFilePath);
|
|
1221
1294
|
} else {
|
|
1222
|
-
args.push("--
|
|
1295
|
+
args.push("--system-prompt", standingPrompt);
|
|
1223
1296
|
}
|
|
1224
1297
|
if (config.sessionId) {
|
|
1225
1298
|
args.push("--resume", config.sessionId);
|
|
1226
1299
|
}
|
|
1227
1300
|
return args;
|
|
1228
1301
|
}
|
|
1229
|
-
|
|
1302
|
+
buildRuntimeActionsMcpConfig(ctx) {
|
|
1230
1303
|
const isTsSource = ctx.chatBridgePath.endsWith(".ts");
|
|
1231
1304
|
const command = isTsSource ? "npx" : "node";
|
|
1232
1305
|
const bridgeArgs = isTsSource ? ["tsx", ctx.chatBridgePath] : [ctx.chatBridgePath];
|
|
@@ -1245,7 +1318,7 @@ var ClaudeDriver = class {
|
|
|
1245
1318
|
"--runtime",
|
|
1246
1319
|
this.id,
|
|
1247
1320
|
...ctx.launchId ? ["--launch-id", ctx.launchId] : [],
|
|
1248
|
-
"--
|
|
1321
|
+
"--runtime-actions-only"
|
|
1249
1322
|
]
|
|
1250
1323
|
}
|
|
1251
1324
|
}
|
|
@@ -1255,7 +1328,7 @@ var ClaudeDriver = class {
|
|
|
1255
1328
|
const systemPromptPath = path3.join(slockDir, CLAUDE_SYSTEM_PROMPT_FILE);
|
|
1256
1329
|
const mcpConfigPath = path3.join(slockDir, CLAUDE_MCP_CONFIG_FILE);
|
|
1257
1330
|
writeFileSync2(systemPromptPath, ctx.standingPrompt, { mode: 384 });
|
|
1258
|
-
writeFileSync2(mcpConfigPath, this.
|
|
1331
|
+
writeFileSync2(mcpConfigPath, this.buildRuntimeActionsMcpConfig(ctx), { mode: 384 });
|
|
1259
1332
|
return { systemPromptPath, mcpConfigPath };
|
|
1260
1333
|
}
|
|
1261
1334
|
spawn(ctx) {
|
|
@@ -1264,7 +1337,7 @@ var ClaudeDriver = class {
|
|
|
1264
1337
|
const args = this.buildClaudeArgs(ctx.config, ctx.standingPrompt, {
|
|
1265
1338
|
standingPromptFilePath: systemPromptPath
|
|
1266
1339
|
});
|
|
1267
|
-
args.push("--mcp-config", mcpConfigPath);
|
|
1340
|
+
args.push("--mcp-config", mcpConfigPath, "--strict-mcp-config");
|
|
1268
1341
|
delete spawnEnv.CLAUDECODE;
|
|
1269
1342
|
logger.info(
|
|
1270
1343
|
`[Agent ${ctx.agentId}] transport=cli cli=${ctx.slockCliPath} token_file=${tokenFile}`
|
|
@@ -1388,7 +1461,9 @@ var ClaudeDriver = class {
|
|
|
1388
1461
|
buildSystemPrompt(config, _agentId) {
|
|
1389
1462
|
return buildCliTransportSystemPrompt(config, {
|
|
1390
1463
|
toolPrefix: "mcp__chat__",
|
|
1391
|
-
extraCriticalRules: [
|
|
1464
|
+
extraCriticalRules: [
|
|
1465
|
+
"- 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 `mcp__chat__runtime_profile_migration_done` tool with the exact `migration_key`; do not use `slock` CLI or reply in chat as the acknowledgment."
|
|
1466
|
+
],
|
|
1392
1467
|
postStartupNotes: [
|
|
1393
1468
|
"**Claude runtime note:** Slock preserves Claude Code same-turn steering through a gated stream-json delivery path. Busy messages are buffered and delivered at Claude-observed safe boundaries; if no earlier safe boundary is available, they are delivered after the current turn ends.",
|
|
1394
1469
|
"For long tool runs, you can also use `slock message check` at natural breakpoints to pull pending messages explicitly."
|
|
@@ -1498,7 +1573,7 @@ var CodexDriver = class {
|
|
|
1498
1573
|
probe() {
|
|
1499
1574
|
return probeCodex();
|
|
1500
1575
|
}
|
|
1501
|
-
|
|
1576
|
+
buildRuntimeActionsConfigArgs(ctx) {
|
|
1502
1577
|
const isTsSource = ctx.chatBridgePath.endsWith(".ts");
|
|
1503
1578
|
const command = isTsSource ? "npx" : "node";
|
|
1504
1579
|
const bridgeArgs = isTsSource ? [
|
|
@@ -1513,7 +1588,7 @@ var CodexDriver = class {
|
|
|
1513
1588
|
"--runtime",
|
|
1514
1589
|
this.id,
|
|
1515
1590
|
...ctx.launchId ? ["--launch-id", ctx.launchId] : [],
|
|
1516
|
-
"--
|
|
1591
|
+
"--runtime-actions-only"
|
|
1517
1592
|
] : [
|
|
1518
1593
|
ctx.chatBridgePath,
|
|
1519
1594
|
"--agent-id",
|
|
@@ -1525,7 +1600,7 @@ var CodexDriver = class {
|
|
|
1525
1600
|
"--runtime",
|
|
1526
1601
|
this.id,
|
|
1527
1602
|
...ctx.launchId ? ["--launch-id", ctx.launchId] : [],
|
|
1528
|
-
"--
|
|
1603
|
+
"--runtime-actions-only"
|
|
1529
1604
|
];
|
|
1530
1605
|
return [
|
|
1531
1606
|
"-c",
|
|
@@ -1595,7 +1670,7 @@ var CodexDriver = class {
|
|
|
1595
1670
|
this.streamedAgentMessageIds.clear();
|
|
1596
1671
|
this.streamedReasoningIds.clear();
|
|
1597
1672
|
const args = ["app-server", "--listen", "stdio://"];
|
|
1598
|
-
args.push(...this.
|
|
1673
|
+
args.push(...this.buildRuntimeActionsConfigArgs(ctx));
|
|
1599
1674
|
const { command, args: spawnArgs } = resolveCodexSpawn(args);
|
|
1600
1675
|
const proc = spawn2(command, spawnArgs, {
|
|
1601
1676
|
cwd: ctx.workingDirectory,
|
|
@@ -2660,6 +2735,86 @@ function formatMessageTarget(message) {
|
|
|
2660
2735
|
function getMessageShortId(messageId) {
|
|
2661
2736
|
return messageId.startsWith("thread-") ? messageId.slice(7) : messageId.slice(0, 8);
|
|
2662
2737
|
}
|
|
2738
|
+
function findSessionJsonl(root, predicate) {
|
|
2739
|
+
let visited = 0;
|
|
2740
|
+
const maxEntries = 1e4;
|
|
2741
|
+
const maxDepth = 8;
|
|
2742
|
+
const visit = (dir, depth) => {
|
|
2743
|
+
if (depth < 0 || visited >= maxEntries) return null;
|
|
2744
|
+
let entries;
|
|
2745
|
+
try {
|
|
2746
|
+
entries = readdirSync(dir, { withFileTypes: true }).sort((a, b) => b.name.localeCompare(a.name));
|
|
2747
|
+
} catch {
|
|
2748
|
+
return null;
|
|
2749
|
+
}
|
|
2750
|
+
for (const entry of entries) {
|
|
2751
|
+
if (++visited > maxEntries) return null;
|
|
2752
|
+
if (!entry.isFile() || !predicate(entry.name)) continue;
|
|
2753
|
+
return path10.join(dir, entry.name);
|
|
2754
|
+
}
|
|
2755
|
+
for (const entry of entries) {
|
|
2756
|
+
if (++visited > maxEntries) return null;
|
|
2757
|
+
if (!entry.isDirectory()) continue;
|
|
2758
|
+
const found = visit(path10.join(dir, entry.name), depth - 1);
|
|
2759
|
+
if (found) return found;
|
|
2760
|
+
}
|
|
2761
|
+
return null;
|
|
2762
|
+
};
|
|
2763
|
+
return visit(root, maxDepth);
|
|
2764
|
+
}
|
|
2765
|
+
function safeSessionFilename(value) {
|
|
2766
|
+
const normalized = value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
2767
|
+
return normalized || "unknown-session";
|
|
2768
|
+
}
|
|
2769
|
+
function writeRuntimeSessionHandoff(runtime, sessionId, fallbackDir) {
|
|
2770
|
+
try {
|
|
2771
|
+
const dir = path10.join(fallbackDir, ".slock", "runtime-sessions");
|
|
2772
|
+
mkdirSync4(dir, { recursive: true });
|
|
2773
|
+
const filePath = path10.join(dir, `${runtime}-${safeSessionFilename(sessionId)}.jsonl`);
|
|
2774
|
+
writeFileSync7(filePath, JSON.stringify({
|
|
2775
|
+
type: "runtime_session_handoff",
|
|
2776
|
+
runtime,
|
|
2777
|
+
sessionId,
|
|
2778
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2779
|
+
note: "The native runtime transcript file was not found on this machine; this daemon-created handoff records the previous session identity for Runtime Profile migration."
|
|
2780
|
+
}) + "\n", { mode: 384 });
|
|
2781
|
+
return {
|
|
2782
|
+
label: sessionId,
|
|
2783
|
+
path: filePath,
|
|
2784
|
+
runtime,
|
|
2785
|
+
reachable: true,
|
|
2786
|
+
reason: "native session file path not found; using daemon handoff file"
|
|
2787
|
+
};
|
|
2788
|
+
} catch {
|
|
2789
|
+
return null;
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
function resolveRuntimeSessionRef(runtime, sessionId, homeDir = os3.homedir(), fallbackDir) {
|
|
2793
|
+
const directPath = path10.isAbsolute(sessionId) ? sessionId : null;
|
|
2794
|
+
if (directPath) {
|
|
2795
|
+
try {
|
|
2796
|
+
if (statSync(directPath).isFile()) {
|
|
2797
|
+
return { label: sessionId, path: directPath, runtime, reachable: true };
|
|
2798
|
+
}
|
|
2799
|
+
} catch {
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2802
|
+
const resolvedPath = runtime === "claude" ? findSessionJsonl(path10.join(homeDir, ".claude", "projects"), (filename) => filename === `${sessionId}.jsonl`) : runtime === "codex" ? findSessionJsonl(path10.join(homeDir, ".codex", "sessions"), (filename) => filename.endsWith(".jsonl") && filename.includes(sessionId)) : null;
|
|
2803
|
+
if (!resolvedPath && fallbackDir) {
|
|
2804
|
+
const fallback = writeRuntimeSessionHandoff(runtime, sessionId, fallbackDir);
|
|
2805
|
+
if (fallback) return fallback;
|
|
2806
|
+
}
|
|
2807
|
+
const ref = {
|
|
2808
|
+
label: sessionId,
|
|
2809
|
+
path: resolvedPath ?? sessionId,
|
|
2810
|
+
runtime,
|
|
2811
|
+
reachable: Boolean(resolvedPath)
|
|
2812
|
+
};
|
|
2813
|
+
if (!resolvedPath) {
|
|
2814
|
+
ref.reason = "session file path not found";
|
|
2815
|
+
}
|
|
2816
|
+
return ref;
|
|
2817
|
+
}
|
|
2663
2818
|
function formatSenderHandle(message) {
|
|
2664
2819
|
return message.sender_description ? `@${message.sender_name} \u2014 ${message.sender_description}` : `@${message.sender_name}`;
|
|
2665
2820
|
}
|
|
@@ -2699,6 +2854,42 @@ function formatIncomingMessage(message) {
|
|
|
2699
2854
|
return threadJoinPrefix ? `${threadJoinPrefix}
|
|
2700
2855
|
${body}` : body;
|
|
2701
2856
|
}
|
|
2857
|
+
function formatRuntimeProfileControlPrompt(messages) {
|
|
2858
|
+
const controls = messages.map((message) => ({
|
|
2859
|
+
message,
|
|
2860
|
+
notification: runtimeProfileNotificationFromMessage(message)
|
|
2861
|
+
}));
|
|
2862
|
+
if (controls.length === 0 || controls.some(({ notification }) => !notification)) {
|
|
2863
|
+
return null;
|
|
2864
|
+
}
|
|
2865
|
+
const body = controls.map(({ message }) => message.content).join("\n\n---\n\n");
|
|
2866
|
+
return [
|
|
2867
|
+
"Runtime Profile control notice.",
|
|
2868
|
+
"",
|
|
2869
|
+
"Complete the required runtime control action before reading or responding to normal inbox messages.",
|
|
2870
|
+
"",
|
|
2871
|
+
body,
|
|
2872
|
+
"",
|
|
2873
|
+
"Do not answer this notice in prose as the acknowledgment."
|
|
2874
|
+
].join("\n");
|
|
2875
|
+
}
|
|
2876
|
+
function formatRuntimeProfileControlStartupInput(control, driver) {
|
|
2877
|
+
if (control.kind !== "migration") {
|
|
2878
|
+
return [
|
|
2879
|
+
"Read the Runtime Profile daemon release notice from your system prompt before normal work.",
|
|
2880
|
+
"No chat reply is required for this notice. Stop after reading it; queued inbox messages will be delivered separately."
|
|
2881
|
+
].join("\n");
|
|
2882
|
+
}
|
|
2883
|
+
const actionName = `${driver.mcpToolPrefix}runtime_profile_migration_done`;
|
|
2884
|
+
return [
|
|
2885
|
+
"Runtime Profile migration is required before normal work.",
|
|
2886
|
+
`Invoke the available runtime control action \`${actionName}\` with exactly this migration_key: \`${control.key}\`.`,
|
|
2887
|
+
"Do not read MEMORY.md, check messages, or send a chat reply before this tool call.",
|
|
2888
|
+
"After the runtime control action succeeds, stop. Queued inbox messages will be delivered separately.",
|
|
2889
|
+
"",
|
|
2890
|
+
control.message
|
|
2891
|
+
].join("\n");
|
|
2892
|
+
}
|
|
2702
2893
|
function buildUnreadSummary(messages, excludeChannel) {
|
|
2703
2894
|
const summary = /* @__PURE__ */ new Map();
|
|
2704
2895
|
for (const message of messages) {
|
|
@@ -2723,6 +2914,16 @@ var FIRST_CINDY_SEED_MODE = "first-cindy";
|
|
|
2723
2914
|
function getOnboardingSeedMode(config) {
|
|
2724
2915
|
return (config.envVars?.[ONBOARDING_MEMORY_SEED_ENV] || "").trim().toLowerCase();
|
|
2725
2916
|
}
|
|
2917
|
+
function withLocalRuntimeContext(config, agentId, workspacePath) {
|
|
2918
|
+
return {
|
|
2919
|
+
...config,
|
|
2920
|
+
runtimeContext: {
|
|
2921
|
+
...config.runtimeContext ?? {},
|
|
2922
|
+
agentId: config.runtimeContext?.agentId ?? agentId,
|
|
2923
|
+
workspacePath
|
|
2924
|
+
}
|
|
2925
|
+
};
|
|
2926
|
+
}
|
|
2726
2927
|
function buildOnboardingPlaybookMd() {
|
|
2727
2928
|
return `# Cindy Onboarding Playbook
|
|
2728
2929
|
|
|
@@ -2731,14 +2932,18 @@ Start warm and brief.
|
|
|
2731
2932
|
Move quickly to one useful action, not a feature tour.
|
|
2732
2933
|
Keep activation energy low: invite the user to start with one sentence about what they need now.
|
|
2733
2934
|
|
|
2734
|
-
## Step 2:
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2935
|
+
## Step 2: Activate or Propose
|
|
2936
|
+
Use one decision: does the user already know what they want to do?
|
|
2937
|
+
- Yes: skip role/work intake and propose a starter plan.
|
|
2938
|
+
- No: ask what they do and what they are working on. These questions are activation, not a questionnaire.
|
|
2939
|
+
|
|
2940
|
+
After any usable signal, stop asking and propose.
|
|
2941
|
+
After confirming language preference, do not give a generic product introduction; move into the user's work or a starter action.
|
|
2738
2942
|
|
|
2739
2943
|
## Step 3: Route by Intent (A-E)
|
|
2740
2944
|
- A: Specific project/task
|
|
2741
|
-
-
|
|
2945
|
+
- Enter starter-task mode immediately.
|
|
2946
|
+
- Propose first setup actions before asking for more detail.
|
|
2742
2947
|
- B: "What can you do?" curiosity
|
|
2743
2948
|
- Proactively share 1-2 interview-grounded examples, then ask the user to pick one.
|
|
2744
2949
|
- Use this opener tone: "Here are some examples our users have shared with us. I'm sharing these to inspire you."
|
|
@@ -2749,6 +2954,28 @@ Ask only what is needed to route:
|
|
|
2749
2954
|
- E: Low-intent greeting/testing
|
|
2750
2955
|
- Use a low-pressure prompt and guide to one concrete starter action.
|
|
2751
2956
|
|
|
2957
|
+
### Starter Plan Output
|
|
2958
|
+
A starter plan should make the next action executable, not just descriptive:
|
|
2959
|
+
- agent name + role description that can be copied into the create-agent form
|
|
2960
|
+
- suggested channel or workstream
|
|
2961
|
+
- first task to send after creation
|
|
2962
|
+
- next UI action if the user needs to create an agent or channel
|
|
2963
|
+
|
|
2964
|
+
Do not use a rigid keyword routing table. Use examples as inspiration, then adapt to the user's context.
|
|
2965
|
+
If details are missing but not blocking, state reasonable defaults and invite correction.
|
|
2966
|
+
Only ask one blocking question first if the answer is required before any useful starter plan can be proposed.
|
|
2967
|
+
Do not imply you have already created agents or channels unless the action has actually happened.
|
|
2968
|
+
|
|
2969
|
+
### Capability Boundary Pivot
|
|
2970
|
+
If the user's primary request is outside current capabilities, acknowledge the limitation once and pivot immediately to the nearest useful alternative.
|
|
2971
|
+
Do not repeat that something is impossible across multiple turns.
|
|
2972
|
+
Offer a concrete substitute: a manual input path, a narrower analysis task, an agent/team setup, or another workflow Slock can execute now.
|
|
2973
|
+
|
|
2974
|
+
### Active-Elsewhere Handoff
|
|
2975
|
+
Channel silence is not failure.
|
|
2976
|
+
If the user is already active outside the onboarding channel, follow the work instead of trying to pull them back.
|
|
2977
|
+
Offer a concrete next step in the context they are using: first task, second agent suggestion, channel structure, or reminder.
|
|
2978
|
+
|
|
2752
2979
|
## Step 4: Progress Setup (Soft Guidance)
|
|
2753
2980
|
While helping with real work, progressively shape:
|
|
2754
2981
|
- initial team target >= 3 agents
|
|
@@ -2762,6 +2989,8 @@ Do not force setup before value.
|
|
|
2762
2989
|
|
|
2763
2990
|
## Step 5: End Every Turn with One Next Step
|
|
2764
2991
|
Each reply should end with one clear, immediate action.
|
|
2992
|
+
At wrap-up, if there is a concrete next check-in, ask consent to set one contextual reminder.
|
|
2993
|
+
The reminder must reference the user's goal, agent, recent step, or suggested next action; do not send generic "come back later" reminders.
|
|
2765
2994
|
|
|
2766
2995
|
## Inspiration Stories (Interview-Grounded)
|
|
2767
2996
|
- Story 1: "Sense of abundance" \u2014 agents self-organize, you do not need to micro-manage.
|
|
@@ -2954,6 +3183,34 @@ Do not copy these answers verbatim.
|
|
|
2954
3183
|
|
|
2955
3184
|
### Guardrail
|
|
2956
3185
|
- Keep contact guidance concrete and current; do not invent alternative support channels.
|
|
3186
|
+
|
|
3187
|
+
## FAQ 14: Can I use Slock on my phone?
|
|
3188
|
+
### Answer idea
|
|
3189
|
+
- Yes. Slock can be used from a mobile browser.
|
|
3190
|
+
- For easier return access, users can add Slock to their phone home screen as a web app:
|
|
3191
|
+
- iPhone: Safari \u2192 Share \u2192 Add to Home Screen
|
|
3192
|
+
- Android: Chrome \u2192 menu \u2192 Add to Home Screen / Install app
|
|
3193
|
+
- Good mobile use cases: quick check-ins, todos/reminders, short replies, and reviewing agent updates.
|
|
3194
|
+
|
|
3195
|
+
### Next step
|
|
3196
|
+
- If the user wants mobile access now, ask whether they use iPhone or Android, then guide the matching Add to Home Screen step.
|
|
3197
|
+
|
|
3198
|
+
### Guardrail
|
|
3199
|
+
- Do not imply Slock has a native iOS/Android App Store app.
|
|
3200
|
+
- Do not over-sell it as fully equivalent to a native app; call it mobile browser / home-screen web app.
|
|
3201
|
+
|
|
3202
|
+
## FAQ 15: How do I create agents or channels?
|
|
3203
|
+
### Answer idea
|
|
3204
|
+
- The user can create agents from the Agents section in the People tab by clicking the + button, or from a computer/machine context menu via Create Agent.
|
|
3205
|
+
- The user can create channels from the Channels section by clicking the + button.
|
|
3206
|
+
- When recommending setup, provide copyable agent names/descriptions, suggested channel names, and the first task to send after creation.
|
|
3207
|
+
|
|
3208
|
+
### Next step
|
|
3209
|
+
- Give the exact agent/channel spec the user can copy, then guide them to the relevant + button or creation dialog.
|
|
3210
|
+
|
|
3211
|
+
### Guardrail
|
|
3212
|
+
- Do not imply you already created agents or channels unless that action actually happened.
|
|
3213
|
+
- If you cannot directly create something, avoid a long permissions explanation; give the copyable spec and the next UI action.
|
|
2957
3214
|
`;
|
|
2958
3215
|
}
|
|
2959
3216
|
function buildOnboardingSeedFiles() {
|
|
@@ -3129,7 +3386,12 @@ function getBusyDeliveryNote(driver) {
|
|
|
3129
3386
|
}
|
|
3130
3387
|
return "\n\nNote: While you are busy, you may receive [System notification: ...] messages. Finish your current step, then call check_messages to check for messages.";
|
|
3131
3388
|
}
|
|
3132
|
-
var NATIVE_STANDING_PROMPT_STARTUP_INPUT =
|
|
3389
|
+
var NATIVE_STANDING_PROMPT_STARTUP_INPUT = (
|
|
3390
|
+
// Claude Code 2.1.114 treats "follow your system prompt" style user turns as
|
|
3391
|
+
// prompt-injection bait even when the prompt is supplied via --system-prompt-file.
|
|
3392
|
+
// A neutral starter lets the native standing prompt drive startup/migration.
|
|
3393
|
+
"Start."
|
|
3394
|
+
);
|
|
3133
3395
|
var AgentProcessManager = class _AgentProcessManager {
|
|
3134
3396
|
agents = /* @__PURE__ */ new Map();
|
|
3135
3397
|
agentsStarting = /* @__PURE__ */ new Set();
|
|
@@ -3143,6 +3405,7 @@ var AgentProcessManager = class _AgentProcessManager {
|
|
|
3143
3405
|
daemonApiKey;
|
|
3144
3406
|
serverUrl;
|
|
3145
3407
|
dataDir;
|
|
3408
|
+
runtimeSessionHomeDir;
|
|
3146
3409
|
driverResolver;
|
|
3147
3410
|
defaultAgentEnvVarsProvider;
|
|
3148
3411
|
tracer;
|
|
@@ -3153,6 +3416,7 @@ var AgentProcessManager = class _AgentProcessManager {
|
|
|
3153
3416
|
this.daemonApiKey = daemonApiKey;
|
|
3154
3417
|
this.serverUrl = opts.serverUrl;
|
|
3155
3418
|
this.dataDir = opts.dataDir || DATA_DIR;
|
|
3419
|
+
this.runtimeSessionHomeDir = opts.runtimeSessionHomeDir || os3.homedir();
|
|
3156
3420
|
this.driverResolver = opts.driverResolver || getDriver;
|
|
3157
3421
|
this.defaultAgentEnvVarsProvider = opts.defaultAgentEnvVarsProvider || null;
|
|
3158
3422
|
this.tracer = opts.tracer ?? noopTracer;
|
|
@@ -3171,11 +3435,12 @@ var AgentProcessManager = class _AgentProcessManager {
|
|
|
3171
3435
|
const driver = this.driverResolver(config.runtime || "claude");
|
|
3172
3436
|
const agentDataDir = path10.join(this.dataDir, agentId);
|
|
3173
3437
|
await mkdir(agentDataDir, { recursive: true });
|
|
3438
|
+
const runtimeConfig = withLocalRuntimeContext(config, agentId, agentDataDir);
|
|
3174
3439
|
const memoryMdPath = path10.join(agentDataDir, "MEMORY.md");
|
|
3175
3440
|
try {
|
|
3176
3441
|
await access(memoryMdPath);
|
|
3177
3442
|
} catch {
|
|
3178
|
-
const initialMemoryMd = buildInitialMemoryMd(
|
|
3443
|
+
const initialMemoryMd = buildInitialMemoryMd(runtimeConfig);
|
|
3179
3444
|
await writeFile(memoryMdPath, initialMemoryMd);
|
|
3180
3445
|
}
|
|
3181
3446
|
const notesDir = path10.join(agentDataDir, "notes");
|
|
@@ -3192,18 +3457,21 @@ var AgentProcessManager = class _AgentProcessManager {
|
|
|
3192
3457
|
}
|
|
3193
3458
|
}
|
|
3194
3459
|
}
|
|
3195
|
-
const isResume = !!
|
|
3196
|
-
const standingPrompt = driver.buildSystemPrompt(
|
|
3460
|
+
const isResume = !!runtimeConfig.sessionId;
|
|
3461
|
+
const standingPrompt = driver.buildSystemPrompt(runtimeConfig, agentId);
|
|
3197
3462
|
let prompt;
|
|
3198
|
-
if (
|
|
3463
|
+
if (runtimeConfig.runtimeProfileControl && !wakeMessage) {
|
|
3464
|
+
prompt = driver.supportsNativeStandingPrompt ? NATIVE_STANDING_PROMPT_STARTUP_INPUT : formatRuntimeProfileControlStartupInput(runtimeConfig.runtimeProfileControl, driver);
|
|
3465
|
+
} else if (isResume && resumePrompt) {
|
|
3199
3466
|
prompt = resumePrompt;
|
|
3200
3467
|
prompt += getBusyDeliveryNote(driver);
|
|
3201
3468
|
} else if (wakeMessage) {
|
|
3469
|
+
const runtimeProfileControlPrompt = formatRuntimeProfileControlPrompt([wakeMessage]);
|
|
3202
3470
|
const channelLabel = formatChannelLabel(wakeMessage);
|
|
3203
|
-
prompt = `New message received:
|
|
3471
|
+
prompt = runtimeProfileControlPrompt ?? `New message received:
|
|
3204
3472
|
|
|
3205
3473
|
${formatIncomingMessage(wakeMessage)}`;
|
|
3206
|
-
if (unreadSummary && Object.keys(unreadSummary).length > 0) {
|
|
3474
|
+
if (!runtimeProfileControlPrompt && unreadSummary && Object.keys(unreadSummary).length > 0) {
|
|
3207
3475
|
const otherUnread = Object.entries(unreadSummary).filter(([key]) => key !== channelLabel);
|
|
3208
3476
|
if (otherUnread.length > 0) {
|
|
3209
3477
|
prompt += `
|
|
@@ -3218,12 +3486,14 @@ You also have unread messages in other channels:`;
|
|
|
3218
3486
|
Use read_history to catch up, or respond to the message above first.`;
|
|
3219
3487
|
}
|
|
3220
3488
|
}
|
|
3221
|
-
|
|
3489
|
+
if (!runtimeProfileControlPrompt) {
|
|
3490
|
+
prompt += `
|
|
3222
3491
|
|
|
3223
3492
|
Respond as appropriate \u2014 reply using send_message, or take action as needed. Complete ALL your work before stopping.
|
|
3224
3493
|
|
|
3225
3494
|
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)}`;
|
|
3226
|
-
|
|
3495
|
+
prompt += getBusyDeliveryNote(driver);
|
|
3496
|
+
}
|
|
3227
3497
|
} else if (isResume && unreadSummary && Object.keys(unreadSummary).length > 0) {
|
|
3228
3498
|
prompt = `You have unread messages from while you were offline:`;
|
|
3229
3499
|
for (const [ch, count] of Object.entries(unreadSummary)) {
|
|
@@ -3239,7 +3509,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
3239
3509
|
} else {
|
|
3240
3510
|
prompt = driver.supportsNativeStandingPrompt ? NATIVE_STANDING_PROMPT_STARTUP_INPUT : standingPrompt;
|
|
3241
3511
|
}
|
|
3242
|
-
const effectiveConfig = await this.buildSpawnConfig(agentId,
|
|
3512
|
+
const effectiveConfig = await this.buildSpawnConfig(agentId, runtimeConfig);
|
|
3243
3513
|
const { process: proc } = driver.spawn({
|
|
3244
3514
|
agentId,
|
|
3245
3515
|
config: effectiveConfig,
|
|
@@ -3255,8 +3525,8 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
3255
3525
|
process: proc,
|
|
3256
3526
|
driver,
|
|
3257
3527
|
inbox: this.startingInboxes.get(agentId) || [],
|
|
3258
|
-
config,
|
|
3259
|
-
sessionId:
|
|
3528
|
+
config: runtimeConfig,
|
|
3529
|
+
sessionId: runtimeConfig.sessionId || null,
|
|
3260
3530
|
launchId: launchId || null,
|
|
3261
3531
|
isIdle: false,
|
|
3262
3532
|
notificationTimer: null,
|
|
@@ -3282,6 +3552,9 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
3282
3552
|
this.agents.set(agentId, agentProcess);
|
|
3283
3553
|
this.startRuntimeTrace(agentId, agentProcess, "spawn");
|
|
3284
3554
|
this.agentsStarting.delete(agentId);
|
|
3555
|
+
if (config.runtimeProfileControl) {
|
|
3556
|
+
this.ackInjectedRuntimeProfileControl(agentId, config.runtimeProfileControl, agentProcess.launchId);
|
|
3557
|
+
}
|
|
3285
3558
|
if (wakeMessage) {
|
|
3286
3559
|
this.ackInjectedRuntimeProfileMessages(agentId, [wakeMessage], agentProcess.launchId);
|
|
3287
3560
|
}
|
|
@@ -3492,6 +3765,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
3492
3765
|
this.agents.delete(agentId);
|
|
3493
3766
|
ap.process.kill("SIGTERM");
|
|
3494
3767
|
if (!silent) {
|
|
3768
|
+
this.sendRuntimeProfileReportFor(agentId, ap.config, ap.sessionId, ap.launchId);
|
|
3495
3769
|
this.sendAgentStatus(agentId, "inactive", ap.launchId);
|
|
3496
3770
|
this.broadcastActivity(agentId, "offline", "Stopped");
|
|
3497
3771
|
logger.info(`[Agent ${agentId}] Stopped by request`);
|
|
@@ -3610,12 +3884,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
3610
3884
|
path: workspacePath,
|
|
3611
3885
|
reachable: true
|
|
3612
3886
|
},
|
|
3613
|
-
sessionRef: sessionId ?
|
|
3614
|
-
label: sessionId,
|
|
3615
|
-
path: sessionId,
|
|
3616
|
-
runtime: config.runtime,
|
|
3617
|
-
reachable: true
|
|
3618
|
-
} : null
|
|
3887
|
+
sessionRef: sessionId ? resolveRuntimeSessionRef(config.runtime, sessionId, this.runtimeSessionHomeDir, workspacePath) : null
|
|
3619
3888
|
}
|
|
3620
3889
|
};
|
|
3621
3890
|
}
|
|
@@ -3706,9 +3975,26 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
3706
3975
|
}
|
|
3707
3976
|
}
|
|
3708
3977
|
}
|
|
3709
|
-
|
|
3710
|
-
const
|
|
3711
|
-
|
|
3978
|
+
ackInjectedRuntimeProfileControl(agentId, control, launchId) {
|
|
3979
|
+
const title = runtimeProfileNotificationTitle(control.kind);
|
|
3980
|
+
this.broadcastActivity(agentId, "working", title, [{ kind: "system", title, text: control.message }], launchId);
|
|
3981
|
+
if (control.kind === "migration") {
|
|
3982
|
+
this.sendToServer({
|
|
3983
|
+
type: "agent:runtime_profile:migration:ack",
|
|
3984
|
+
agentId,
|
|
3985
|
+
migrationKey: control.key,
|
|
3986
|
+
launchId: launchId || void 0
|
|
3987
|
+
});
|
|
3988
|
+
} else {
|
|
3989
|
+
this.sendToServer({
|
|
3990
|
+
type: "agent:runtime_profile:daemon_release_notice:ack",
|
|
3991
|
+
agentId,
|
|
3992
|
+
noticeKey: control.key,
|
|
3993
|
+
launchId: launchId || void 0
|
|
3994
|
+
});
|
|
3995
|
+
}
|
|
3996
|
+
}
|
|
3997
|
+
sendRuntimeProfileWireReport(report) {
|
|
3712
3998
|
this.sendToServer({
|
|
3713
3999
|
type: "agent:runtime_profile",
|
|
3714
4000
|
agentId: report.agentId,
|
|
@@ -3716,6 +4002,14 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
3716
4002
|
launchId: report.launchId || void 0
|
|
3717
4003
|
});
|
|
3718
4004
|
}
|
|
4005
|
+
sendRuntimeProfileReportFor(agentId, config, sessionId, launchId) {
|
|
4006
|
+
this.sendRuntimeProfileWireReport(this.buildRuntimeProfileReport(agentId, config, sessionId, launchId));
|
|
4007
|
+
}
|
|
4008
|
+
sendRuntimeProfileReport(agentId) {
|
|
4009
|
+
const report = this.getAgentRuntimeProfileReport(agentId);
|
|
4010
|
+
if (!report) return;
|
|
4011
|
+
this.sendRuntimeProfileWireReport(report);
|
|
4012
|
+
}
|
|
3719
4013
|
// Machine-level workspace scanning
|
|
3720
4014
|
async scanAllWorkspaces() {
|
|
3721
4015
|
return scanWorkspaceDirectories(this.dataDir);
|
|
@@ -4311,7 +4605,7 @@ Use read_history to catch up on the channels listed above, then stop. Read each
|
|
|
4311
4605
|
/** Deliver a message to an agent via stdin, formatting it the same way as the MCP bridge */
|
|
4312
4606
|
deliverMessagesViaStdin(agentId, ap, messages, mode) {
|
|
4313
4607
|
if (messages.length === 0) return true;
|
|
4314
|
-
const prompt = messages.length === 1 ? `New message received:
|
|
4608
|
+
const prompt = formatRuntimeProfileControlPrompt(messages) ?? (messages.length === 1 ? `New message received:
|
|
4315
4609
|
|
|
4316
4610
|
${formatIncomingMessage(messages[0])}
|
|
4317
4611
|
|
|
@@ -4319,7 +4613,7 @@ Respond as appropriate. Complete all your work before stopping.` : `New messages
|
|
|
4319
4613
|
|
|
4320
4614
|
${messages.map((message) => formatIncomingMessage(message)).join("\n")}
|
|
4321
4615
|
|
|
4322
|
-
Respond as appropriate. Complete all your work before stopping
|
|
4616
|
+
Respond as appropriate. Complete all your work before stopping.`);
|
|
4323
4617
|
const encoded = ap.driver.encodeStdinMessage(prompt, ap.sessionId, { mode });
|
|
4324
4618
|
if (!encoded) {
|
|
4325
4619
|
ap.inbox.unshift(...messages);
|
|
@@ -4585,6 +4879,112 @@ var ReminderCache = class {
|
|
|
4585
4879
|
}
|
|
4586
4880
|
};
|
|
4587
4881
|
|
|
4882
|
+
// src/machineLock.ts
|
|
4883
|
+
import { createHash, randomUUID as randomUUID2 } from "crypto";
|
|
4884
|
+
import { mkdirSync as mkdirSync5, readFileSync as readFileSync3, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync8 } from "fs";
|
|
4885
|
+
import os4 from "os";
|
|
4886
|
+
import path11 from "path";
|
|
4887
|
+
var DEFAULT_MACHINE_STATE_ROOT = path11.join(os4.homedir(), ".slock", "machines");
|
|
4888
|
+
var INCOMPLETE_LOCK_STALE_MS = 3e4;
|
|
4889
|
+
var DaemonMachineLockConflictError = class extends Error {
|
|
4890
|
+
code = "DAEMON_MACHINE_LOCK_HELD";
|
|
4891
|
+
constructor(lockDir, owner) {
|
|
4892
|
+
const ownerText = owner ? `pid=${owner.pid}, startedAt=${owner.startedAt}, host=${owner.hostname}` : "unknown owner";
|
|
4893
|
+
super(
|
|
4894
|
+
`Another Slock daemon is already running for this machine key (${ownerText}). Lock: ${lockDir}. Stop the existing daemon first, or use a different machine key/state directory.`
|
|
4895
|
+
);
|
|
4896
|
+
this.name = "DaemonMachineLockConflictError";
|
|
4897
|
+
}
|
|
4898
|
+
};
|
|
4899
|
+
function apiKeyFingerprint(apiKey) {
|
|
4900
|
+
return createHash("sha256").update(apiKey).digest("hex");
|
|
4901
|
+
}
|
|
4902
|
+
function getDaemonMachineLockId(apiKey) {
|
|
4903
|
+
return `machine-${apiKeyFingerprint(apiKey).slice(0, 16)}`;
|
|
4904
|
+
}
|
|
4905
|
+
function ownerPath(lockDir) {
|
|
4906
|
+
return path11.join(lockDir, "owner.json");
|
|
4907
|
+
}
|
|
4908
|
+
function readOwner(lockDir) {
|
|
4909
|
+
try {
|
|
4910
|
+
return JSON.parse(readFileSync3(ownerPath(lockDir), "utf8"));
|
|
4911
|
+
} catch {
|
|
4912
|
+
return null;
|
|
4913
|
+
}
|
|
4914
|
+
}
|
|
4915
|
+
function lockAgeMs(lockDir) {
|
|
4916
|
+
try {
|
|
4917
|
+
return Date.now() - statSync2(lockDir).mtimeMs;
|
|
4918
|
+
} catch {
|
|
4919
|
+
return null;
|
|
4920
|
+
}
|
|
4921
|
+
}
|
|
4922
|
+
function isProcessAlive(pid) {
|
|
4923
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
4924
|
+
try {
|
|
4925
|
+
process.kill(pid, 0);
|
|
4926
|
+
return true;
|
|
4927
|
+
} catch (err) {
|
|
4928
|
+
const code = typeof err === "object" && err && "code" in err ? err.code : void 0;
|
|
4929
|
+
return code !== "ESRCH";
|
|
4930
|
+
}
|
|
4931
|
+
}
|
|
4932
|
+
function acquireDaemonMachineLock(options) {
|
|
4933
|
+
const rootDir = options.rootDir ?? DEFAULT_MACHINE_STATE_ROOT;
|
|
4934
|
+
const fingerprint = apiKeyFingerprint(options.apiKey);
|
|
4935
|
+
const lockId = getDaemonMachineLockId(options.apiKey);
|
|
4936
|
+
const machineDir = path11.join(rootDir, lockId);
|
|
4937
|
+
const lockDir = path11.join(machineDir, "daemon.lock");
|
|
4938
|
+
const token = randomUUID2();
|
|
4939
|
+
mkdirSync5(machineDir, { recursive: true });
|
|
4940
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
4941
|
+
try {
|
|
4942
|
+
mkdirSync5(lockDir);
|
|
4943
|
+
const owner = {
|
|
4944
|
+
pid: process.pid,
|
|
4945
|
+
token,
|
|
4946
|
+
hostname: os4.hostname(),
|
|
4947
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4948
|
+
serverUrl: options.serverUrl,
|
|
4949
|
+
apiKeyFingerprint: fingerprint.slice(0, 16)
|
|
4950
|
+
};
|
|
4951
|
+
try {
|
|
4952
|
+
writeFileSync8(ownerPath(lockDir), `${JSON.stringify(owner, null, 2)}
|
|
4953
|
+
`, { mode: 384 });
|
|
4954
|
+
} catch (err) {
|
|
4955
|
+
rmSync2(lockDir, { recursive: true, force: true });
|
|
4956
|
+
throw err;
|
|
4957
|
+
}
|
|
4958
|
+
return {
|
|
4959
|
+
lockId,
|
|
4960
|
+
machineDir,
|
|
4961
|
+
lockDir,
|
|
4962
|
+
release: () => {
|
|
4963
|
+
const currentOwner = readOwner(lockDir);
|
|
4964
|
+
if (currentOwner?.pid === process.pid && currentOwner.token === token) {
|
|
4965
|
+
rmSync2(lockDir, { recursive: true, force: true });
|
|
4966
|
+
}
|
|
4967
|
+
}
|
|
4968
|
+
};
|
|
4969
|
+
} catch (err) {
|
|
4970
|
+
const code = typeof err === "object" && err && "code" in err ? err.code : void 0;
|
|
4971
|
+
if (code !== "EEXIST") throw err;
|
|
4972
|
+
const owner = readOwner(lockDir);
|
|
4973
|
+
if (owner?.pid && isProcessAlive(owner.pid)) {
|
|
4974
|
+
throw new DaemonMachineLockConflictError(lockDir, owner);
|
|
4975
|
+
}
|
|
4976
|
+
if (!owner) {
|
|
4977
|
+
const ageMs = lockAgeMs(lockDir);
|
|
4978
|
+
if (ageMs === null || ageMs < INCOMPLETE_LOCK_STALE_MS) {
|
|
4979
|
+
throw new DaemonMachineLockConflictError(lockDir, null);
|
|
4980
|
+
}
|
|
4981
|
+
}
|
|
4982
|
+
rmSync2(lockDir, { recursive: true, force: true });
|
|
4983
|
+
}
|
|
4984
|
+
}
|
|
4985
|
+
throw new DaemonMachineLockConflictError(lockDir, readOwner(lockDir));
|
|
4986
|
+
}
|
|
4987
|
+
|
|
4588
4988
|
// src/core.ts
|
|
4589
4989
|
var DAEMON_CLI_USAGE = "Usage: slock-daemon --server-url <url> --api-key <key>";
|
|
4590
4990
|
function parseDaemonCliArgs(args) {
|
|
@@ -4606,23 +5006,23 @@ function readDaemonVersion(moduleUrl = import.meta.url) {
|
|
|
4606
5006
|
}
|
|
4607
5007
|
}
|
|
4608
5008
|
function resolveChatBridgePath(moduleUrl = import.meta.url) {
|
|
4609
|
-
const dirname =
|
|
4610
|
-
const jsPath =
|
|
5009
|
+
const dirname = path12.dirname(fileURLToPath(moduleUrl));
|
|
5010
|
+
const jsPath = path12.resolve(dirname, "chat-bridge.js");
|
|
4611
5011
|
try {
|
|
4612
5012
|
accessSync(jsPath);
|
|
4613
5013
|
return jsPath;
|
|
4614
5014
|
} catch {
|
|
4615
|
-
return
|
|
5015
|
+
return path12.resolve(dirname, "chat-bridge.ts");
|
|
4616
5016
|
}
|
|
4617
5017
|
}
|
|
4618
5018
|
function resolveSlockCliPath(moduleUrl = import.meta.url) {
|
|
4619
|
-
const thisDir =
|
|
4620
|
-
const bundledDistPath =
|
|
5019
|
+
const thisDir = path12.dirname(fileURLToPath(moduleUrl));
|
|
5020
|
+
const bundledDistPath = path12.resolve(thisDir, "cli", "index.js");
|
|
4621
5021
|
try {
|
|
4622
5022
|
accessSync(bundledDistPath);
|
|
4623
5023
|
return bundledDistPath;
|
|
4624
5024
|
} catch {
|
|
4625
|
-
const workspaceDistPath =
|
|
5025
|
+
const workspaceDistPath = path12.resolve(thisDir, "..", "..", "cli", "dist", "index.js");
|
|
4626
5026
|
accessSync(workspaceDistPath);
|
|
4627
5027
|
return workspaceDistPath;
|
|
4628
5028
|
}
|
|
@@ -4701,6 +5101,7 @@ var DaemonCore = class {
|
|
|
4701
5101
|
connection;
|
|
4702
5102
|
reminderCache;
|
|
4703
5103
|
tracer;
|
|
5104
|
+
machineLock = null;
|
|
4704
5105
|
constructor(options) {
|
|
4705
5106
|
this.options = options;
|
|
4706
5107
|
this.daemonVersion = options.daemonVersion ?? readDaemonVersion();
|
|
@@ -4731,15 +5132,39 @@ var DaemonCore = class {
|
|
|
4731
5132
|
});
|
|
4732
5133
|
this.connection = connection;
|
|
4733
5134
|
}
|
|
5135
|
+
resolveMachineStateRoot() {
|
|
5136
|
+
if (this.options.machineStateDir) return this.options.machineStateDir;
|
|
5137
|
+
if (this.options.dataDir) return path12.join(path12.dirname(this.options.dataDir), "machines");
|
|
5138
|
+
return DEFAULT_MACHINE_STATE_ROOT;
|
|
5139
|
+
}
|
|
4734
5140
|
start() {
|
|
4735
5141
|
logger.info("[Slock Daemon] Starting...");
|
|
4736
|
-
this.
|
|
5142
|
+
if (!this.machineLock) {
|
|
5143
|
+
this.machineLock = acquireDaemonMachineLock({
|
|
5144
|
+
apiKey: this.options.apiKey,
|
|
5145
|
+
serverUrl: this.options.serverUrl,
|
|
5146
|
+
rootDir: this.resolveMachineStateRoot()
|
|
5147
|
+
});
|
|
5148
|
+
logger.info(`[Slock Daemon] Acquired machine lock: ${this.machineLock.lockDir}`);
|
|
5149
|
+
}
|
|
5150
|
+
try {
|
|
5151
|
+
this.connection.connect();
|
|
5152
|
+
} catch (err) {
|
|
5153
|
+
this.machineLock.release();
|
|
5154
|
+
this.machineLock = null;
|
|
5155
|
+
throw err;
|
|
5156
|
+
}
|
|
4737
5157
|
}
|
|
4738
5158
|
async stop() {
|
|
4739
5159
|
logger.info("[Slock Daemon] Shutting down...");
|
|
4740
5160
|
this.reminderCache.clear();
|
|
4741
|
-
|
|
4742
|
-
|
|
5161
|
+
try {
|
|
5162
|
+
await this.agentManager.stopAll();
|
|
5163
|
+
} finally {
|
|
5164
|
+
this.connection.disconnect();
|
|
5165
|
+
this.machineLock?.release();
|
|
5166
|
+
this.machineLock = null;
|
|
5167
|
+
}
|
|
4743
5168
|
}
|
|
4744
5169
|
get connected() {
|
|
4745
5170
|
return this.connection.connected;
|
|
@@ -4900,8 +5325,8 @@ var DaemonCore = class {
|
|
|
4900
5325
|
capabilities: ["agent:start", "agent:stop", "agent:deliver", "workspace:files"],
|
|
4901
5326
|
runtimes,
|
|
4902
5327
|
runningAgents: this.agentManager.getRunningAgentIds(),
|
|
4903
|
-
hostname: this.options.hostname ??
|
|
4904
|
-
os: this.options.osDescription ?? `${
|
|
5328
|
+
hostname: this.options.hostname ?? os5.hostname(),
|
|
5329
|
+
os: this.options.osDescription ?? `${os5.platform()} ${os5.arch()}`,
|
|
4905
5330
|
daemonVersion: this.daemonVersion
|
|
4906
5331
|
});
|
|
4907
5332
|
for (const agentId of this.agentManager.getRunningAgentIds()) {
|