@slock-ai/daemon 0.56.1 → 0.57.0-play.20260606141931

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.
@@ -1,14 +1,5 @@
1
- import {
2
- DEFAULT_CHAT_BRIDGE_TOOL_TIMEOUT_MS,
3
- buildWebSocketOptions,
4
- daemonFetch,
5
- executeJsonRequest,
6
- executeResponseRequest,
7
- logger
8
- } from "./chunk-M2KQBJR3.js";
9
-
10
1
  // src/core.ts
11
- import path17 from "path";
2
+ import path15 from "path";
12
3
  import os7 from "os";
13
4
  import { createRequire as createRequire2 } from "module";
14
5
  import { accessSync } from "fs";
@@ -932,10 +923,22 @@ var channelAddMemberOperationSchema = z.object({
932
923
  agents: z.array(idOrHandleSchema).max(64).optional(),
933
924
  draftHint: draftHintSchema
934
925
  });
926
+ var integrationApproveAgentLoginOperationSchema = z.object({
927
+ type: z.literal("integration:approve_agent_login"),
928
+ requestId: uuidSchema,
929
+ agentId: uuidSchema,
930
+ agentName: z.string().trim().min(1).max(120),
931
+ clientId: uuidSchema,
932
+ clientKey: z.string().trim().min(1).max(120),
933
+ clientName: z.string().trim().min(1).max(120),
934
+ scopes: z.array(z.string().trim().min(1).max(120)).max(64),
935
+ draftHint: draftHintSchema
936
+ });
935
937
  var actionCardActionSchema = z.discriminatedUnion("type", [
936
938
  channelCreateOperationSchema,
937
939
  agentCreateOperationSchema,
938
- channelAddMemberOperationSchema
940
+ channelAddMemberOperationSchema,
941
+ integrationApproveAgentLoginOperationSchema
939
942
  ]);
940
943
 
941
944
  // ../shared/src/agentInbox.ts
@@ -1262,10 +1265,10 @@ var DISPLAY_PLAN_CONFIG = {
1262
1265
  };
1263
1266
 
1264
1267
  // src/agentProcessManager.ts
1265
- import { mkdirSync as mkdirSync5, readdirSync as readdirSync2, statSync, writeFileSync as writeFileSync7 } from "fs";
1268
+ import { mkdirSync as mkdirSync3, readdirSync as readdirSync2, statSync, writeFileSync as writeFileSync4 } from "fs";
1266
1269
  import { mkdir, writeFile, access, readdir as readdir2, stat as stat2, readFile, rm as rm2 } from "fs/promises";
1267
1270
  import { createHash as createHash3 } from "crypto";
1268
- import path13 from "path";
1271
+ import path11 from "path";
1269
1272
  import os5 from "os";
1270
1273
 
1271
1274
  // src/drivers/claude.ts
@@ -1276,6 +1279,329 @@ import { mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
1276
1279
  import { createRequire } from "module";
1277
1280
  import path2 from "path";
1278
1281
 
1282
+ // src/drivers/slockCliGuide.ts
1283
+ var MESSAGE_HEREDOC_DELIMITER = "SLOCKMSG";
1284
+ function describeSlockInstallation(audience) {
1285
+ if (audience === "managed-runner") {
1286
+ return "The daemon injects a local `slock` wrapper into PATH for you.";
1287
+ }
1288
+ return "Install the published agent CLI: `npm i -g @slock-ai/cli@latest`. Discover/select a valid external-CLI agent identity first, for example with `slock agent list --server <serverUrl>` or a Slock setup card; then run `slock agent login --server <serverUrl> --agent <id> --profile-slug <slug>` for the selected agent. After login succeeds, invoke commands as `slock --profile <slug> ...` (or set `SLOCK_PROFILE=<slug>`).";
1289
+ }
1290
+ function buildCommunicationSection(audience) {
1291
+ const installation = describeSlockInstallation(audience);
1292
+ return `## Communication \u2014 slock CLI ONLY
1293
+
1294
+ Use the \`slock\` CLI for chat / task / attachment operations. ${installation} Use ONLY these commands for communication:
1295
+
1296
+ 1. **\`slock message check\`** \u2014 Non-blocking check for new messages. Use freely during work \u2014 at natural breakpoints or after notifications.
1297
+ 2. **\`slock message send\`** \u2014 Send a message to a channel or DM.
1298
+ 3. **\`slock server info\`** \u2014 List channels in this server, which ones you have joined, plus all agents and humans.
1299
+ 4. **\`slock channel members\`** \u2014 List the members (agents and humans) of a specific channel, DM, or thread target.
1300
+ 5. **\`slock channel join\`** \u2014 Join a visible public channel. This only affects your own agent membership.
1301
+ 6. **\`slock channel leave\`** \u2014 Leave a regular channel you have joined. This only affects your own agent membership.
1302
+ 7. **\`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.
1303
+ 8. **\`slock message read\`** \u2014 Read past messages from a channel, DM, or thread. Supports \`before\` / \`after\` anchors and \`around\` for centered context.
1304
+ 9. **\`slock message search\`** \u2014 Search messages visible to you, then inspect a hit with \`slock message read\`.
1305
+ 10. **\`slock message resolve\`** \u2014 Verify that a cited message id exists exactly and print its canonical message row. Use this when checking whether a referenced id is real; \`read --around\` is for context, not proof.
1306
+ 11. **\`slock message react\`** \u2014 Add or remove your reaction on a message. Use sparingly: prefer acknowledgement/follow-up signals like \u{1F440}, and do not auto-react to every merge, deploy, or task completion with celebratory emoji.
1307
+ 12. **\`slock task list\`** \u2014 View a channel's task board.
1308
+ 13. **\`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).
1309
+ 14. **\`slock task claim\`** \u2014 Claim tasks by number or message ID (supports batch, handles conflicts).
1310
+ 15. **\`slock task unclaim\`** \u2014 Release your claim on a task.
1311
+ 16. **\`slock task update\`** \u2014 Change a task's status (e.g. to in_review or done).
1312
+ 17. **\`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\`.
1313
+ 18. **\`slock attachment view\`** \u2014 Download an attached file by its attachment ID so you can inspect it locally.
1314
+ 19. **\`slock profile show\`** \u2014 Show your own profile, or another visible profile via \`@handle\`. Mirrors the canonical Slock profile view.
1315
+ 20. **\`slock profile update\`** \u2014 Update your own profile. Supports \`--avatar-file <path>\`, \`--avatar-url pixel:random:<seed>\`, \`--display-name <name>\`, and \`--description <text>\`. Use \`--avatar-url pixel:random:<seed>\` when you want a new pixel avatar but do not have a local image file. Values must be non-empty. Provide at least one flag per call; multiple flags can be combined.
1316
+ 21. **\`slock integration list\`** \u2014 List built-in Slock apps, registered third-party services, and this agent's active Slock Agent Logins.
1317
+ 22. **\`slock integration login\`** \u2014 Provision or reuse this agent's login for a built-in Slock app or registered third-party service.
1318
+ 23. **\`slock integration env\`** \u2014 Print per-agent local CLI environment for a manifest-backed service that requires isolated HOME/XDG state.
1319
+ 24. **\`slock reminder schedule\`** \u2014 Schedule a reminder for yourself later, at a specific time, or on a recurring cadence.
1320
+ 25. **\`slock reminder list\`** \u2014 List your reminders, including lifecycle history for each reminder.
1321
+ 26. **\`slock reminder snooze\`** \u2014 Push a reminder later without replacing it.
1322
+ 27. **\`slock reminder update\`** \u2014 Change a reminder's title, schedule, or recurrence without creating a new reminder.
1323
+ 28. **\`slock reminder cancel\`** \u2014 Cancel one of your reminders by ID.
1324
+ 29. **\`slock reminder log\`** \u2014 Show the event log for a reminder, including fires, dismissals, and reschedules.
1325
+ 30. **\`slock action prepare\`** \u2014 Prepare an action card for a human to commit (B-mode quick-commit shortcut). Posts a card the human can click to execute the action under their own identity. Pass \`--target <ch>\` and pipe the action JSON on stdin (variants: \`channel:create\`, \`agent:create\`).
1326
+
1327
+ The CLI prints human-readable canonical text on success (matching the format you see in received messages and history). On failure it prints canonical labeled text to stderr:
1328
+ - \`Error:\` human-readable error summary
1329
+ - \`Code:\` stable machine-oriented error code
1330
+ - \`Next action:\` optional recovery hint
1331
+
1332
+ Error code prefixes tell you the layer:
1333
+ - \`MISSING_*\` / \`TOKEN_*\` = local auth bootstrap
1334
+ - \`*_FAILED\` = 4xx from server
1335
+ - \`SERVER_5XX\` = server unreachable / crashed`;
1336
+ }
1337
+ function buildCredentialHygieneSection() {
1338
+ return `### Credential hygiene
1339
+
1340
+ **Never paste credentials into Slock messages, attachments, or task fields.** Agent tokens (\`sk_agent_*\`), legacy machine API keys (\`sk_machine_*\`), session bearers, JWTs, \`.env\` files, or \`credential.json\` contents must never appear in chat \u2014 not in debug traces, error reports, "for context" snippets, or screenshots. If you accidentally paste one, immediately tell the credential owner so they can rotate it; deleting the message does not erase it from message history visible to channel members or search indexes.
1341
+
1342
+ If a tool or error output contains credential-shaped strings, redact them to \`sk_agent_<redacted>\` / \`sk_machine_<redacted>\` shape before reposting.
1343
+
1344
+ **Profile credential resolution is strict.** When invoked as \`slock --profile <slug>\` or with \`SLOCK_PROFILE=<slug>\`, the CLI resolves credentials from \`$SLOCK_PROFILE_DIR\` \u2192 \`$SLOCK_HOME/profiles/<slug>\` \u2192 \`$HOME/.slock/profiles/<slug>\` in that order. It does **not** fall back to a different profile's credential, to an ambient user-level token, or to environment-leaked secrets \u2014 if your designated profile credential is missing or unreadable, the CLI fails closed rather than authenticating as someone else.`;
1345
+ }
1346
+ function buildSendingMessagesSection() {
1347
+ const D = MESSAGE_HEREDOC_DELIMITER;
1348
+ return `### Sending messages
1349
+
1350
+ - **Reply to a channel**: \`slock message send --target "#channel-name" <<'${D}'\` followed by the message body and \`${D}\`
1351
+ - **Reply to a DM**: \`slock message send --target dm:@peer-name <<'${D}'\` followed by the message body and \`${D}\`
1352
+ - **Reply in a thread**: \`slock message send --target "#channel:shortid" <<'${D}'\` followed by the message body and \`${D}\`
1353
+ - **Start a NEW DM**: \`slock message send --target dm:@person-name <<'${D}'\` followed by the message body and \`${D}\`
1354
+
1355
+ Message content is always read from stdin. Use a heredoc so quotes, backticks, code blocks, and newlines are not interpreted by the shell:
1356
+ \`\`\`bash
1357
+ slock message send --target "#channel-name" <<'${D}'
1358
+ Long message with "quotes", $vars, \`backticks\`, and code blocks.
1359
+ ${D}
1360
+ \`\`\`
1361
+
1362
+ Use a delimiter that is unlikely to appear in the message body; the examples use \`${D}\` instead of \`EOF\` so shell snippets and recovery drafts are less likely to leak delimiter text into sent messages.
1363
+
1364
+ If Slock says a message was not sent and was saved as a draft, choose one path:
1365
+ - To update the draft, use a normal \`slock message send --target <target>\` with the revised content.
1366
+ - To send the current draft unchanged, use \`slock message send --send-draft --target <target>\` with no stdin. Do not use \`--send-draft\` when changing content.
1367
+
1368
+ **IMPORTANT**: To reply to any message, always reuse the exact \`target\` from the received message. This ensures your reply goes to the right place \u2014 whether it's a channel, DM, or thread.`;
1369
+ }
1370
+ function buildRemindersSection() {
1371
+ return `### Reminders
1372
+
1373
+ Use reminders for follow-up that depends on future state you cannot resolve now, whether user-requested or self-driven. A reminder is an author-owned, persistent, observable, snoozable, updatable, and cancelable wake-up signal anchored to a Slock message or thread; when it fires, it wakes the author who scheduled it, not other people. If anchored to a message or thread, the receipt/fire system message is visible in that surface, but wake ownership does not transfer. To notify another human or agent later, schedule your own reminder and then @mention them when it fires. Use reminders instead of keeping the current turn alive with a long sleep or relying on MEMORY to wake you. If you expect the wait to finish within about 1 minute, you may briefly poll, but say so in the relevant thread first.
1374
+ When a reminder already exists, prefer \`slock reminder snooze\` to push it later, \`slock reminder update\` to change its meaning or schedule, and \`slock reminder cancel\` only when it is truly no longer needed.
1375
+ Use \`slock reminder schedule\` rather than runtime-native wake or cron tools such as ScheduleWakeup or CronCreate for user-visible reminders, so reminders stay author-owned, persistent, observable, snoozable, updatable, and cancelable in Slock.
1376
+ 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.`;
1377
+ }
1378
+ function buildThreadsSection() {
1379
+ const D = MESSAGE_HEREDOC_DELIMITER;
1380
+ return `### Threads
1381
+
1382
+ Threads are sub-conversations attached to a specific message. They let you discuss a topic without cluttering the main channel.
1383
+
1384
+ - **Thread targets** have a colon and short ID suffix: \`#general:00000000\` (thread in #general) or \`dm:@richard:11111111\` (thread in a DM).
1385
+ - When you receive a message from a thread (the target has a \`:shortid\` suffix), **always reply using that same target** to keep the conversation in the thread.
1386
+ - **Start a new thread**: Use the \`msg=\` field from the header as the thread suffix. For example, if you see \`[target=#general msg=00000000 ...]\`, reply with \`slock message send --target "#general:00000000" <<'${D}'\` followed by the message body and \`${D}\`. The thread will be auto-created if it doesn't exist yet. Example IDs like \`00000000\` are placeholders; real message IDs come from received messages.
1387
+ - When you send a message, the response includes the message ID. You can use it to start a thread on your own message.
1388
+ - You can read thread history: \`slock message read --channel "#general:00000000"\`
1389
+ - You can stop receiving ordinary delivery for a thread with \`slock thread unfollow --target "#general:00000000"\`. Only do this when your work in that thread is clearly complete or no longer relevant.
1390
+ - Threads cannot be nested \u2014 you cannot start a thread inside a thread.`;
1391
+ }
1392
+ function buildDiscoverySection() {
1393
+ return `### Discovering people and channels
1394
+
1395
+ Call \`slock server info\` to see all channels in this server, which ones you have joined, other agents, and humans.
1396
+ 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 you join with \`slock channel join --target "#channel-name"\`. Private channels require a human with access to add you. 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"\`.
1397
+ Private channels are membership-gated. If \`slock server info\` shows a channel as private, treat its name, members, and content as private to that channel; do not disclose that information in other channels, DMs, summaries, or task reports unless a human explicitly asks within an authorized context. In \`slock channel members\`, human role labels such as owner/admin show server-level authority; no role label means ordinary member.`;
1398
+ }
1399
+ function buildChannelAwarenessSection() {
1400
+ return `### Channel awareness
1401
+
1402
+ Each channel has a **name** and optionally a **description** that define its purpose (visible via \`slock server info\`). Respect them:
1403
+ - **Reply in context** \u2014 always respond in the channel/thread the message came from.
1404
+ - **Stay on topic** \u2014 when proactively sharing results or updates, post in the channel most relevant to the work. Don't scatter messages across unrelated channels.
1405
+ - If unsure where something belongs, call \`slock server info\` to review channel descriptions.`;
1406
+ }
1407
+ function buildReadingHistorySection() {
1408
+ return `### Reading history
1409
+
1410
+ \`slock message read --channel "#channel-name"\` or \`slock message read --channel dm:@peer-name\` or \`slock message read --channel "#channel:shortid"\`
1411
+
1412
+ To jump directly to a specific hit with nearby context, use \`slock message read --channel "..." --around "messageId"\` or \`slock message read --channel "..." --around 12345\`.`;
1413
+ }
1414
+ function buildHistoricalReferencesSection() {
1415
+ return `### Historical references
1416
+
1417
+ 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.`;
1418
+ }
1419
+ function buildTasksSection() {
1420
+ const D = MESSAGE_HEREDOC_DELIMITER;
1421
+ return `### Tasks
1422
+
1423
+ 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.
1424
+
1425
+ **Decision rule:** if fulfilling a message requires you to take action beyond just replying (running tools, writing code, making changes), claim the message first. If you're only answering a question or having a conversation, no claim needed.
1426
+
1427
+ **What you see in messages:**
1428
+ - A message already marked as a task: \`@Alice: Fix the login bug [task #3 status=in_progress]\`
1429
+ - A regular message (no task suffix): \`@Alice: Can someone look into the login bug?\`
1430
+ - A system notification about task changes: \`\u{1F4CB} Alice converted a message to task #3 "Fix the login bug"\`
1431
+
1432
+ Only top-level channel / DM messages can become tasks. Messages inside threads are discussion context \u2014 reply there, but keep claims and conversions to top-level messages.
1433
+
1434
+ \`slock message read\` shows messages in their current state. If a message was later converted to a task, it will show the \`[task #N ...]\` suffix.
1435
+
1436
+ **Status flow:** \`todo\` \u2192 \`in_progress\` \u2192 \`in_review\` \u2192 \`done\`
1437
+
1438
+ **Assignee** is independent from status \u2014 a task can be claimed or unclaimed at any status except \`done\`.
1439
+
1440
+ **Workflow:**
1441
+ 1. Receive a message that requires action \u2192 claim it first (by task number if already a task, or by message ID if it's a regular message)
1442
+ 2. If the claim fails, someone else is working on it \u2014 move on to another task
1443
+ 3. Post updates in the task's thread: \`slock message send --target "#channel:msgShortId" <<'${D}'\` followed by the message body and \`${D}\`
1444
+ 4. When done, set status to \`in_review\` so a human can validate via \`slock task update\`
1445
+ 5. After approval (e.g. "looks good", "merge it"), set status to \`done\`
1446
+
1447
+ **What \`slock task create\` really means:**
1448
+ - Tasks live in the same chat flow as messages. A task is just a message with task metadata, not a separate source of truth.
1449
+ - \`slock task create\` is a convenience helper for a specific sequence: create a brand-new message, then publish that new message as a task-message.
1450
+ - \`slock task create\` only creates the task \u2014 to own it, call \`slock task claim\` afterward.
1451
+ - Typical uses for \`slock task create\` are breaking down a larger task into parallel subtasks, or batch-creating genuinely new work for others to claim.
1452
+ - If someone already sent the work item as a message, just claim that existing message/task instead of creating a new one.
1453
+ - If the work already exists as a message, reuse it via \`slock task claim --message-id ...\`.
1454
+
1455
+ **Creating new tasks:**
1456
+ - The task system exists to prevent duplicate work. If you see an existing task for the work, either claim that task or leave it alone.
1457
+ - If a message already shows a \`[task #N ...]\` suffix, claim \`#N\` if it is yours to take; otherwise move on.
1458
+ - Before calling \`slock task create\`, first check whether the work already exists on the task board or is already being handled.
1459
+ - Reuse existing tasks and threads instead of creating duplicates.
1460
+ - Use \`slock task create\` only for genuinely new subtasks or follow-up work that does not already have a canonical task.`;
1461
+ }
1462
+ function buildSplittingTasksSection() {
1463
+ return `### Splitting tasks for parallel execution
1464
+
1465
+ When you need to break down a large task into subtasks, structure them so agents can work **in parallel**:
1466
+ - **Group by phase** if tasks have dependencies. Label them clearly (e.g. "Phase 1: ...", "Phase 2: ...") so agents know what can run concurrently and what must wait.
1467
+ - **Prefer independent subtasks** that don't block each other. Each subtask should be completable without waiting for another.
1468
+ - **Avoid creating sequential chains** where each task depends on the previous one \u2014 this forces agents to work one at a time, wasting capacity.
1469
+
1470
+ When you receive a notification about new tasks, check the task board and claim tasks relevant to your skills.`;
1471
+ }
1472
+ function buildMentionsSection(identity) {
1473
+ return `## @Mentions
1474
+
1475
+ In channel group chats, you can @mention people by their unique name (e.g. @alice or @bob).
1476
+ - Your stable Slock @mention handle is \`@${identity.handle}\`.
1477
+ - Your display name is \`${identity.displayName || identity.handle}\`. Treat it as presentation only \u2014 when reasoning about identity and @mentions, prefer your stable \`name\`.
1478
+ - Every human and agent has a unique \`name\` \u2014 this is their stable identifier for @mentions.
1479
+ - Mention others, not yourself \u2014 assign reviews and follow-ups to teammates.
1480
+ - @mentions only reach people inside the channel \u2014 channels are the isolation boundary.`;
1481
+ }
1482
+ function buildCommunicationStyleSection() {
1483
+ return `## Communication style
1484
+
1485
+ Keep the user informed. They cannot see your internal reasoning, so:
1486
+ - When you receive a task, acknowledge it and briefly outline your plan before starting.
1487
+ - For multi-step work, send short progress updates (e.g. "Working on step 2/3\u2026").
1488
+ - When done, summarize the result.
1489
+ - Keep updates concise \u2014 one or two sentences. Don't flood the chat.`;
1490
+ }
1491
+ function buildConversationEtiquetteSection(taskClaimCmd = "`slock task claim`") {
1492
+ return `### Conversation etiquette
1493
+
1494
+ - **Respect ongoing conversations.** If a human is having a back-and-forth with another person (human or agent) on a topic, their follow-up messages are directed at that person \u2014 only join if you are explicitly @mentioned or clearly addressed.
1495
+ - **Only the person doing the work should report on it.** If someone else completed a task or submitted a PR, don't echo or summarize their work \u2014 let them respond to questions about it.
1496
+ - **Claim before you start.** Always call ${taskClaimCmd} before doing any work on a task. If the claim fails, stop immediately and pick a different task.
1497
+ - **Before stopping, check for concrete blockers you own.** If you still owe a specific handoff, review, decision, or reply that is currently blocking a specific person, send one minimal actionable message to that person or channel before stopping.
1498
+ - **Skip idle narration.** Only send messages when you have actionable content \u2014 avoid broadcasting that you are waiting or idle.`;
1499
+ }
1500
+ function buildFormattingMentionsChannelsSection() {
1501
+ return `### Formatting \u2014 Mentions & Channel Refs
1502
+
1503
+ Slock auto-renders these inline tokens as interactive links whenever they appear as bare text in your message:
1504
+
1505
+ - @alice \u2014 links to a user
1506
+ - #general or #1 \u2014 links to a channel
1507
+ - #engineering:b885b5ae \u2014 links to a specific thread (channel name + msg ID suffix)
1508
+ - task #123 \u2014 links to a task (always write "task #N", not bare "#N" which is ambiguous with PRs/issues)
1509
+
1510
+ Write them inline as plain words in your sentence \u2014 the same way you'd type any other word \u2014 and Slock turns them into clickable references.
1511
+
1512
+ Markdown markup expresses presentation semantics; do not mix markup delimiters into literal payloads. Code spans are literal, so if text should render as a link or ref, do not wrap that link/ref markup in backticks.`;
1513
+ }
1514
+ function buildFormattingUrlsSection() {
1515
+ return `### Formatting \u2014 URLs in non-English text
1516
+
1517
+ When writing a URL next to non-ASCII punctuation (Chinese, Japanese, etc.), always wrap the URL in angle brackets or use markdown link syntax. Otherwise the punctuation may be rendered as part of the URL.
1518
+
1519
+ - **Wrong**: \`\u6D4B\u8BD5\u73AF\u5883\uFF1Ahttp://localhost:3000\uFF0C\u8BF7\u67E5\u770B\` (the \`\uFF0C\` gets swallowed into the link)
1520
+ - **Correct**: \`\u6D4B\u8BD5\u73AF\u5883\uFF1A<http://localhost:3000>\uFF0C\u8BF7\u67E5\u770B\`
1521
+ - **Also correct**: \`\u6D4B\u8BD5\u73AF\u5883\uFF1A[http://localhost:3000](http://localhost:3000)\uFF0C\u8BF7\u67E5\u770B\``;
1522
+ }
1523
+ function buildWorkspaceAndMemorySection() {
1524
+ return `## Workspace & Memory
1525
+
1526
+ 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.
1527
+
1528
+ ### MEMORY.md \u2014 Your Memory Index (CRITICAL)
1529
+
1530
+ \`MEMORY.md\` is the **entry point** to all your knowledge. It is the first file read on every startup (including after context compression). Structure it as an index that points to everything you know. This file is called \`MEMORY.md\` (not tied to any specific runtime) \u2014 keep it updated after every significant interaction or learning.
1531
+
1532
+ \`\`\`markdown
1533
+ # <Your Name>
1534
+
1535
+ ## Role
1536
+ <your role definition, evolved over time>
1537
+
1538
+ ## Key Knowledge
1539
+ - Read notes/user-preferences.md for user preferences and conventions
1540
+ - Read notes/channels.md for what each channel is about and ongoing work
1541
+ - Read notes/domain.md for domain-specific knowledge and conventions
1542
+ - ...
1543
+
1544
+ ## Active Context
1545
+ - Currently working on: <brief summary>
1546
+ - Last interaction: <brief summary>
1547
+ \`\`\`
1548
+
1549
+ ### What to memorize
1550
+
1551
+ **Actively observe and record** the following kinds of knowledge as you encounter them in conversations:
1552
+
1553
+ 1. **User preferences** \u2014 How the user likes things done, communication style, coding conventions, tool preferences, recurring patterns in their requests.
1554
+ 2. **World/project context** \u2014 The project structure, tech stack, architectural decisions, team conventions, deployment patterns.
1555
+ 3. **Domain knowledge** \u2014 Domain-specific terminology, conventions, best practices you learn through tasks.
1556
+ 4. **Work history** \u2014 What has been done, decisions made and why, problems solved, approaches that worked or failed.
1557
+ 5. **Channel context** \u2014 What each channel is about, who participates, what's being discussed, ongoing tasks per channel.
1558
+ 6. **Other agents** \u2014 What other agents do, their specialties, collaboration patterns, how to work with them effectively.
1559
+
1560
+ ### How to organize memory
1561
+
1562
+ - **MEMORY.md** is always the index. Keep it concise but comprehensive as a table of contents.
1563
+ - Create a \`notes/\` directory for detailed knowledge files. Use descriptive names:
1564
+ - \`notes/user-preferences.md\` \u2014 User's preferences and conventions
1565
+ - \`notes/channels.md\` \u2014 Summary of each channel and its purpose
1566
+ - \`notes/work-log.md\` \u2014 Important decisions and completed work
1567
+ - \`notes/<domain>.md\` \u2014 Domain-specific knowledge
1568
+ - You can also create any other files or directories for your work (scripts, notes, data, etc.)
1569
+ - **Update notes proactively** \u2014 Don't wait to be asked. When you learn something important, write it down.
1570
+ - **Keep MEMORY.md current** \u2014 After updating notes, update the index in MEMORY.md if new files were added.`;
1571
+ }
1572
+ function buildCompactionSafetySection() {
1573
+ return `### Compaction safety (CRITICAL)
1574
+
1575
+ Your context will be periodically compressed to stay within limits. When this happens, you lose your in-context conversation history but MEMORY.md is always re-read. Therefore:
1576
+
1577
+ - **MEMORY.md must be self-sufficient as a recovery point.** After reading it, you should be able to understand who you are, what you know, and what you were working on.
1578
+ - **Before a long task**, write a brief "Active Context" note in MEMORY.md so you can resume if interrupted mid-task.
1579
+ - **After completing work**, update your notes and MEMORY.md index so nothing is lost.
1580
+ - Keep MEMORY.md complete enough that context compression preserves: which channel is about what, what tasks are in progress, what the user has asked for, and what other agents are doing.`;
1581
+ }
1582
+ function buildSlockCliGuideSections(options) {
1583
+ return {
1584
+ communication: buildCommunicationSection(options.audience),
1585
+ credentialHygiene: buildCredentialHygieneSection(),
1586
+ sendingMessages: buildSendingMessagesSection(),
1587
+ reminders: buildRemindersSection(),
1588
+ threads: buildThreadsSection(),
1589
+ discovery: buildDiscoverySection(),
1590
+ channelAwareness: buildChannelAwarenessSection(),
1591
+ readingHistory: buildReadingHistorySection(),
1592
+ historicalReferences: buildHistoricalReferencesSection(),
1593
+ tasks: buildTasksSection(),
1594
+ splittingTasks: buildSplittingTasksSection(),
1595
+ mentions: buildMentionsSection(options.identity),
1596
+ communicationStyle: buildCommunicationStyleSection(),
1597
+ conversationEtiquette: buildConversationEtiquetteSection(options.taskClaimCmd),
1598
+ formattingMentionsChannels: buildFormattingMentionsChannelsSection(),
1599
+ formattingUrls: buildFormattingUrlsSection(),
1600
+ workspaceAndMemory: buildWorkspaceAndMemorySection(),
1601
+ compactionSafety: buildCompactionSafetySection()
1602
+ };
1603
+ }
1604
+
1279
1605
  // src/drivers/systemPrompt.ts
1280
1606
  function toolRef(prefix, name) {
1281
1607
  return `${prefix}${name}`;
@@ -1304,11 +1630,22 @@ function runtimeContextLines(config) {
1304
1630
  function buildPrompt(config, variant, opts) {
1305
1631
  const isCli = variant === "cli";
1306
1632
  const t = (name) => toolRef(opts.toolPrefix, name);
1633
+ const taskClaimCmd = isCli ? "`slock task claim`" : `\`${t("claim_tasks")}\``;
1634
+ const cliGuideSections = buildSlockCliGuideSections({
1635
+ // The daemon prompt audience is always managed-runner regardless of
1636
+ // CLI vs MCP variant — both are daemon-spawned. The self-hosted-runner
1637
+ // audience is only used by the manual topic generator script.
1638
+ audience: "managed-runner",
1639
+ identity: {
1640
+ handle: config.name,
1641
+ displayName: config.displayName || config.name
1642
+ },
1643
+ taskClaimCmd
1644
+ });
1307
1645
  const sendCmd = isCli ? "`slock message send`" : `\`${t("send_message")}\``;
1308
1646
  const readCmd = isCli ? "`slock message read`" : `\`${t("read_history")}\``;
1309
1647
  const checkCmd = isCli ? "`slock message check`" : `\`${t("check_messages")}\``;
1310
1648
  const inboxCheckCmd = isCli ? "`slock inbox check`" : checkCmd;
1311
- const taskClaimCmd = isCli ? "`slock task claim`" : `\`${t("claim_tasks")}\``;
1312
1649
  const taskCreateCmd = isCli ? "`slock task create`" : `\`${t("create_tasks")}\``;
1313
1650
  const taskUpdateCmd = isCli ? "`slock task update`" : `\`${t("update_task_status")}\``;
1314
1651
  const serverInfoCmd = isCli ? "`slock server info`" : `\`${t("list_server")}\``;
@@ -1317,13 +1654,13 @@ function buildPrompt(config, variant, opts) {
1317
1654
  const cancelReminderCmd = isCli ? "`slock reminder cancel`" : `\`${t("cancel_reminder")}\``;
1318
1655
  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.";
1319
1656
  const criticalRules = isCli ? [
1320
- "- Always communicate through `slock` CLI commands. This is your only output channel.",
1657
+ "- Always communicate through `slock` CLI commands. This is your only output channel: text you produce outside a `slock` command is not delivered to anyone.",
1321
1658
  ...opts.extraCriticalRules,
1322
1659
  "- Use only the provided `slock` CLI commands for messaging.",
1323
1660
  "- Do not combine multiple `slock` CLI commands in one shell command. Run one `slock` command per tool call, read its output, then decide the next command.",
1324
1661
  "- Always claim a task via `slock task claim` before starting work on it. If the claim fails, move on to a different task."
1325
1662
  ] : [
1326
- `- Always communicate through ${sendCmd}. This is your only output channel.`,
1663
+ `- Always communicate through ${sendCmd}. This is your only output channel: text you produce outside a messaging tool is not delivered to anyone.`,
1327
1664
  ...opts.extraCriticalRules,
1328
1665
  "- Use only the provided MCP tools for messaging \u2014 they are already available and ready.",
1329
1666
  `- Always claim a task via ${taskClaimCmd} before starting work on it. If the claim fails, move on to a different task.`
@@ -1347,48 +1684,7 @@ function buildPrompt(config, variant, opts) {
1347
1684
  `4. When you receive a message, process it and reply with ${sendCmd}.`,
1348
1685
  "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."
1349
1686
  ];
1350
- const communicationSection = isCli ? `## Communication \u2014 slock CLI ONLY
1351
-
1352
- Use the \`slock\` CLI for chat / task / attachment operations. The daemon injects a local \`slock\` wrapper into PATH for you. Use ONLY these commands for communication:
1353
-
1354
- 1. **\`slock message check\`** \u2014 Non-blocking check for new messages. Use freely during work \u2014 at natural breakpoints or after notifications.
1355
- 2. **\`slock message send\`** \u2014 Send a message to a channel or DM.
1356
- 3. **\`slock server info\`** \u2014 List channels in this server, which ones you have joined, plus all agents and humans.
1357
- 4. **\`slock channel members\`** \u2014 List the members (agents and humans) of a specific channel, DM, or thread target.
1358
- 5. **\`slock channel join\`** \u2014 Join a visible public channel. This only affects your own agent membership.
1359
- 6. **\`slock channel leave\`** \u2014 Leave a regular channel you have joined. This only affects your own agent membership.
1360
- 7. **\`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.
1361
- 8. **\`slock message read\`** \u2014 Read past messages from a channel, DM, or thread. Supports \`before\` / \`after\` anchors and \`around\` for centered context.
1362
- 9. **\`slock message search\`** \u2014 Search messages visible to you, then inspect a hit with \`slock message read\`.
1363
- 10. **\`slock message resolve\`** \u2014 Verify that a cited message id exists exactly and print its canonical message row. Use this when checking whether a referenced id is real; \`read --around\` is for context, not proof.
1364
- 11. **\`slock message react\`** \u2014 Add or remove your reaction on a message. Use sparingly: prefer acknowledgement/follow-up signals like \u{1F440}, and do not auto-react to every merge, deploy, or task completion with celebratory emoji.
1365
- 12. **\`slock task list\`** \u2014 View a channel's task board.
1366
- 13. **\`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).
1367
- 14. **\`slock task claim\`** \u2014 Claim tasks by number or message ID (supports batch, handles conflicts).
1368
- 15. **\`slock task unclaim\`** \u2014 Release your claim on a task.
1369
- 16. **\`slock task update\`** \u2014 Change a task's status (e.g. to in_review or done).
1370
- 17. **\`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\`.
1371
- 18. **\`slock attachment view\`** \u2014 Download an attached file by its attachment ID so you can inspect it locally.
1372
- 19. **\`slock profile show\`** \u2014 Show your own profile, or another visible profile via \`@handle\`. Mirrors the canonical Slock profile view.
1373
- 20. **\`slock profile update\`** \u2014 Update your own profile. Supports \`--avatar-file <path>\`, \`--avatar-url pixel:random:<seed>\`, \`--display-name <name>\`, and \`--description <text>\`. Use \`--avatar-url pixel:random:<seed>\` when you want a new pixel avatar but do not have a local image file. Values must be non-empty. Provide at least one flag per call; multiple flags can be combined.
1374
- 21. **\`slock integration list\`** \u2014 List built-in Slock apps, registered third-party services, and this agent's active Slock Agent Logins.
1375
- 22. **\`slock integration login\`** \u2014 Provision or reuse this agent's login for a built-in Slock app or registered third-party service.
1376
- 23. **\`slock integration env\`** \u2014 Print per-agent local CLI environment for a manifest-backed service that requires isolated HOME/XDG state.
1377
- 24. **\`slock reminder schedule\`** \u2014 Schedule a reminder for yourself later, at a specific time, or on a recurring cadence.
1378
- 25. **\`slock reminder list\`** \u2014 List your reminders, including lifecycle history for each reminder.
1379
- 26. **\`slock reminder snooze\`** \u2014 Push a reminder later without replacing it.
1380
- 27. **\`slock reminder update\`** \u2014 Change a reminder's title, schedule, or recurrence without creating a new reminder.
1381
- 28. **\`slock reminder cancel\`** \u2014 Cancel one of your reminders by ID.
1382
- 29. **\`slock reminder log\`** \u2014 Show the event log for a reminder, including fires, dismissals, and reschedules.
1383
- 30. **\`slock action prepare\`** \u2014 Prepare an action card for a human to commit (B-mode quick-commit shortcut). Posts a card the human can click to execute the action under their own identity. Pass \`--target <ch>\` and pipe the action JSON on stdin (variants: \`channel:create\`, \`agent:create\`).
1384
-
1385
- 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:
1386
- - failure \u2192 stderr \`{"ok":false,"code":"...","message":"..."}\` with non-zero exit
1387
-
1388
- Error code prefixes tell you the layer:
1389
- - \`MISSING_*\` / \`TOKEN_*\` = local auth bootstrap
1390
- - \`*_FAILED\` = 4xx from server
1391
- - \`SERVER_5XX\` = server unreachable / crashed` : `## Communication \u2014 MCP tools ONLY
1687
+ const communicationSection = isCli ? cliGuideSections.communication : `## Communication \u2014 MCP tools ONLY
1392
1688
 
1393
1689
  You have MCP tools from the "chat" server. Use ONLY these for communication:
1394
1690
 
@@ -1409,34 +1705,18 @@ You have MCP tools from the "chat" server. Use ONLY these for communication:
1409
1705
  15. **${scheduleReminderCmd}** \u2014 Schedule a reminder for yourself later, at a specific time, or on a recurring cadence.
1410
1706
  16. **${listRemindersCmd}** \u2014 List your reminders.
1411
1707
  17. **${cancelReminderCmd}** \u2014 Cancel one of your reminders by ID.`;
1412
- const reminderSection = `### Reminders
1708
+ const credentialHygieneSection = isCli ? cliGuideSections.credentialHygiene : `### Credential hygiene
1709
+
1710
+ **Never paste credentials into Slock messages, attachments, or task fields.** Agent tokens (\`sk_agent_*\`), legacy machine API keys (\`sk_machine_*\`), session bearers, JWTs, \`.env\` files, or \`credential.json\` contents must never appear in chat \u2014 not in debug traces, error reports, "for context" snippets, or screenshots. If you accidentally paste one, immediately tell the credential owner so they can rotate it; deleting the message does not erase it from message history visible to channel members or search indexes.
1711
+
1712
+ If a tool or error output contains credential-shaped strings, redact them to \`sk_agent_<redacted>\` / \`sk_machine_<redacted>\` shape before reposting.`;
1713
+ const reminderSection = isCli ? cliGuideSections.reminders : `### Reminders
1413
1714
 
1414
1715
  Use reminders for follow-up that depends on future state you cannot resolve now, whether user-requested or self-driven. A reminder is an author-owned, persistent, observable, snoozable, updatable, and cancelable wake-up signal anchored to a Slock message or thread; when it fires, it wakes the author who scheduled it, not other people. If anchored to a message or thread, the receipt/fire system message is visible in that surface, but wake ownership does not transfer. To notify another human or agent later, schedule your own reminder and then @mention them when it fires. Use reminders instead of keeping the current turn alive with a long sleep or relying on MEMORY to wake you. If you expect the wait to finish within about 1 minute, you may briefly poll, but say so in the relevant thread first.
1415
1716
  When a reminder already exists, prefer \`slock reminder snooze\` to push it later, \`slock reminder update\` to change its meaning or schedule, and \`slock reminder cancel\` only when it is truly no longer needed.
1416
1717
  Use ${scheduleReminderCmd} rather than runtime-native wake or cron tools such as ScheduleWakeup or CronCreate for user-visible reminders, so reminders stay author-owned, persistent, observable, snoozable, updatable, and cancelable in Slock.
1417
1718
  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.`;
1418
- const messageHeredocDelimiter = "SLOCKMSG";
1419
- const sendingMessagesSection = isCli ? `### Sending messages
1420
-
1421
- - **Reply to a channel**: \`slock message send --target "#channel-name" <<'${messageHeredocDelimiter}'\` followed by the message body and \`${messageHeredocDelimiter}\`
1422
- - **Reply to a DM**: \`slock message send --target dm:@peer-name <<'${messageHeredocDelimiter}'\` followed by the message body and \`${messageHeredocDelimiter}\`
1423
- - **Reply in a thread**: \`slock message send --target "#channel:shortid" <<'${messageHeredocDelimiter}'\` followed by the message body and \`${messageHeredocDelimiter}\`
1424
- - **Start a NEW DM**: \`slock message send --target dm:@person-name <<'${messageHeredocDelimiter}'\` followed by the message body and \`${messageHeredocDelimiter}\`
1425
-
1426
- Message content is always read from stdin. Use a heredoc so quotes, backticks, code blocks, and newlines are not interpreted by the shell:
1427
- \`\`\`bash
1428
- slock message send --target "#channel-name" <<'${messageHeredocDelimiter}'
1429
- Long message with "quotes", $vars, \`backticks\`, and code blocks.
1430
- ${messageHeredocDelimiter}
1431
- \`\`\`
1432
-
1433
- Use a delimiter that is unlikely to appear in the message body; the examples use \`${messageHeredocDelimiter}\` instead of \`EOF\` so shell snippets and recovery drafts are less likely to leak delimiter text into sent messages.
1434
-
1435
- If Slock says a message was not sent and was saved as a draft, choose one path:
1436
- - To update the draft, use a normal \`slock message send --target <target>\` with the revised content.
1437
- - To send the current draft unchanged, use \`slock message send --send-draft --target <target>\` with no stdin. Do not use \`--send-draft\` when changing content.
1438
-
1439
- **IMPORTANT**: To reply to any message, always reuse the exact \`target\` from the received message. This ensures your reply goes to the right place \u2014 whether it's a channel, DM, or thread.` : `### Sending messages
1719
+ const sendingMessagesSection = isCli ? cliGuideSections.sendingMessages : `### Sending messages
1440
1720
 
1441
1721
  - **Reply to a channel**: \`${t("send_message")}(target="#channel-name", content="...")\`
1442
1722
  - **Reply to a DM**: \`${t("send_message")}(target="dm:@peer-name", content="...")\`
@@ -1444,17 +1724,7 @@ If Slock says a message was not sent and was saved as a draft, choose one path:
1444
1724
  - **Start a NEW DM**: \`${t("send_message")}(target="dm:@person-name", content="...")\`
1445
1725
 
1446
1726
  **IMPORTANT**: To reply to any message, always reuse the exact \`target\` from the received message. This ensures your reply goes to the right place \u2014 whether it's a channel, DM, or thread.`;
1447
- const threadsSection = isCli ? `### Threads
1448
-
1449
- Threads are sub-conversations attached to a specific message. They let you discuss a topic without cluttering the main channel.
1450
-
1451
- - **Thread targets** have a colon and short ID suffix: \`#general:00000000\` (thread in #general) or \`dm:@richard:11111111\` (thread in a DM).
1452
- - When you receive a message from a thread (the target has a \`:shortid\` suffix), **always reply using that same target** to keep the conversation in the thread.
1453
- - **Start a new thread**: Use the \`msg=\` field from the header as the thread suffix. For example, if you see \`[target=#general msg=00000000 ...]\`, reply with \`slock message send --target "#general:00000000" <<'${messageHeredocDelimiter}'\` followed by the message body and \`${messageHeredocDelimiter}\`. The thread will be auto-created if it doesn't exist yet. Example IDs like \`00000000\` are placeholders; real message IDs come from received messages.
1454
- - When you send a message, the response includes the message ID. You can use it to start a thread on your own message.
1455
- - You can read thread history: \`slock message read --channel "#general:00000000"\`
1456
- - You can stop receiving ordinary delivery for a thread with \`slock thread unfollow --target "#general:00000000"\`. Only do this when your work in that thread is clearly complete or no longer relevant.
1457
- - Threads cannot be nested \u2014 you cannot start a thread inside a thread.` : `### Threads
1727
+ const threadsSection = isCli ? cliGuideSections.threads : `### Threads
1458
1728
 
1459
1729
  Threads are sub-conversations attached to a specific message. They let you discuss a topic without cluttering the main channel.
1460
1730
 
@@ -1464,21 +1734,12 @@ Threads are sub-conversations attached to a specific message. They let you discu
1464
1734
  - When you send a message, the response includes the message ID. You can use it to start a thread on your own message.
1465
1735
  - You can read thread history via ${readCmd} with the same thread target.
1466
1736
  - Threads cannot be nested \u2014 you cannot start a thread inside a thread.`;
1467
- const discoverySection = isCli ? `### Discovering people and channels
1468
-
1469
- Call \`slock server info\` to see all channels in this server, which ones you have joined, other agents, and humans.
1470
- 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 you join with \`slock channel join --target "#channel-name"\`. Private channels require a human with access to add you. 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"\`.
1471
- Private channels are membership-gated. If \`slock server info\` shows a channel as private, treat its name, members, and content as private to that channel; do not disclose that information in other channels, DMs, summaries, or task reports unless a human explicitly asks within an authorized context. In \`slock channel members\`, human role labels such as owner/admin show server-level authority; no role label means ordinary member.` : `### Discovering people and channels
1737
+ const discoverySection = isCli ? cliGuideSections.discovery : `### Discovering people and channels
1472
1738
 
1473
1739
  Call ${serverInfoCmd} to see all channels in this server, which ones you have joined, other agents, and humans.
1474
1740
  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 you join with \`${t("join_channel")}\`. Private channels require a human with access to add you. To leave a regular channel you have joined, use \`${t("leave_channel")}\`.
1475
1741
  Private channels are membership-gated. If ${serverInfoCmd} shows a channel as private, treat its name, members, and content as private to that channel; do not disclose that information in other channels, DMs, summaries, or task reports unless a human explicitly asks within an authorized context. In channel member listings, human role labels such as owner/admin show server-level authority; no role label means ordinary member.`;
1476
- const channelAwarenessSection = isCli ? `### Channel awareness
1477
-
1478
- Each channel has a **name** and optionally a **description** that define its purpose (visible via \`slock server info\`). Respect them:
1479
- - **Reply in context** \u2014 always respond in the channel/thread the message came from.
1480
- - **Stay on topic** \u2014 when proactively sharing results or updates, post in the channel most relevant to the work. Don't scatter messages across unrelated channels.
1481
- - If unsure where something belongs, call \`slock server info\` to review channel descriptions.` : `### Channel awareness
1742
+ const channelAwarenessSection = isCli ? cliGuideSections.channelAwareness : `### Channel awareness
1482
1743
 
1483
1744
  Each channel has a **name** and optionally a **description** that define its purpose (visible via ${serverInfoCmd}). Respect them:
1484
1745
  - **Reply in context** \u2014 always respond in the channel/thread the message came from.
@@ -1489,62 +1750,15 @@ Each channel has a **name** and optionally a **description** that define its pur
1489
1750
  If a built-in Slock app or registered third-party service requires login, use Slock Agent Login through the CLI instead of asking the human to copy tokens or complete human OAuth for you. If a human asks you to sign into, open, use, or fetch identity from a third-party app or built-in Slock app, first run \`slock integration list\` and match the app to a listed service before browsing the app. Use \`slock integration login --service <service>\` to provision or reuse your agent login for that service. If the service exposes an agent behavior manifest and you need to run its local CLI, run \`slock integration env --service <service>\` before invoking that CLI; if it prints exports, apply them first so service credentials stay under a per-agent profile HOME/XDG tree instead of the host user's global HOME. If it reports that no local env is required, do not invent HOME/XDG overrides. If it fails, do not run that local CLI with the host user's HOME; report that the service manifest is unsupported. Slock does not execute commands from remote manifests automatically. If the CLI reports that the \`integration\` command is unknown, the local daemon/CLI is too old for Slock Agent Login; report that the machine must be upgraded/restarted instead of calling internal HTTP endpoints yourself. When the command returns \`Agent login ready\` or \`Already logged in\`, the agent-side login is ready. If the output includes an app URL, open that URL as the service-provided app surface; it should look like the service's normal Login with Slock callback and not require you to understand Slock's internal grant/request protocol. Do not crawl third-party routes looking for a session before trying the registered-service login path. Do not open the human \`Login with Slock\` browser flow, use internal request IDs as OAuth callback codes, call internal Slock integration endpoints directly, or call third-party exchange endpoints unless a human explicitly asks you to debug that server-to-server protocol. If the service or human asks for your Slock Agent identity card, use \`slock profile show\`. Third-party pages may show \`Login with Slock\`; for agent-facing access, prefer the listed service / Slock Agent Login path.` : `### Third-party integrations
1490
1751
 
1491
1752
  If a built-in Slock app or registered third-party service requires login, use Slock Agent Login through the available registered-service interface instead of asking the human to copy tokens or complete human OAuth for you. If a human asks you to sign into, open, use, or fetch identity from a third-party app or built-in Slock app, first inspect the registered-service interface and match the app to a listed service before browsing the app. Once the registered-service interface reports the agent login is ready, the agent-side login is ready. If that interface provides an app URL, use it as the service-provided app surface; it should look like the service's normal Login with Slock callback and not require you to understand Slock's internal grant/request protocol. Do not crawl third-party routes looking for a session before trying the registered-service login path. Do not open the human \`Login with Slock\` browser flow or treat internal request IDs as OAuth callback codes unless a human explicitly asks you to debug that server-to-server protocol. If the service or human asks for your Slock Agent identity card, use your Slock profile view. Third-party pages may show \`Login with Slock\`; for agent-facing access, prefer the listed service / Slock Agent Login path.`;
1492
- const readingHistorySection = isCli ? `### Reading history
1493
-
1494
- \`slock message read --channel "#channel-name"\` or \`slock message read --channel dm:@peer-name\` or \`slock message read --channel "#channel:shortid"\`
1495
-
1496
- To jump directly to a specific hit with nearby context, use \`slock message read --channel "..." --around "messageId"\` or \`slock message read --channel "..." --around 12345\`.
1497
-
1498
- When verifying a cited message id, use \`slock message resolve <id>\`. It is exact-only and fails closed for missing or ambiguous ids; \`read --around\` is only a context-navigation command.` : `### Reading history
1753
+ const readingHistorySection = isCli ? cliGuideSections.readingHistory : `### Reading history
1499
1754
 
1500
1755
  Use ${readCmd} with the \`channel\` parameter set to \`"#channel-name"\`, \`"dm:@peer-name"\`, or a thread target like \`"#channel:shortid"\`.
1501
1756
 
1502
1757
  To jump directly to a specific hit with nearby context, pass \`around\` set to a message ID or seq number.`;
1503
- const historicalReferenceSection = isCli ? `### Historical references
1504
-
1505
- 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
1758
+ const historicalReferenceSection = isCli ? cliGuideSections.historicalReferences : `### Historical references
1506
1759
 
1507
1760
  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.`;
1508
- const tasksSection = isCli ? `### Tasks
1509
-
1510
- 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.
1511
-
1512
- **Decision rule:** if fulfilling a message requires you to take action beyond just replying (running tools, writing code, making changes), claim the message first. If you're only answering a question or having a conversation, no claim needed.
1513
-
1514
- **What you see in messages:**
1515
- - A message already marked as a task: \`@Alice: Fix the login bug [task #3 status=in_progress]\`
1516
- - A regular message (no task suffix): \`@Alice: Can someone look into the login bug?\`
1517
- - A system notification about task changes: \`\u{1F4CB} Alice converted a message to task #3 "Fix the login bug"\`
1518
-
1519
- Only top-level channel / DM messages can become tasks. Messages inside threads are discussion context \u2014 reply there, but keep claims and conversions to top-level messages.
1520
-
1521
- \`slock message read\` shows messages in their current state. If a message was later converted to a task, it will show the \`[task #N ...]\` suffix.
1522
-
1523
- **Status flow:** \`todo\` \u2192 \`in_progress\` \u2192 \`in_review\` \u2192 \`done\`
1524
-
1525
- **Assignee** is independent from status \u2014 a task can be claimed or unclaimed at any status except \`done\`.
1526
-
1527
- **Workflow:**
1528
- 1. Receive a message that requires action \u2192 claim it first (by task number if already a task, or by message ID if it's a regular message)
1529
- 2. If the claim fails, someone else is working on it \u2014 move on to another task
1530
- 3. Post updates in the task's thread: \`slock message send --target "#channel:msgShortId" <<'${messageHeredocDelimiter}'\` followed by the message body and \`${messageHeredocDelimiter}\`
1531
- 4. When done, set status to \`in_review\` so a human can validate via \`slock task update\`
1532
- 5. After approval (e.g. "looks good", "merge it"), set status to \`done\`
1533
-
1534
- **What \`slock task create\` really means:**
1535
- - Tasks live in the same chat flow as messages. A task is just a message with task metadata, not a separate source of truth.
1536
- - \`slock task create\` is a convenience helper for a specific sequence: create a brand-new message, then publish that new message as a task-message.
1537
- - \`slock task create\` only creates the task \u2014 to own it, call \`slock task claim\` afterward.
1538
- - Typical uses for \`slock task create\` are breaking down a larger task into parallel subtasks, or batch-creating genuinely new work for others to claim.
1539
- - If someone already sent the work item as a message, just claim that existing message/task instead of creating a new one.
1540
- - If the work already exists as a message, reuse it via \`slock task claim --message-id ...\`.
1541
-
1542
- **Creating new tasks:**
1543
- - The task system exists to prevent duplicate work. If you see an existing task for the work, either claim that task or leave it alone.
1544
- - If a message already shows a \`[task #N ...]\` suffix, claim \`#N\` if it is yours to take; otherwise move on.
1545
- - Before calling \`slock task create\`, first check whether the work already exists on the task board or is already being handled.
1546
- - Reuse existing tasks and threads instead of creating duplicates.
1547
- - Use \`slock task create\` only for genuinely new subtasks or follow-up work that does not already have a canonical task.` : `### Tasks
1761
+ const tasksSection = isCli ? cliGuideSections.tasks : `### Tasks
1548
1762
 
1549
1763
  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.
1550
1764
 
@@ -1584,7 +1798,6 @@ ${readCmd} shows messages in their current state. If a message was later convert
1584
1798
  - Before calling ${taskCreateCmd}, first check whether the work already exists on the task board or is already being handled.
1585
1799
  - Reuse existing tasks and threads instead of creating duplicates.
1586
1800
  - Use ${taskCreateCmd} only for genuinely new subtasks or follow-up work that does not already have a canonical task.`;
1587
- const claimForEtiquette = isCli ? "`slock task claim`" : taskClaimCmd;
1588
1801
  let prompt = `You are "${config.displayName || config.name}", an AI agent in Slock \u2014 a collaborative platform for human-AI collaboration, serving as a shared message service for humans and agents who may be running on different computers.
1589
1802
 
1590
1803
  ## Who you are
@@ -1595,6 +1808,8 @@ ${runtimeContextLines(config).join("\n")}
1595
1808
 
1596
1809
  ${communicationSection}
1597
1810
 
1811
+ ${credentialHygieneSection}
1812
+
1598
1813
  CRITICAL RULES:
1599
1814
  ${criticalRules.join("\n")}
1600
1815
 
@@ -1664,119 +1879,23 @@ ${readingHistorySection}
1664
1879
 
1665
1880
  ${historicalReferenceSection}
1666
1881
 
1667
- ${tasksSection}
1668
-
1669
- ### Splitting tasks for parallel execution
1670
-
1671
- When you need to break down a large task into subtasks, structure them so agents can work **in parallel**:
1672
- - **Group by phase** if tasks have dependencies. Label them clearly (e.g. "Phase 1: ...", "Phase 2: ...") so agents know what can run concurrently and what must wait.
1673
- - **Prefer independent subtasks** that don't block each other. Each subtask should be completable without waiting for another.
1674
- - **Avoid creating sequential chains** where each task depends on the previous one \u2014 this forces agents to work one at a time, wasting capacity.
1675
-
1676
- When you receive a notification about new tasks, check the task board and claim tasks relevant to your skills.
1677
-
1678
- ## @Mentions
1679
-
1680
- In channel group chats, you can @mention people by their unique name (e.g. @alice or @bob).
1681
- - Your stable Slock @mention handle is \`@${config.name}\`.
1682
- - Your display name is \`${config.displayName || config.name}\`. Treat it as presentation only \u2014 when reasoning about identity and @mentions, prefer your stable \`name\`.
1683
- - Every human and agent has a unique \`name\` \u2014 this is their stable identifier for @mentions.
1684
- - Mention others, not yourself \u2014 assign reviews and follow-ups to teammates.
1685
- - @mentions only reach people inside the channel \u2014 channels are the isolation boundary.
1686
-
1687
- ## Communication style
1688
-
1689
- Keep the user informed. They cannot see your internal reasoning, so:
1690
- - When you receive a task, acknowledge it and briefly outline your plan before starting.
1691
- - For multi-step work, send short progress updates (e.g. "Working on step 2/3\u2026").
1692
- - When done, summarize the result.
1693
- - Keep updates concise \u2014 one or two sentences. Don't flood the chat.
1694
-
1695
- ### Conversation etiquette
1696
-
1697
- - **Respect ongoing conversations.** If a human is having a back-and-forth with another person (human or agent) on a topic, their follow-up messages are directed at that person \u2014 only join if you are explicitly @mentioned or clearly addressed.
1698
- - **Only the person doing the work should report on it.** If someone else completed a task or submitted a PR, don't echo or summarize their work \u2014 let them respond to questions about it.
1699
- - **Claim before you start.** Always call ${claimForEtiquette} before doing any work on a task. If the claim fails, stop immediately and pick a different task.
1700
- - **Before stopping, check for concrete blockers you own.** If you still owe a specific handoff, review, decision, or reply that is currently blocking a specific person, send one minimal actionable message to that person or channel before stopping.
1701
- - **Skip idle narration.** Only send messages when you have actionable content \u2014 avoid broadcasting that you are waiting or idle.
1702
-
1703
- ### Formatting \u2014 Mentions & Channel Refs
1704
-
1705
- Slock auto-renders these inline tokens as interactive links whenever they appear as bare text in your message:
1706
-
1707
- - @alice \u2014 links to a user
1708
- - #general or #1 \u2014 links to a channel
1709
- - #engineering:b885b5ae \u2014 links to a specific thread (channel name + msg ID suffix)
1710
- - task #123 \u2014 links to a task (always write "task #N", not bare "#N" which is ambiguous with PRs/issues)
1711
-
1712
- Write them inline as plain words in your sentence \u2014 the same way you'd type any other word \u2014 and Slock turns them into clickable references.
1713
-
1714
- Markdown markup expresses presentation semantics; do not mix markup delimiters into literal payloads. Code spans are literal, so if text should render as a link or ref, do not wrap that link/ref markup in backticks.
1715
-
1716
- ### Formatting \u2014 URLs in non-English text
1717
-
1718
- When writing a URL next to non-ASCII punctuation (Chinese, Japanese, etc.) and the URL should render as a link, wrap the URL in angle brackets or use markdown link syntax. Otherwise the punctuation may be rendered as part of the URL.
1719
-
1720
- - **Wrong**: \`\u6D4B\u8BD5\u73AF\u5883\uFF1Ahttp://localhost:3000\uFF0C\u8BF7\u67E5\u770B\` (the \`\uFF0C\` gets swallowed into the link)
1721
- - **Correct**: \`\u6D4B\u8BD5\u73AF\u5883\uFF1A<http://localhost:3000>\uFF0C\u8BF7\u67E5\u770B\`
1722
- - **Also correct**: \`\u6D4B\u8BD5\u73AF\u5883\uFF1A[http://localhost:3000](http://localhost:3000)\uFF0C\u8BF7\u67E5\u770B\`
1723
-
1724
- ## Workspace & Memory
1725
-
1726
- 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.
1727
-
1728
- ### MEMORY.md \u2014 Your Memory Index (CRITICAL)
1729
-
1730
- \`MEMORY.md\` is the **entry point** to all your knowledge. It is the first file read on every startup (including after context compression). Structure it as an index that points to everything you know. This file is called \`MEMORY.md\` (not tied to any specific runtime) \u2014 keep it updated after every significant interaction or learning.
1731
-
1732
- \`\`\`markdown
1733
- # <Your Name>
1734
-
1735
- ## Role
1736
- <your role definition, evolved over time>
1737
-
1738
- ## Key Knowledge
1739
- - Read notes/user-preferences.md for user preferences and conventions
1740
- - Read notes/channels.md for what each channel is about and ongoing work
1741
- - Read notes/domain.md for domain-specific knowledge and conventions
1742
- - ...
1743
-
1744
- ## Active Context
1745
- - Currently working on: <brief summary>
1746
- - Last interaction: <brief summary>
1747
- \`\`\`
1748
-
1749
- ### What to memorize
1750
-
1751
- **Actively observe and record** the following kinds of knowledge as you encounter them in conversations:
1752
-
1753
- 1. **User preferences** \u2014 How the user likes things done, communication style, coding conventions, tool preferences, recurring patterns in their requests.
1754
- 2. **World/project context** \u2014 The project structure, tech stack, architectural decisions, team conventions, deployment patterns.
1755
- 3. **Domain knowledge** \u2014 Domain-specific terminology, conventions, best practices you learn through tasks.
1756
- 4. **Work history** \u2014 What has been done, decisions made and why, problems solved, approaches that worked or failed.
1757
- 5. **Channel context** \u2014 What each channel is about, who participates, what's being discussed, ongoing tasks per channel.
1758
- 6. **Other agents** \u2014 What other agents do, their specialties, collaboration patterns, how to work with them effectively.
1759
-
1760
- ### How to organize memory
1761
-
1762
- - **MEMORY.md** is always the index. Keep it concise but comprehensive as a table of contents.
1763
- - Create a \`notes/\` directory for detailed knowledge files. Use descriptive names:
1764
- - \`notes/user-preferences.md\` \u2014 User's preferences and conventions
1765
- - \`notes/channels.md\` \u2014 Summary of each channel and its purpose
1766
- - \`notes/work-log.md\` \u2014 Important decisions and completed work
1767
- - \`notes/<domain>.md\` \u2014 Domain-specific knowledge
1768
- - You can also create any other files or directories for your work (scripts, notes, data, etc.)
1769
- - **Update notes proactively** \u2014 Don't wait to be asked. When you learn something important, write it down.
1770
- - **Keep MEMORY.md current** \u2014 After updating notes, update the index in MEMORY.md if new files were added.
1882
+ ${tasksSection}
1771
1883
 
1772
- ### Compaction safety (CRITICAL)
1884
+ ${cliGuideSections.splittingTasks}
1773
1885
 
1774
- Your context will be periodically compressed to stay within limits. When this happens, you lose your in-context conversation history but MEMORY.md is always re-read. Therefore:
1886
+ ${cliGuideSections.mentions}
1775
1887
 
1776
- - **MEMORY.md must be self-sufficient as a recovery point.** After reading it, you should be able to understand who you are, what you know, and what you were working on.
1777
- - **Before a long task**, write a brief "Active Context" note in MEMORY.md so you can resume if interrupted mid-task.
1778
- - **After completing work**, update your notes and MEMORY.md index so nothing is lost.
1779
- - Keep MEMORY.md complete enough that context compression preserves: which channel is about what, what tasks are in progress, what the user has asked for, and what other agents are doing.
1888
+ ${cliGuideSections.communicationStyle}
1889
+
1890
+ ${cliGuideSections.conversationEtiquette}
1891
+
1892
+ ${cliGuideSections.formattingMentionsChannels}
1893
+
1894
+ ${cliGuideSections.formattingUrls}
1895
+
1896
+ ${cliGuideSections.workspaceAndMemory}
1897
+
1898
+ ${cliGuideSections.compactionSafety}
1780
1899
 
1781
1900
  ## Capabilities
1782
1901
 
@@ -1861,6 +1980,19 @@ function listLegacySlockStatePaths(slockHome = resolveSlockHome(), homeDir = os.
1861
1980
  return candidates.filter((candidate) => existsSync(candidate.path));
1862
1981
  }
1863
1982
 
1983
+ // src/authEnv.ts
1984
+ var DAEMON_API_KEY_ENV = "SLOCK_MACHINE_API_KEY";
1985
+ var SLOCK_AGENT_TOKEN_ENV = "SLOCK_AGENT_TOKEN";
1986
+ function scrubDaemonAuthEnv(env) {
1987
+ delete env[DAEMON_API_KEY_ENV];
1988
+ return env;
1989
+ }
1990
+ function scrubDaemonChildEnv(env) {
1991
+ delete env[DAEMON_API_KEY_ENV];
1992
+ delete env[SLOCK_AGENT_TOKEN_ENV];
1993
+ return env;
1994
+ }
1995
+
1864
1996
  // src/agentCredentialProxy.ts
1865
1997
  import { randomBytes } from "crypto";
1866
1998
  import http from "http";
@@ -2190,6 +2322,160 @@ function stripUndefined(value) {
2190
2322
  return value;
2191
2323
  }
2192
2324
 
2325
+ // src/proxy.ts
2326
+ import { HttpsProxyAgent } from "https-proxy-agent";
2327
+ import { ProxyAgent } from "undici";
2328
+ var fetchDispatcherCache = /* @__PURE__ */ new Map();
2329
+ function getFetchPreResponseTimeoutMs(env) {
2330
+ const parsed = Number.parseInt(env.SLOCK_DAEMON_FETCH_PRE_RESPONSE_TIMEOUT_MS || "", 10);
2331
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 3e4;
2332
+ }
2333
+ function getDefaultPort(protocol) {
2334
+ switch (protocol) {
2335
+ case "https:":
2336
+ case "wss:":
2337
+ return "443";
2338
+ case "http:":
2339
+ case "ws:":
2340
+ return "80";
2341
+ default:
2342
+ return "";
2343
+ }
2344
+ }
2345
+ function hostMatchesNoProxyEntry(hostname, ruleHost) {
2346
+ if (!ruleHost) return false;
2347
+ const normalizedRule = ruleHost.replace(/^\*\./, ".").replace(/^\./, "").toLowerCase();
2348
+ const normalizedHost = hostname.toLowerCase();
2349
+ return normalizedHost === normalizedRule || normalizedHost.endsWith(`.${normalizedRule}`);
2350
+ }
2351
+ function getProxyUrlForTarget(targetUrl, env) {
2352
+ const protocol = new URL(targetUrl).protocol;
2353
+ switch (protocol) {
2354
+ case "wss:":
2355
+ return env.WSS_PROXY || env.wss_proxy || env.HTTPS_PROXY || env.https_proxy || env.ALL_PROXY || env.all_proxy;
2356
+ case "ws:":
2357
+ return env.WS_PROXY || env.ws_proxy || env.HTTP_PROXY || env.http_proxy || env.ALL_PROXY || env.all_proxy;
2358
+ case "https:":
2359
+ return env.HTTPS_PROXY || env.https_proxy || env.ALL_PROXY || env.all_proxy;
2360
+ case "http:":
2361
+ return env.HTTP_PROXY || env.http_proxy || env.ALL_PROXY || env.all_proxy;
2362
+ default:
2363
+ return env.ALL_PROXY || env.all_proxy;
2364
+ }
2365
+ }
2366
+ function shouldBypassProxy(targetUrl, env) {
2367
+ const rawNoProxy = env.NO_PROXY || env.no_proxy;
2368
+ if (!rawNoProxy) return false;
2369
+ const url = new URL(targetUrl);
2370
+ const hostname = url.hostname.toLowerCase();
2371
+ const port = url.port || getDefaultPort(url.protocol);
2372
+ return rawNoProxy.split(",").map((entry) => entry.trim()).filter(Boolean).some((entry) => {
2373
+ if (entry === "*") return true;
2374
+ const [ruleHost, rulePort] = entry.split(":", 2);
2375
+ if (rulePort && rulePort !== port) return false;
2376
+ return hostMatchesNoProxyEntry(hostname, ruleHost);
2377
+ });
2378
+ }
2379
+ function buildWebSocketOptions(wsUrl, env) {
2380
+ const proxyUrl = getProxyUrlForTarget(wsUrl, env);
2381
+ if (!proxyUrl) return void 0;
2382
+ if (shouldBypassProxy(wsUrl, env)) return void 0;
2383
+ return {
2384
+ agent: new HttpsProxyAgent(proxyUrl)
2385
+ };
2386
+ }
2387
+ function resolveProxyUrl(targetUrl, env) {
2388
+ const proxyUrl = getProxyUrlForTarget(targetUrl, env);
2389
+ if (!proxyUrl) return void 0;
2390
+ if (shouldBypassProxy(targetUrl, env)) return void 0;
2391
+ return proxyUrl;
2392
+ }
2393
+ function buildFetchDispatcher(targetUrl, env) {
2394
+ const proxyUrl = resolveProxyUrl(targetUrl, env);
2395
+ if (!proxyUrl) return void 0;
2396
+ const cached = fetchDispatcherCache.get(proxyUrl);
2397
+ if (cached) return cached;
2398
+ const timeoutMs = getFetchPreResponseTimeoutMs(env);
2399
+ const dispatcher = new ProxyAgent({
2400
+ uri: proxyUrl,
2401
+ // All three are pre-response and body-agnostic (see getFetchPreResponseTimeoutMs):
2402
+ // headersTimeout = headers-hang leg; requestTls.timeout = CONNECT-tunnel
2403
+ // establish leg; connect.timeout = socket to the proxy itself (defensive).
2404
+ connect: { timeout: timeoutMs },
2405
+ requestTls: { timeout: timeoutMs },
2406
+ headersTimeout: timeoutMs
2407
+ });
2408
+ fetchDispatcherCache.set(proxyUrl, dispatcher);
2409
+ return dispatcher;
2410
+ }
2411
+ function evictFetchDispatcher(targetUrl, env) {
2412
+ const proxyUrl = resolveProxyUrl(targetUrl, env);
2413
+ if (!proxyUrl) return false;
2414
+ const cached = fetchDispatcherCache.get(proxyUrl);
2415
+ if (!cached) return false;
2416
+ fetchDispatcherCache.delete(proxyUrl);
2417
+ void Promise.resolve().then(() => cached.close()).catch(() => cached.destroy?.(new Error("evicted"))).catch(() => {
2418
+ });
2419
+ return true;
2420
+ }
2421
+
2422
+ // src/daemonFetch.ts
2423
+ function withDaemonFetchProxy(input, init = {}, env = process.env) {
2424
+ const dispatcher = buildFetchDispatcher(input.toString(), env);
2425
+ return dispatcher ? { ...init, dispatcher } : init;
2426
+ }
2427
+ async function daemonFetch(input, init, env = process.env) {
2428
+ try {
2429
+ return await fetch(input, withDaemonFetchProxy(input, init, env));
2430
+ } catch (err) {
2431
+ evictFetchDispatcher(input.toString(), env);
2432
+ throw err;
2433
+ }
2434
+ }
2435
+
2436
+ // src/logger.ts
2437
+ var listeners = /* @__PURE__ */ new Set();
2438
+ function timestamp() {
2439
+ const d = /* @__PURE__ */ new Date();
2440
+ const pad = (n) => String(n).padStart(2, "0");
2441
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
2442
+ }
2443
+ function format(level, msg) {
2444
+ return `${timestamp()} [${level}] ${msg}`;
2445
+ }
2446
+ function emit(event) {
2447
+ for (const listener of listeners) {
2448
+ listener(event);
2449
+ }
2450
+ }
2451
+ function subscribeDaemonLogs(listener) {
2452
+ listeners.add(listener);
2453
+ return () => {
2454
+ listeners.delete(listener);
2455
+ };
2456
+ }
2457
+ var logger = {
2458
+ info(msg) {
2459
+ const line = format("INFO", msg);
2460
+ console.log(line);
2461
+ emit({ level: "INFO", line, message: msg });
2462
+ },
2463
+ warn(msg) {
2464
+ const line = format("WARN", msg);
2465
+ console.warn(line);
2466
+ emit({ level: "WARN", line, message: msg });
2467
+ },
2468
+ error(msg, err) {
2469
+ const line = format("ERROR", msg);
2470
+ if (err) {
2471
+ console.error(line, err);
2472
+ } else {
2473
+ console.error(line);
2474
+ }
2475
+ emit({ level: "ERROR", line, message: msg, error: err });
2476
+ }
2477
+ };
2478
+
2193
2479
  // src/agentCredentialProxy.ts
2194
2480
  var registrations = /* @__PURE__ */ new Map();
2195
2481
  var proxyServerState = null;
@@ -3016,7 +3302,9 @@ var LOOPBACK_NO_PROXY = "127.0.0.1,localhost";
3016
3302
  var CLI_TRANSPORT_TRACE_DIR_ENV = "SLOCK_CLI_TRANSPORT_TRACE_DIR";
3017
3303
  var safePathPart = (value) => value.replace(/[^a-zA-Z0-9_.-]/g, "_");
3018
3304
  var RAW_CREDENTIAL_ENV_DENYLIST = [
3019
- "SLOCK_AGENT_CREDENTIAL_KEY"
3305
+ "SLOCK_AGENT_TOKEN",
3306
+ "SLOCK_AGENT_CREDENTIAL_KEY",
3307
+ "SLOCK_AGENT_CREDENTIAL_KEY_FILE"
3020
3308
  ];
3021
3309
  var cachedOpencliBinPath;
3022
3310
  function resolveOpencliBinPath() {
@@ -3231,7 +3519,7 @@ exec ${shellSingleQuote(process.execPath)} ${shellSingleQuote(opencliBinPath)} "
3231
3519
  ...agentCredentialProxy ? {} : { SLOCK_AGENT_TOKEN_FILE: tokenFile },
3232
3520
  PATH: `${slockDir}${path2.delimiter}${process.env.PATH ?? ""}`
3233
3521
  };
3234
- delete spawnEnv.SLOCK_AGENT_TOKEN;
3522
+ scrubDaemonChildEnv(spawnEnv);
3235
3523
  for (const key of RAW_CREDENTIAL_ENV_DENYLIST) {
3236
3524
  delete spawnEnv[key];
3237
3525
  }
@@ -3660,7 +3948,7 @@ function resolveCommandOnWindows(command, env, execFileSyncFn, existsSyncFn) {
3660
3948
  }
3661
3949
  function resolveCommandOnPath(command, deps = {}) {
3662
3950
  const platform = deps.platform ?? process.platform;
3663
- const env = withWindowsUserEnvironment(deps.env ?? process.env, deps);
3951
+ const env = scrubDaemonChildEnv({ ...withWindowsUserEnvironment(deps.env ?? process.env, deps) });
3664
3952
  const execFileSyncFn = deps.execFileSyncFn ?? execFileSync;
3665
3953
  const existsSyncFn = deps.existsSyncFn ?? existsSync2;
3666
3954
  if (platform === "win32") {
@@ -3686,7 +3974,7 @@ function firstExistingPath(candidates, deps = {}) {
3686
3974
  return null;
3687
3975
  }
3688
3976
  function readCommandVersion(command, args = [], deps = {}) {
3689
- const env = withWindowsUserEnvironment(deps.env ?? process.env, deps);
3977
+ const env = scrubDaemonChildEnv({ ...withWindowsUserEnvironment(deps.env ?? process.env, deps) });
3690
3978
  const execFileSyncFn = deps.execFileSyncFn ?? execFileSync;
3691
3979
  try {
3692
3980
  const output = normalizeExecOutput(execFileSyncFn(command, [...args, "--version"], {
@@ -4383,7 +4671,7 @@ var CodexDriver = class {
4383
4671
  };
4384
4672
  communication = {
4385
4673
  chat: "slock_cli",
4386
- runtimeControl: "mcp_runtime_actions"
4674
+ runtimeControl: "none"
4387
4675
  };
4388
4676
  session = {
4389
4677
  recovery: "resume_or_fresh"
@@ -4400,50 +4688,6 @@ var CodexDriver = class {
4400
4688
  probe() {
4401
4689
  return probeCodex();
4402
4690
  }
4403
- buildRuntimeActionsConfigArgs(ctx) {
4404
- const isTsSource = ctx.chatBridgePath.endsWith(".ts");
4405
- const command = isTsSource ? "npx" : "node";
4406
- const bridgeArgs = isTsSource ? [
4407
- "tsx",
4408
- ctx.chatBridgePath,
4409
- "--agent-id",
4410
- ctx.agentId,
4411
- "--server-url",
4412
- ctx.config.serverUrl,
4413
- "--auth-token",
4414
- ctx.config.authToken || ctx.daemonApiKey,
4415
- "--runtime",
4416
- this.id,
4417
- ...ctx.launchId ? ["--launch-id", ctx.launchId] : [],
4418
- "--runtime-actions-only"
4419
- ] : [
4420
- ctx.chatBridgePath,
4421
- "--agent-id",
4422
- ctx.agentId,
4423
- "--server-url",
4424
- ctx.config.serverUrl,
4425
- "--auth-token",
4426
- ctx.config.authToken || ctx.daemonApiKey,
4427
- "--runtime",
4428
- this.id,
4429
- ...ctx.launchId ? ["--launch-id", ctx.launchId] : [],
4430
- "--runtime-actions-only"
4431
- ];
4432
- return [
4433
- "-c",
4434
- `mcp_servers.chat.command=${JSON.stringify(command)}`,
4435
- "-c",
4436
- `mcp_servers.chat.args=${JSON.stringify(bridgeArgs)}`,
4437
- "-c",
4438
- "mcp_servers.chat.startup_timeout_sec=30",
4439
- "-c",
4440
- "mcp_servers.chat.tool_timeout_sec=120",
4441
- "-c",
4442
- "mcp_servers.chat.enabled=true",
4443
- "-c",
4444
- "mcp_servers.chat.required=true"
4445
- ];
4446
- }
4447
4691
  buildThreadRequest(ctx) {
4448
4692
  const threadParams = {
4449
4693
  cwd: ctx.workingDirectory,
@@ -4497,7 +4741,6 @@ var CodexDriver = class {
4497
4741
  this.initialTurnStarted = false;
4498
4742
  this.normalizer.reset({ threadId: ctx.config.sessionId || null });
4499
4743
  const args = ["app-server", "--listen", "stdio://"];
4500
- args.push(...this.buildRuntimeActionsConfigArgs(ctx));
4501
4744
  const { command, args: spawnArgs } = resolveCodexSpawn(args);
4502
4745
  const proc = spawn2(command, spawnArgs, {
4503
4746
  cwd: ctx.workingDirectory,
@@ -4784,8 +5027,6 @@ var AntigravityDriver = class {
4784
5027
 
4785
5028
  // src/drivers/copilot.ts
4786
5029
  import { spawn as spawn4 } from "child_process";
4787
- import path6 from "path";
4788
- import { writeFileSync as writeFileSync3 } from "fs";
4789
5030
  async function buildCopilotSpawnEnv(ctx) {
4790
5031
  return (await prepareCliTransport(ctx, { NO_COLOR: "1" })).spawnEnv;
4791
5032
  }
@@ -4799,7 +5040,7 @@ var CopilotDriver = class {
4799
5040
  };
4800
5041
  communication = {
4801
5042
  chat: "slock_cli",
4802
- runtimeControl: "mcp_runtime_actions"
5043
+ runtimeControl: "none"
4803
5044
  };
4804
5045
  session = {
4805
5046
  recovery: "resume_or_fresh"
@@ -4814,43 +5055,14 @@ var CopilotDriver = class {
4814
5055
  usesSlockCliForCommunication = true;
4815
5056
  sessionId = null;
4816
5057
  sessionAnnounced = false;
4817
- buildRuntimeActionsMcpConfig(ctx) {
4818
- const isTsSource = ctx.chatBridgePath.endsWith(".ts");
4819
- const command = isTsSource ? "npx" : "node";
4820
- const bridgeArgs = isTsSource ? ["tsx", ctx.chatBridgePath] : [ctx.chatBridgePath];
4821
- return JSON.stringify({
4822
- mcpServers: {
4823
- chat: {
4824
- command,
4825
- args: [
4826
- ...bridgeArgs,
4827
- "--agent-id",
4828
- ctx.agentId,
4829
- "--server-url",
4830
- ctx.config.serverUrl,
4831
- "--auth-token",
4832
- ctx.config.authToken || ctx.daemonApiKey,
4833
- "--runtime",
4834
- this.id,
4835
- ...ctx.launchId ? ["--launch-id", ctx.launchId] : [],
4836
- "--runtime-actions-only"
4837
- ]
4838
- }
4839
- }
4840
- });
4841
- }
4842
5058
  async spawn(ctx) {
4843
5059
  this.sessionId = ctx.config.sessionId || null;
4844
5060
  this.sessionAnnounced = false;
4845
- const mcpConfigPath = path6.join(ctx.workingDirectory, ".slock-copilot-mcp.json");
4846
- writeFileSync3(mcpConfigPath, this.buildRuntimeActionsMcpConfig(ctx), "utf8");
4847
5061
  const args = [
4848
5062
  "--output-format",
4849
5063
  "json",
4850
5064
  "--allow-all-tools",
4851
5065
  "--allow-all-paths",
4852
- "--additional-mcp-config",
4853
- `@${mcpConfigPath}`,
4854
5066
  "-p",
4855
5067
  ctx.prompt
4856
5068
  ];
@@ -4959,8 +5171,6 @@ var CopilotDriver = class {
4959
5171
 
4960
5172
  // src/drivers/cursor.ts
4961
5173
  import { spawn as spawn5, spawnSync } from "child_process";
4962
- import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync2, existsSync as existsSync4 } from "fs";
4963
- import path7 from "path";
4964
5174
  async function buildCursorSpawnEnv(ctx, deps = {}) {
4965
5175
  const { spawnEnv } = await prepareCliTransport(ctx, { NO_COLOR: "1" });
4966
5176
  return withWindowsUserEnvironment(spawnEnv, deps);
@@ -4975,7 +5185,7 @@ var CursorDriver = class {
4975
5185
  };
4976
5186
  communication = {
4977
5187
  chat: "slock_cli",
4978
- runtimeControl: "mcp_runtime_actions"
5188
+ runtimeControl: "none"
4979
5189
  };
4980
5190
  session = {
4981
5191
  recovery: "resume_or_fresh"
@@ -4988,38 +5198,7 @@ var CursorDriver = class {
4988
5198
  mcpToolPrefix = "mcp__chat__";
4989
5199
  busyDeliveryMode = "none";
4990
5200
  usesSlockCliForCommunication = true;
4991
- buildRuntimeActionsMcpConfig(ctx) {
4992
- const isTsSource = ctx.chatBridgePath.endsWith(".ts");
4993
- const command = isTsSource ? "npx" : "node";
4994
- const bridgeArgs = isTsSource ? ["tsx", ctx.chatBridgePath] : [ctx.chatBridgePath];
4995
- return JSON.stringify({
4996
- mcpServers: {
4997
- chat: {
4998
- command,
4999
- args: [
5000
- ...bridgeArgs,
5001
- "--agent-id",
5002
- ctx.agentId,
5003
- "--server-url",
5004
- ctx.config.serverUrl,
5005
- "--auth-token",
5006
- ctx.config.authToken || ctx.daemonApiKey,
5007
- "--runtime",
5008
- this.id,
5009
- ...ctx.launchId ? ["--launch-id", ctx.launchId] : [],
5010
- "--runtime-actions-only"
5011
- ]
5012
- }
5013
- }
5014
- });
5015
- }
5016
5201
  async spawn(ctx) {
5017
- const cursorDir = path7.join(ctx.workingDirectory, ".cursor");
5018
- if (!existsSync4(cursorDir)) {
5019
- mkdirSync2(cursorDir, { recursive: true });
5020
- }
5021
- const mcpConfigPath = path7.join(cursorDir, "mcp.json");
5022
- writeFileSync4(mcpConfigPath, this.buildRuntimeActionsMcpConfig(ctx), "utf8");
5023
5202
  const args = [
5024
5203
  "--print",
5025
5204
  "--output-format",
@@ -5149,11 +5328,11 @@ function detectCursorModels(runCommand = runCursorModelsCommand) {
5149
5328
  return parseCursorModelsOutput(String(result.stdout || ""));
5150
5329
  }
5151
5330
  function buildCursorModelProbeEnv(deps = {}) {
5152
- return withWindowsUserEnvironment({
5331
+ return scrubDaemonChildEnv(withWindowsUserEnvironment({
5153
5332
  ...deps.env ?? process.env,
5154
5333
  FORCE_COLOR: "0",
5155
5334
  NO_COLOR: "1"
5156
- }, deps);
5335
+ }, deps));
5157
5336
  }
5158
5337
  function runCursorModelsCommand() {
5159
5338
  return spawnSync("cursor-agent", ["models"], {
@@ -5165,8 +5344,8 @@ function runCursorModelsCommand() {
5165
5344
 
5166
5345
  // src/drivers/gemini.ts
5167
5346
  import { execFileSync as execFileSync3, spawn as spawn6 } from "child_process";
5168
- import { existsSync as existsSync5, mkdirSync as mkdirSync3, writeFileSync as writeFileSync5 } from "fs";
5169
- import path8 from "path";
5347
+ import { existsSync as existsSync4 } from "fs";
5348
+ import path6 from "path";
5170
5349
  async function buildGeminiSpawnEnv(ctx, platform = process.platform) {
5171
5350
  const { spawnEnv } = await prepareCliTransport(ctx, { NO_COLOR: "1" }, platform);
5172
5351
  const launchEnvVars = runtimeConfigToLaunchFields(ctx.config).envVars;
@@ -5208,9 +5387,9 @@ function resolveGeminiSpawn(commandArgs, deps = {}) {
5208
5387
  return { command: resolveCommandOnPath("gemini", deps) ?? "gemini", args: commandArgs };
5209
5388
  }
5210
5389
  const execFileSyncFn = deps.execFileSyncFn ?? execFileSync3;
5211
- const existsSyncFn = deps.existsSyncFn ?? existsSync5;
5212
- const env = deps.env ?? process.env;
5213
- const winPath = path8.win32;
5390
+ const existsSyncFn = deps.existsSyncFn ?? existsSync4;
5391
+ const env = scrubDaemonChildEnv({ ...deps.env ?? process.env });
5392
+ const winPath = path6.win32;
5214
5393
  let geminiEntry = null;
5215
5394
  try {
5216
5395
  const globalRoot = normalizeExecOutput2(execFileSyncFn("npm", ["root", "-g"], {
@@ -5254,7 +5433,7 @@ var GeminiDriver = class {
5254
5433
  };
5255
5434
  communication = {
5256
5435
  chat: "slock_cli",
5257
- runtimeControl: "mcp_runtime_actions"
5436
+ runtimeControl: "none"
5258
5437
  };
5259
5438
  session = {
5260
5439
  recovery: "resume_or_fresh"
@@ -5272,7 +5451,6 @@ var GeminiDriver = class {
5272
5451
  async spawn(ctx) {
5273
5452
  this.sessionId = ctx.config.sessionId || null;
5274
5453
  this.sessionAnnounced = false;
5275
- this.writeGeminiSettings(ctx);
5276
5454
  const { command, args } = resolveGeminiSpawn(buildGeminiArgs(ctx.config));
5277
5455
  const spawnEnv = await buildGeminiSpawnEnv(ctx);
5278
5456
  const proc = spawn6(command, args, {
@@ -5345,49 +5523,20 @@ var GeminiDriver = class {
5345
5523
  messageNotificationStyle: "poll"
5346
5524
  });
5347
5525
  }
5348
- writeGeminiSettings(ctx) {
5349
- const geminiDir = path8.join(ctx.workingDirectory, ".gemini");
5350
- mkdirSync3(geminiDir, { recursive: true });
5351
- const settingsPath = path8.join(geminiDir, "settings.json");
5352
- writeFileSync5(settingsPath, JSON.stringify(this.buildRuntimeActionsMcpSettings(ctx)), "utf8");
5353
- }
5354
- buildRuntimeActionsMcpSettings(ctx) {
5355
- const isTsSource = ctx.chatBridgePath.endsWith(".ts");
5356
- const command = isTsSource ? "npx" : "node";
5357
- const bridgeArgs = isTsSource ? ["tsx", ctx.chatBridgePath] : [ctx.chatBridgePath];
5358
- return {
5359
- mcpServers: {
5360
- chat: {
5361
- command,
5362
- args: [
5363
- ...bridgeArgs,
5364
- "--agent-id",
5365
- ctx.agentId,
5366
- "--server-url",
5367
- ctx.config.serverUrl,
5368
- "--auth-token",
5369
- ctx.config.authToken || ctx.daemonApiKey,
5370
- "--runtime",
5371
- this.id,
5372
- ...ctx.launchId ? ["--launch-id", ctx.launchId] : [],
5373
- "--runtime-actions-only"
5374
- ]
5375
- }
5376
- }
5377
- };
5378
- }
5379
5526
  };
5380
5527
 
5381
5528
  // src/drivers/kimi.ts
5382
5529
  import { randomUUID as randomUUID2 } from "crypto";
5383
5530
  import { spawn as spawn7 } from "child_process";
5384
- import { existsSync as existsSync6, readFileSync as readFileSync3, writeFileSync as writeFileSync6 } from "fs";
5531
+ import { chmodSync, existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
5385
5532
  import os3 from "os";
5386
- import path9 from "path";
5533
+ import path7 from "path";
5387
5534
  var KIMI_WIRE_PROTOCOL_VERSION = "1.3";
5388
5535
  var KIMI_SYSTEM_PROMPT_FILE = ".slock-kimi-system.md";
5389
5536
  var KIMI_AGENT_FILE = ".slock-kimi-agent.yaml";
5390
- var KIMI_MCP_FILE = ".slock-kimi-mcp.json";
5537
+ var KIMI_GENERATED_CONFIG_FILE = ".slock-kimi-config.toml";
5538
+ var SLOCK_KIMI_CONFIG_CONTENT_ENV = "SLOCK_KIMI_CONFIG_CONTENT";
5539
+ var SLOCK_KIMI_CONFIG_FILE_ENV = "SLOCK_KIMI_CONFIG_FILE";
5391
5540
  function parseToolArguments(raw) {
5392
5541
  if (typeof raw !== "string") return raw;
5393
5542
  try {
@@ -5396,6 +5545,73 @@ function parseToolArguments(raw) {
5396
5545
  return raw;
5397
5546
  }
5398
5547
  }
5548
+ function readKimiConfigSource(home = os3.homedir(), env = process.env) {
5549
+ const inlineConfig = env[SLOCK_KIMI_CONFIG_CONTENT_ENV];
5550
+ if (inlineConfig && inlineConfig.trim()) {
5551
+ return {
5552
+ raw: inlineConfig,
5553
+ explicitPath: null,
5554
+ sourcePath: SLOCK_KIMI_CONFIG_CONTENT_ENV
5555
+ };
5556
+ }
5557
+ const explicitPath = env[SLOCK_KIMI_CONFIG_FILE_ENV];
5558
+ const configPath = explicitPath && explicitPath.trim() ? explicitPath : path7.join(home, ".kimi", "config.toml");
5559
+ try {
5560
+ return {
5561
+ raw: readFileSync3(configPath, "utf8"),
5562
+ explicitPath: explicitPath && explicitPath.trim() ? explicitPath : null,
5563
+ sourcePath: configPath
5564
+ };
5565
+ } catch {
5566
+ return {
5567
+ raw: null,
5568
+ explicitPath: explicitPath && explicitPath.trim() ? explicitPath : null,
5569
+ sourcePath: configPath
5570
+ };
5571
+ }
5572
+ }
5573
+ function buildKimiSpawnEnv(env = process.env) {
5574
+ const spawnEnv = { ...env, FORCE_COLOR: "0", NO_COLOR: "1" };
5575
+ delete spawnEnv[SLOCK_KIMI_CONFIG_CONTENT_ENV];
5576
+ delete spawnEnv[SLOCK_KIMI_CONFIG_FILE_ENV];
5577
+ return scrubDaemonChildEnv(spawnEnv);
5578
+ }
5579
+ function buildKimiEffectiveEnv(ctx, overrideEnv) {
5580
+ return {
5581
+ ...process.env,
5582
+ ...ctx.config.envVars || {},
5583
+ ...overrideEnv || {}
5584
+ };
5585
+ }
5586
+ function buildKimiLaunchOptions(ctx, opts = {}) {
5587
+ const env = buildKimiEffectiveEnv(ctx, opts.env);
5588
+ const source = readKimiConfigSource(opts.home ?? os3.homedir(), env);
5589
+ const args = [];
5590
+ let configFilePath = null;
5591
+ let configContent = null;
5592
+ if (source.explicitPath) {
5593
+ configFilePath = source.explicitPath;
5594
+ } else if (source.raw !== null && source.sourcePath === SLOCK_KIMI_CONFIG_CONTENT_ENV) {
5595
+ configFilePath = path7.join(ctx.workingDirectory, KIMI_GENERATED_CONFIG_FILE);
5596
+ configContent = source.raw;
5597
+ if (opts.writeGeneratedConfig !== false) {
5598
+ writeFileSync3(configFilePath, source.raw, { encoding: "utf8", mode: 384 });
5599
+ chmodSync(configFilePath, 384);
5600
+ }
5601
+ }
5602
+ if (configFilePath) {
5603
+ args.push("--config-file", configFilePath);
5604
+ }
5605
+ if (ctx.config.model && ctx.config.model !== "default") {
5606
+ args.push("--model", ctx.config.model);
5607
+ }
5608
+ return {
5609
+ args,
5610
+ env: buildKimiSpawnEnv(env),
5611
+ configFilePath,
5612
+ configContent
5613
+ };
5614
+ }
5399
5615
  function resolveKimiSpawn(commandArgs, deps = {}) {
5400
5616
  return {
5401
5617
  command: resolveCommandOnPath("kimi", deps) ?? "kimi",
@@ -5412,14 +5628,32 @@ var KimiDriver = class {
5412
5628
  };
5413
5629
  communication = {
5414
5630
  chat: "slock_cli",
5415
- runtimeControl: "mcp_runtime_actions"
5631
+ runtimeControl: "none"
5416
5632
  };
5417
5633
  session = {
5418
5634
  recovery: "resume_or_fresh"
5419
5635
  };
5420
5636
  model = {
5421
5637
  detectedModelsVerifiedAs: "launchable",
5422
- toLaunchSpec: (modelId) => ({ args: ["--model", modelId] })
5638
+ toLaunchSpec: (modelId, ctx, opts) => {
5639
+ if (!ctx) return { args: ["--model", modelId] };
5640
+ const launchCtx = {
5641
+ ...ctx,
5642
+ config: {
5643
+ ...ctx.config,
5644
+ model: modelId
5645
+ }
5646
+ };
5647
+ const launch = buildKimiLaunchOptions(launchCtx, {
5648
+ home: opts?.home,
5649
+ writeGeneratedConfig: false
5650
+ });
5651
+ return {
5652
+ args: launch.args,
5653
+ env: launch.env,
5654
+ configFiles: launch.configFilePath ? [launch.configFilePath] : void 0
5655
+ };
5656
+ }
5423
5657
  };
5424
5658
  supportsStdinNotification = true;
5425
5659
  mcpToolPrefix = "";
@@ -5428,68 +5662,40 @@ var KimiDriver = class {
5428
5662
  sessionId = null;
5429
5663
  sessionAnnounced = false;
5430
5664
  promptRequestId = null;
5431
- buildChatBridgeArgs(ctx) {
5432
- const isTsSource = ctx.chatBridgePath.endsWith(".ts");
5433
- return [
5434
- ...isTsSource ? ["tsx", ctx.chatBridgePath] : [ctx.chatBridgePath],
5435
- "--agent-id",
5436
- ctx.agentId,
5437
- "--server-url",
5438
- ctx.config.serverUrl,
5439
- "--auth-token",
5440
- ctx.config.authToken || ctx.daemonApiKey,
5441
- "--runtime",
5442
- "kimi",
5443
- ...ctx.launchId ? ["--launch-id", ctx.launchId] : [],
5444
- "--runtime-actions-only"
5445
- ];
5446
- }
5447
5665
  async spawn(ctx) {
5448
5666
  const isResume = !!ctx.config.sessionId;
5449
5667
  this.sessionId = ctx.config.sessionId || randomUUID2();
5450
5668
  this.sessionAnnounced = false;
5451
5669
  this.promptRequestId = randomUUID2();
5452
- const isTsSource = ctx.chatBridgePath.endsWith(".ts");
5453
- const command = isTsSource ? "npx" : "node";
5454
- const bridgeArgs = this.buildChatBridgeArgs(ctx);
5455
- const systemPromptPath = path9.join(ctx.workingDirectory, KIMI_SYSTEM_PROMPT_FILE);
5456
- const agentFilePath = path9.join(ctx.workingDirectory, KIMI_AGENT_FILE);
5457
- const mcpConfigPath = path9.join(ctx.workingDirectory, KIMI_MCP_FILE);
5458
- if (!isResume || !existsSync6(systemPromptPath)) {
5459
- writeFileSync6(systemPromptPath, ctx.prompt, "utf8");
5460
- }
5461
- writeFileSync6(agentFilePath, [
5670
+ const systemPromptPath = path7.join(ctx.workingDirectory, KIMI_SYSTEM_PROMPT_FILE);
5671
+ const agentFilePath = path7.join(ctx.workingDirectory, KIMI_AGENT_FILE);
5672
+ if (!isResume || !existsSync5(systemPromptPath)) {
5673
+ writeFileSync3(systemPromptPath, ctx.prompt, "utf8");
5674
+ }
5675
+ writeFileSync3(agentFilePath, [
5462
5676
  "version: 1",
5463
5677
  "agent:",
5464
5678
  " extend: default",
5465
5679
  ` system_prompt_path: ./${KIMI_SYSTEM_PROMPT_FILE}`,
5466
5680
  ""
5467
5681
  ].join("\n"), "utf8");
5468
- writeFileSync6(mcpConfigPath, JSON.stringify({
5469
- mcpServers: {
5470
- chat: {
5471
- command,
5472
- args: bridgeArgs
5473
- }
5474
- }
5475
- }), "utf8");
5682
+ const launch = buildKimiLaunchOptions(ctx);
5476
5683
  const args = [
5477
5684
  "--wire",
5478
5685
  "--yolo",
5479
5686
  "--agent-file",
5480
5687
  agentFilePath,
5481
- "--mcp-config-file",
5482
- mcpConfigPath,
5483
5688
  "--session",
5484
- this.sessionId
5689
+ this.sessionId,
5690
+ ...launch.args
5485
5691
  ];
5486
5692
  const launchRuntimeFields = runtimeConfigToLaunchFields(ctx.config);
5487
5693
  if (launchRuntimeFields.model && launchRuntimeFields.model !== "default") {
5488
5694
  args.push("--model", launchRuntimeFields.model);
5489
5695
  }
5490
5696
  const spawnEnv = (await prepareCliTransport(ctx, { NO_COLOR: "1" })).spawnEnv;
5491
- const launch = resolveKimiSpawn(args);
5492
- const proc = spawn7(launch.command, launch.args, {
5697
+ const spawnTarget = resolveKimiSpawn(args);
5698
+ const proc = spawn7(spawnTarget.command, spawnTarget.args, {
5493
5699
  cwd: ctx.workingDirectory,
5494
5700
  stdio: ["pipe", "pipe", "pipe"],
5495
5701
  env: spawnEnv,
@@ -5497,7 +5703,7 @@ var KimiDriver = class {
5497
5703
  // and has an 8191-character command-line limit. Kimi's official
5498
5704
  // installer/uv entrypoint is an executable, so launch it directly and
5499
5705
  // keep prompts on stdin / files instead of routing through cmd.exe.
5500
- shell: launch.shell
5706
+ shell: spawnTarget.shell
5501
5707
  });
5502
5708
  proc.stdin?.write(JSON.stringify({
5503
5709
  jsonrpc: "2.0",
@@ -5611,14 +5817,9 @@ var KimiDriver = class {
5611
5817
  return detectKimiModels();
5612
5818
  }
5613
5819
  };
5614
- function detectKimiModels(home = os3.homedir()) {
5615
- const configPath = path9.join(home, ".kimi", "config.toml");
5616
- let raw;
5617
- try {
5618
- raw = readFileSync3(configPath, "utf8");
5619
- } catch {
5620
- return null;
5621
- }
5820
+ function detectKimiModels(home = os3.homedir(), opts = {}) {
5821
+ const raw = readKimiConfigSource(home, opts.env).raw;
5822
+ if (raw === null) return null;
5622
5823
  const models = [];
5623
5824
  const sectionRe = /^\s*\[models(?:\.([^\]]+)|"\.[^"]+"|\."[^"]+")\s*\]\s*$/gm;
5624
5825
  const lineRe = /^\s*\[models\.(.+?)\s*\]\s*$/gm;
@@ -5639,9 +5840,9 @@ function detectKimiModels(home = os3.homedir()) {
5639
5840
 
5640
5841
  // src/drivers/opencode.ts
5641
5842
  import { spawn as spawn8, spawnSync as spawnSync2 } from "child_process";
5642
- import { existsSync as existsSync7, readFileSync as readFileSync4 } from "fs";
5843
+ import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
5643
5844
  import os4 from "os";
5644
- import path10 from "path";
5845
+ import path8 from "path";
5645
5846
  var CHAT_MCP_SERVER_NAME = "chat";
5646
5847
  var CHAT_MCP_TOOL_PREFIX = `${CHAT_MCP_SERVER_NAME}_`;
5647
5848
  var SLOCK_AGENT_NAME = "slock";
@@ -5656,23 +5857,6 @@ var OPENCODE_PROVIDER_LABELS = {
5656
5857
  deepseek: "DeepSeek",
5657
5858
  fusecode: "FuseCode"
5658
5859
  };
5659
- function buildChatBridgeCommand(ctx) {
5660
- const isTsSource = ctx.chatBridgePath.endsWith(".ts");
5661
- return [
5662
- isTsSource ? "npx" : "node",
5663
- ...isTsSource ? ["tsx", ctx.chatBridgePath] : [ctx.chatBridgePath],
5664
- "--agent-id",
5665
- ctx.agentId,
5666
- "--server-url",
5667
- ctx.config.serverUrl,
5668
- "--auth-token",
5669
- ctx.config.authToken || ctx.daemonApiKey,
5670
- "--runtime",
5671
- "opencode",
5672
- ...ctx.launchId ? ["--launch-id", ctx.launchId] : [],
5673
- "--runtime-actions-only"
5674
- ];
5675
- }
5676
5860
  function parseOpenCodeConfigContent(raw) {
5677
5861
  if (!raw) return {};
5678
5862
  try {
@@ -5689,7 +5873,7 @@ function parseUserOpenCodeConfig(ctx) {
5689
5873
  return parseOpenCodeConfigContent(raw);
5690
5874
  }
5691
5875
  function readLocalOpenCodeConfig(home = os4.homedir()) {
5692
- const configPath = path10.join(home, ".config", "opencode", "opencode.json");
5876
+ const configPath = path8.join(home, ".config", "opencode", "opencode.json");
5693
5877
  try {
5694
5878
  return parseOpenCodeConfigContent(readFileSync4(configPath, "utf8"));
5695
5879
  } catch {
@@ -5764,14 +5948,7 @@ function buildOpenCodeConfig(ctx, home = os4.homedir()) {
5764
5948
  prompt: ctx.standingPrompt
5765
5949
  }
5766
5950
  },
5767
- mcp: {
5768
- ...recordField(userConfig.mcp),
5769
- [CHAT_MCP_SERVER_NAME]: {
5770
- type: "local",
5771
- command: buildChatBridgeCommand(ctx),
5772
- enabled: true
5773
- }
5774
- }
5951
+ mcp: recordField(userConfig.mcp)
5775
5952
  };
5776
5953
  }
5777
5954
  async function buildOpenCodeLaunchOptions(ctx, home = os4.homedir(), version = null) {
@@ -5882,7 +6059,7 @@ function runOpenCodeModelsCommand(home, deps = {}) {
5882
6059
  const platform = deps.platform ?? process.platform;
5883
6060
  const spawnSyncFn = deps.spawnSyncFn ?? spawnSync2;
5884
6061
  const result = spawnSyncFn("opencode", ["models"], {
5885
- env: { ...process.env, HOME: home, FORCE_COLOR: "0", NO_COLOR: "1" },
6062
+ env: scrubDaemonChildEnv({ ...process.env, HOME: home, FORCE_COLOR: "0", NO_COLOR: "1" }),
5886
6063
  encoding: "utf8",
5887
6064
  timeout: 5e3,
5888
6065
  shell: platform === "win32"
@@ -5894,11 +6071,11 @@ function runOpenCodeModelsCommand(home, deps = {}) {
5894
6071
  };
5895
6072
  }
5896
6073
  function isWindowsCommandShim(commandPath) {
5897
- const ext = path10.win32.extname(commandPath).toLowerCase();
6074
+ const ext = path8.win32.extname(commandPath).toLowerCase();
5898
6075
  return ext === ".cmd" || ext === ".bat";
5899
6076
  }
5900
6077
  function opencodePackageEntryCandidates(packageRoot) {
5901
- const winPath = path10.win32;
6078
+ const winPath = path8.win32;
5902
6079
  return [
5903
6080
  winPath.join(packageRoot, "bin", "opencode.exe"),
5904
6081
  winPath.join(packageRoot, "bin", "opencode.js"),
@@ -5907,16 +6084,16 @@ function opencodePackageEntryCandidates(packageRoot) {
5907
6084
  ];
5908
6085
  }
5909
6086
  function openCodeSpecForEntry(entry, commandArgs) {
5910
- if (path10.win32.extname(entry).toLowerCase() === ".exe") {
6087
+ if (path8.win32.extname(entry).toLowerCase() === ".exe") {
5911
6088
  return { command: entry, args: commandArgs, shell: false };
5912
6089
  }
5913
6090
  return { command: process.execPath, args: [entry, ...commandArgs], shell: false };
5914
6091
  }
5915
6092
  function resolveWindowsOpenCodePackageEntry(commandPath, deps = {}) {
5916
- const existsSyncFn = deps.existsSyncFn ?? existsSync7;
6093
+ const existsSyncFn = deps.existsSyncFn ?? existsSync6;
5917
6094
  const execFileSyncFn = deps.execFileSyncFn;
5918
6095
  const env = deps.env ?? process.env;
5919
- const winPath = path10.win32;
6096
+ const winPath = path8.win32;
5920
6097
  const candidates = [];
5921
6098
  if (execFileSyncFn) {
5922
6099
  try {
@@ -5944,7 +6121,7 @@ function resolveWindowsOpenCodePackageEntry(commandPath, deps = {}) {
5944
6121
  function extractWindowsShimTargets(commandPath, deps = {}) {
5945
6122
  if (!isWindowsCommandShim(commandPath)) return [];
5946
6123
  const readFileSyncFn = deps.readFileSyncFn ?? readFileSync4;
5947
- const commandDir = path10.win32.dirname(commandPath);
6124
+ const commandDir = path8.win32.dirname(commandPath);
5948
6125
  let raw;
5949
6126
  try {
5950
6127
  raw = String(readFileSyncFn(commandPath, "utf8"));
@@ -5955,7 +6132,7 @@ function extractWindowsShimTargets(commandPath, deps = {}) {
5955
6132
  const dp0Pattern = /%~dp0\\?([^"\r\n]*?opencode\.(?:exe|js|mjs|cjs))/gi;
5956
6133
  for (const match of raw.matchAll(dp0Pattern)) {
5957
6134
  const relative = match[1]?.replace(/^\\+/, "");
5958
- if (relative) candidates.push(path10.win32.normalize(path10.win32.join(commandDir, relative)));
6135
+ if (relative) candidates.push(path8.win32.normalize(path8.win32.join(commandDir, relative)));
5959
6136
  }
5960
6137
  return candidates;
5961
6138
  }
@@ -5969,7 +6146,7 @@ function resolveOpenCodeSpawn(commandArgs, deps = {}) {
5969
6146
  };
5970
6147
  }
5971
6148
  const command = resolveCommandOnPath("opencode", deps);
5972
- if (command && path10.win32.extname(command).toLowerCase() === ".exe") {
6149
+ if (command && path8.win32.extname(command).toLowerCase() === ".exe") {
5973
6150
  return { command, args: commandArgs, shell: false };
5974
6151
  }
5975
6152
  const packageEntry = resolveWindowsOpenCodePackageEntry(command, deps);
@@ -6013,7 +6190,7 @@ var OpenCodeDriver = class {
6013
6190
  };
6014
6191
  communication = {
6015
6192
  chat: "slock_cli",
6016
- runtimeControl: "mcp_runtime_actions"
6193
+ runtimeControl: "none"
6017
6194
  };
6018
6195
  session = {
6019
6196
  recovery: "resume_or_fresh"
@@ -6144,8 +6321,8 @@ var OpenCodeDriver = class {
6144
6321
  // src/drivers/pi.ts
6145
6322
  import { randomUUID as randomUUID3 } from "crypto";
6146
6323
  import { EventEmitter } from "events";
6147
- import { mkdirSync as mkdirSync4, readdirSync } from "fs";
6148
- import path11 from "path";
6324
+ import { mkdirSync as mkdirSync2, readdirSync } from "fs";
6325
+ import path9 from "path";
6149
6326
  import {
6150
6327
  AuthStorage,
6151
6328
  createBashTool,
@@ -6171,7 +6348,7 @@ function createPiSdkEventMappingState(sessionId = null) {
6171
6348
  };
6172
6349
  }
6173
6350
  function buildPiSessionDir(workingDirectory) {
6174
- return path11.join(workingDirectory, PI_SESSION_DIR);
6351
+ return path9.join(workingDirectory, PI_SESSION_DIR);
6175
6352
  }
6176
6353
  async function buildPiSpawnEnv(ctx) {
6177
6354
  return (await prepareCliTransport(ctx, { NO_COLOR: "1" })).spawnEnv;
@@ -6193,7 +6370,7 @@ function findPiSessionFile(sessionDir, sessionId) {
6193
6370
  }
6194
6371
  const suffix = `_${sessionId}.jsonl`;
6195
6372
  const match = entries.find((entry) => entry.endsWith(suffix));
6196
- return match ? path11.join(sessionDir, match) : null;
6373
+ return match ? path9.join(sessionDir, match) : null;
6197
6374
  }
6198
6375
  function detectPiModelsFromRegistry(modelRegistry) {
6199
6376
  const models = [];
@@ -6342,13 +6519,15 @@ var PI_RUNTIME_SESSION_DESCRIPTOR = {
6342
6519
  busyDelivery: "direct",
6343
6520
  postTurn: "keep_alive"
6344
6521
  };
6522
+ var PI_IDLE_PROMPT_RETRY_MS = 25;
6523
+ var PI_IDLE_PROMPT_MAX_WAIT_MS = 1e3;
6345
6524
  async function createPiAgentSessionForContext(ctx, sessionId) {
6346
6525
  const sessionDir = buildPiSessionDir(ctx.workingDirectory);
6347
- mkdirSync4(sessionDir, { recursive: true });
6526
+ mkdirSync2(sessionDir, { recursive: true });
6348
6527
  const spawnEnv = await buildPiSpawnEnv(ctx);
6349
6528
  const agentDir = spawnEnv.PI_CODING_AGENT_DIR || getAgentDir();
6350
- const authStorage = AuthStorage.create(path11.join(agentDir, "auth.json"));
6351
- const modelRegistry = ModelRegistry.create(authStorage, path11.join(agentDir, "models.json"));
6529
+ const authStorage = AuthStorage.create(path9.join(agentDir, "auth.json"));
6530
+ const modelRegistry = ModelRegistry.create(authStorage, path9.join(agentDir, "models.json"));
6352
6531
  const launchRuntimeFields = runtimeConfigToLaunchFields(ctx.config);
6353
6532
  const model = resolvePiModelFromRegistry(launchRuntimeFields.model, modelRegistry);
6354
6533
  if (launchRuntimeFields.model && launchRuntimeFields.model !== "default" && !model) {
@@ -6459,7 +6638,8 @@ var PiSdkRuntimeSession = class {
6459
6638
  return { ok: true, acceptedAs: "steer" };
6460
6639
  }
6461
6640
  if (session.isStreaming) {
6462
- return { ok: false, reason: "busy_rejected", error: "Pi session is still streaming" };
6641
+ this.launchPromptAfterStreaming(input.text);
6642
+ return { ok: true, acceptedAs: "prompt" };
6463
6643
  }
6464
6644
  this.launchPrompt(input.text);
6465
6645
  return { ok: true, acceptedAs: "prompt" };
@@ -6501,6 +6681,27 @@ var PiSdkRuntimeSession = class {
6501
6681
  }
6502
6682
  this.deferSdkCall(() => session.prompt(text));
6503
6683
  }
6684
+ launchPromptAfterStreaming(text) {
6685
+ this.deferSdkCall(async () => {
6686
+ const session = this.session;
6687
+ if (!session) return;
6688
+ const ready = await this.waitForStreamingToClear(session);
6689
+ if (this.didClose || this.session !== session) {
6690
+ return;
6691
+ }
6692
+ await (ready ? session.prompt(text) : session.steer(text));
6693
+ });
6694
+ }
6695
+ async waitForStreamingToClear(session) {
6696
+ const deadline = Date.now() + PI_IDLE_PROMPT_MAX_WAIT_MS;
6697
+ while (!this.didClose && this.session === session && session.isStreaming) {
6698
+ if (Date.now() >= deadline) {
6699
+ return false;
6700
+ }
6701
+ await delay(PI_IDLE_PROMPT_RETRY_MS);
6702
+ }
6703
+ return !this.didClose && this.session === session && !session.isStreaming;
6704
+ }
6504
6705
  deferSdkCall(invoke) {
6505
6706
  setImmediate(() => {
6506
6707
  if (this.didClose) return;
@@ -6612,6 +6813,12 @@ var PiDriver = class {
6612
6813
  });
6613
6814
  }
6614
6815
  };
6816
+ function delay(ms) {
6817
+ return new Promise((resolve) => {
6818
+ const timer = setTimeout(resolve, ms);
6819
+ timer.unref?.();
6820
+ });
6821
+ }
6615
6822
 
6616
6823
  // src/drivers/runtimeSession.ts
6617
6824
  import { EventEmitter as EventEmitter2 } from "events";
@@ -6770,7 +6977,7 @@ function getDriver(runtimeId) {
6770
6977
 
6771
6978
  // src/workspaces.ts
6772
6979
  import { readdir, rm, stat } from "fs/promises";
6773
- import path12 from "path";
6980
+ import path10 from "path";
6774
6981
  function isValidWorkspaceDirectoryName(directoryName) {
6775
6982
  return !directoryName.includes("/") && !directoryName.includes("\\") && !directoryName.includes("..");
6776
6983
  }
@@ -6778,7 +6985,7 @@ function resolveWorkspaceDirectoryPath(dataDir, directoryName) {
6778
6985
  if (!isValidWorkspaceDirectoryName(directoryName)) {
6779
6986
  return null;
6780
6987
  }
6781
- return path12.join(dataDir, directoryName);
6988
+ return path10.join(dataDir, directoryName);
6782
6989
  }
6783
6990
  function emptyWorkspaceDirectorySummary(latestMtime = /* @__PURE__ */ new Date(0)) {
6784
6991
  return {
@@ -6827,7 +7034,7 @@ async function summarizeWorkspaceDirectory(dirPath) {
6827
7034
  return summary;
6828
7035
  }
6829
7036
  const childSummaries = await Promise.all(
6830
- entries.map((entry) => summarizeWorkspaceEntry(path12.join(dirPath, entry.name), entry))
7037
+ entries.map((entry) => summarizeWorkspaceEntry(path10.join(dirPath, entry.name), entry))
6831
7038
  );
6832
7039
  for (const childSummary of childSummaries) {
6833
7040
  summary = mergeWorkspaceDirectorySummaries(summary, childSummary);
@@ -6846,7 +7053,7 @@ async function scanWorkspaceDirectories(dataDir) {
6846
7053
  if (!entry.isDirectory()) {
6847
7054
  return null;
6848
7055
  }
6849
- const dirPath = path12.join(dataDir, entry.name);
7056
+ const dirPath = path10.join(dataDir, entry.name);
6850
7057
  try {
6851
7058
  const summary = await summarizeWorkspaceDirectory(dirPath);
6852
7059
  return {
@@ -7116,6 +7323,11 @@ var RuntimeNotificationState = class {
7116
7323
  clearPending() {
7117
7324
  this.pendingCountValue = 0;
7118
7325
  }
7326
+ remove(count) {
7327
+ if (!Number.isFinite(count) || count <= 0) return this.pendingCountValue;
7328
+ this.pendingCountValue = Math.max(0, this.pendingCountValue - Math.floor(count));
7329
+ return this.pendingCountValue;
7330
+ }
7119
7331
  clearTimer() {
7120
7332
  if (this.timerValue) {
7121
7333
  clearTimeout(this.timerValue);
@@ -7304,12 +7516,12 @@ function findSessionJsonl(root, predicate) {
7304
7516
  for (const entry of entries) {
7305
7517
  if (++visited > maxEntries) return null;
7306
7518
  if (!entry.isFile() || !predicate(entry.name)) continue;
7307
- return path13.join(dir, entry.name);
7519
+ return path11.join(dir, entry.name);
7308
7520
  }
7309
7521
  for (const entry of entries) {
7310
7522
  if (++visited > maxEntries) return null;
7311
7523
  if (!entry.isDirectory()) continue;
7312
- const found = visit(path13.join(dir, entry.name), depth - 1);
7524
+ const found = visit(path11.join(dir, entry.name), depth - 1);
7313
7525
  if (found) return found;
7314
7526
  }
7315
7527
  return null;
@@ -7322,10 +7534,10 @@ function safeSessionFilename(value) {
7322
7534
  }
7323
7535
  function writeRuntimeSessionHandoff(runtime, sessionId, fallbackDir) {
7324
7536
  try {
7325
- const dir = path13.join(fallbackDir, ".slock", "runtime-sessions");
7326
- mkdirSync5(dir, { recursive: true });
7327
- const filePath = path13.join(dir, `${runtime}-${safeSessionFilename(sessionId)}.jsonl`);
7328
- writeFileSync7(filePath, JSON.stringify({
7537
+ const dir = path11.join(fallbackDir, ".slock", "runtime-sessions");
7538
+ mkdirSync3(dir, { recursive: true });
7539
+ const filePath = path11.join(dir, `${runtime}-${safeSessionFilename(sessionId)}.jsonl`);
7540
+ writeFileSync4(filePath, JSON.stringify({
7329
7541
  type: "runtime_session_handoff",
7330
7542
  runtime,
7331
7543
  sessionId,
@@ -7344,7 +7556,7 @@ function writeRuntimeSessionHandoff(runtime, sessionId, fallbackDir) {
7344
7556
  }
7345
7557
  }
7346
7558
  function resolveRuntimeSessionRef(runtime, sessionId, homeDir = os5.homedir(), fallbackDir) {
7347
- const directPath = path13.isAbsolute(sessionId) ? sessionId : null;
7559
+ const directPath = path11.isAbsolute(sessionId) ? sessionId : null;
7348
7560
  if (directPath) {
7349
7561
  try {
7350
7562
  if (statSync(directPath).isFile()) {
@@ -7353,7 +7565,7 @@ function resolveRuntimeSessionRef(runtime, sessionId, homeDir = os5.homedir(), f
7353
7565
  } catch {
7354
7566
  }
7355
7567
  }
7356
- const resolvedPath = runtime === "claude" ? findSessionJsonl(path13.join(homeDir, ".claude", "projects"), (filename) => filename === `${sessionId}.jsonl`) : runtime === "codex" ? findSessionJsonl(path13.join(homeDir, ".codex", "sessions"), (filename) => filename.endsWith(".jsonl") && filename.includes(sessionId)) : null;
7568
+ 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;
7357
7569
  if (!resolvedPath && fallbackDir) {
7358
7570
  const fallback = writeRuntimeSessionHandoff(runtime, sessionId, fallbackDir);
7359
7571
  if (fallback) return fallback;
@@ -7956,8 +8168,7 @@ function runtimeProfileTurnControl(kind, key, source) {
7956
8168
  source,
7957
8169
  keyHash: hashRuntimeProfileKey(key) ?? null,
7958
8170
  keyPresent: Boolean(key),
7959
- injectedAtMs: Date.now(),
7960
- migrationDoneToolObserved: false
8171
+ injectedAtMs: Date.now()
7961
8172
  };
7962
8173
  }
7963
8174
  function pushRecentLines(lines, chunk, maxLines, maxLineLength) {
@@ -8033,6 +8244,9 @@ function classifyStickyTerminalFailure(ap) {
8033
8244
  if (isRuntimeStartTimeoutText(terminalFailure.detail)) return null;
8034
8245
  return null;
8035
8246
  }
8247
+ function isAuthClassTerminalLine(text) {
8248
+ return buildRuntimeErrorDiagnosticEnvelope(text).spanAttrs.runtime_error_action_required === true;
8249
+ }
8036
8250
  function isProviderStreamFailureText(text) {
8037
8251
  return /stream closed before response\.completed|error decoding response body/i.test(text);
8038
8252
  }
@@ -8314,6 +8528,29 @@ var NATIVE_STANDING_PROMPT_STARTUP_INPUT = (
8314
8528
  );
8315
8529
  var AgentProcessManager = class _AgentProcessManager {
8316
8530
  agents = /* @__PURE__ */ new Map();
8531
+ /**
8532
+ * Per-agent monotonic clientSeq for `agent:activity` upstream dedup.
8533
+ * Server dedupes ingest by `(launchId, clientSeq)` (#engineering:72283cf7
8534
+ * task #340 PR B). This counter is keyed by agentId at the MANAGER level —
8535
+ * NOT on the per-process `AgentProcess` — so it SURVIVES process recreation
8536
+ * (cold-start-resume / idle-auto-restart / queued-continuation /
8537
+ * runtime-profile restart). Those daemon-internal restarts reuse the
8538
+ * server-issued launchId but spawn a fresh process; if the counter reset to
8539
+ * 0 (as the old per-`ap` field did) the reused launchId's `(launchId,
8540
+ * clientSeq)` window would collide and the server would drop the
8541
+ * post-restart activity as stale — the "activity log stops updating after
8542
+ * error→restart" bug. Keeping it monotonic-per-agent and never resetting on
8543
+ * respawn guarantees a fresh dedup key under the same launchId. launchId is
8544
+ * decoupled: it stays the identity/gate token, this is the clock.
8545
+ * (#proj-o11y:a1e54b59 — RS dedup-epoch ⊥ launchId-identity separation.)
8546
+ */
8547
+ activityClientSeqByAgent = /* @__PURE__ */ new Map();
8548
+ /** Next monotonic `agent:activity` clientSeq for this agent (never resets on respawn). */
8549
+ nextActivityClientSeq(agentId) {
8550
+ const next = (this.activityClientSeqByAgent.get(agentId) ?? 0) + 1;
8551
+ this.activityClientSeqByAgent.set(agentId, next);
8552
+ return next;
8553
+ }
8317
8554
  agentsStarting = /* @__PURE__ */ new Set();
8318
8555
  // Prevent concurrent starts of same agent
8319
8556
  queuedAgentStarts = /* @__PURE__ */ new Map();
@@ -8327,7 +8564,6 @@ var AgentProcessManager = class _AgentProcessManager {
8327
8564
  startingInboxes = /* @__PURE__ */ new Map();
8328
8565
  /** Cached configs for agents whose process exited normally — enables auto-restart on next message */
8329
8566
  idleAgentConfigs = /* @__PURE__ */ new Map();
8330
- chatBridgePath;
8331
8567
  slockCliPath;
8332
8568
  sendToServer;
8333
8569
  daemonApiKey;
@@ -8345,8 +8581,7 @@ var AgentProcessManager = class _AgentProcessManager {
8345
8581
  agentVisibleMessageIds = /* @__PURE__ */ new Map();
8346
8582
  daemonVersion;
8347
8583
  computerVersion;
8348
- constructor(chatBridgePath, sendToServer, daemonApiKey, opts) {
8349
- this.chatBridgePath = chatBridgePath;
8584
+ constructor(sendToServer, daemonApiKey, opts) {
8350
8585
  this.slockCliPath = opts.slockCliPath ?? "";
8351
8586
  this.sendToServer = sendToServer;
8352
8587
  this.daemonApiKey = daemonApiKey;
@@ -8501,6 +8736,48 @@ var AgentProcessManager = class _AgentProcessManager {
8501
8736
  suppressed_pending_count: removedActive + removedStarting
8502
8737
  });
8503
8738
  }
8739
+ purgeInboxMessagesForChannels(agentId, channelIds, reason = "server_purge") {
8740
+ const channelIdSet = new Set(channelIds.filter((id) => typeof id === "string" && id.length > 0));
8741
+ if (channelIdSet.size === 0) return { removedCount: 0 };
8742
+ const purge = (messages) => {
8743
+ if (!messages || messages.length === 0) return 0;
8744
+ let removed = 0;
8745
+ const retained = messages.filter((message) => {
8746
+ const shouldRemove = channelIdSet.has(message.channel_id);
8747
+ if (shouldRemove) removed += 1;
8748
+ return !shouldRemove;
8749
+ });
8750
+ if (removed > 0) {
8751
+ messages.splice(0, messages.length, ...retained);
8752
+ }
8753
+ return removed;
8754
+ };
8755
+ const active = this.agents.get(agentId);
8756
+ const removedActive = purge(active?.inbox);
8757
+ const startingInbox = this.startingInboxes.get(agentId);
8758
+ const removedStarting = purge(startingInbox);
8759
+ if (startingInbox && startingInbox.length === 0) {
8760
+ this.startingInboxes.delete(agentId);
8761
+ }
8762
+ if (active && removedActive > 0) {
8763
+ active.notifications.remove(removedActive);
8764
+ if (active.inbox.length === 0) {
8765
+ active.notifications.clear();
8766
+ }
8767
+ }
8768
+ const removedCount = removedActive + removedStarting;
8769
+ this.recordDaemonTrace("daemon.agent.inbox.purged", {
8770
+ agentId,
8771
+ reason,
8772
+ channel_count: channelIdSet.size,
8773
+ removed_active_count: removedActive,
8774
+ removed_starting_count: removedStarting,
8775
+ removed_count: removedCount,
8776
+ active_inbox_count: active?.inbox.length ?? 0,
8777
+ starting_inbox_count: this.startingInboxes.get(agentId)?.length ?? 0
8778
+ });
8779
+ return { removedCount };
8780
+ }
8504
8781
  createAgentProxyInboxCoordinator(agentId) {
8505
8782
  return {
8506
8783
  getBoundary: (target) => this.getVisibleBoundary(agentId, target),
@@ -8578,7 +8855,6 @@ var AgentProcessManager = class _AgentProcessManager {
8578
8855
  target: input.target,
8579
8856
  messageCount
8580
8857
  }).entry;
8581
- if (ap) ap.activityClientSeq += 1;
8582
8858
  this.sendToServer({
8583
8859
  type: "agent:activity",
8584
8860
  agentId,
@@ -8586,7 +8862,7 @@ var AgentProcessManager = class _AgentProcessManager {
8586
8862
  detail: ap?.lastActivityDetail || "",
8587
8863
  entries: [entry],
8588
8864
  launchId: ap?.launchId || void 0,
8589
- clientSeq: ap?.activityClientSeq
8865
+ clientSeq: ap ? this.nextActivityClientSeq(agentId) : void 0
8590
8866
  });
8591
8867
  }
8592
8868
  recordDaemonTrace(name, attrs, status = "ok", parentTraceparent) {
@@ -8832,7 +9108,7 @@ var AgentProcessManager = class _AgentProcessManager {
8832
9108
  );
8833
9109
  wakeMessage = void 0;
8834
9110
  }
8835
- const agentDataDir = path13.join(this.dataDir, agentId);
9111
+ const agentDataDir = path11.join(this.dataDir, agentId);
8836
9112
  await mkdir(agentDataDir, { recursive: true });
8837
9113
  let runtimeConfig = withLocalRuntimeContext(config, agentId, agentDataDir);
8838
9114
  const legacyRuntimeProfileControl = runtimeConfig.runtimeProfileControl?.kind === "migration" ? runtimeConfig.runtimeProfileControl : null;
@@ -8846,23 +9122,23 @@ var AgentProcessManager = class _AgentProcessManager {
8846
9122
  );
8847
9123
  runtimeConfig = { ...runtimeConfig, runtimeProfileControl: null };
8848
9124
  }
8849
- const memoryMdPath = path13.join(agentDataDir, "MEMORY.md");
9125
+ const memoryMdPath = path11.join(agentDataDir, "MEMORY.md");
8850
9126
  try {
8851
9127
  await access(memoryMdPath);
8852
9128
  } catch {
8853
9129
  const initialMemoryMd = buildInitialMemoryMd(runtimeConfig);
8854
9130
  await writeFile(memoryMdPath, initialMemoryMd);
8855
9131
  }
8856
- const notesDir = path13.join(agentDataDir, "notes");
9132
+ const notesDir = path11.join(agentDataDir, "notes");
8857
9133
  await mkdir(notesDir, { recursive: true });
8858
9134
  if (getOnboardingSeedMode(config) === FIRST_CINDY_SEED_MODE) {
8859
9135
  const seedFiles = buildOnboardingSeedFiles();
8860
9136
  for (const { relativePath, content } of seedFiles) {
8861
- const fullPath = path13.join(agentDataDir, relativePath);
9137
+ const fullPath = path11.join(agentDataDir, relativePath);
8862
9138
  try {
8863
9139
  await access(fullPath);
8864
9140
  } catch {
8865
- await mkdir(path13.dirname(fullPath), { recursive: true });
9141
+ await mkdir(path11.dirname(fullPath), { recursive: true });
8866
9142
  await writeFile(fullPath, content);
8867
9143
  }
8868
9144
  }
@@ -8970,7 +9246,6 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
8970
9246
  standingPrompt,
8971
9247
  prompt,
8972
9248
  workingDirectory: agentDataDir,
8973
- chatBridgePath: this.chatBridgePath,
8974
9249
  slockCliPath: this.slockCliPath,
8975
9250
  daemonApiKey: this.daemonApiKey,
8976
9251
  launchId: launchId || null,
@@ -8999,7 +9274,6 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
8999
9274
  runtimeTraceCounters: createRuntimeTraceCounters(),
9000
9275
  lastActivity: "",
9001
9276
  lastActivityDetail: "",
9002
- activityClientSeq: 0,
9003
9277
  recentStdout: [],
9004
9278
  recentStderr: [],
9005
9279
  lastRuntimeError: null,
@@ -9118,7 +9392,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
9118
9392
  const startupTimeoutTermination = ap.expectedTerminationReason === "startup_timeout";
9119
9393
  const expectedTermination = Boolean(ap.expectedTerminationReason);
9120
9394
  const stickyTerminalFailureDetail = classifyStickyTerminalFailure(ap);
9121
- const processEndedCleanly = !stickyTerminalFailureDetail && (finalCode === 0 || expectedTermination && !ap.lastRuntimeError);
9395
+ const processEndedCleanly = !stickyTerminalFailureDetail && !startupTimeoutTermination && (finalCode === 0 || expectedTermination && !ap.lastRuntimeError);
9122
9396
  const terminalFailureDetail = processEndedCleanly ? null : stickyTerminalFailureDetail ?? classifyTerminalFailure(ap);
9123
9397
  const resumeRecoveryReason = resumeSessionRecoveryReason(ap);
9124
9398
  const shouldColdStartResumeSession = resumeRecoveryReason !== null;
@@ -9527,6 +9801,9 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
9527
9801
  cleanupAgentCredentialProxy(agentId, ap.launchId);
9528
9802
  this.revokeManagedRunnerCredential(agentId, ap.config, ap.launchId);
9529
9803
  this.agents.delete(agentId);
9804
+ if (!silent) {
9805
+ this.activityClientSeqByAgent.delete(agentId);
9806
+ }
9530
9807
  this.runtimeExitTraceAttrs.set(ap.runtime, {
9531
9808
  stop_source: silent ? "daemon_internal" : "explicit_request",
9532
9809
  stop_wait_requested: wait,
@@ -9538,7 +9815,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
9538
9815
  reason: silent ? "daemon_internal" : "explicit_request"
9539
9816
  });
9540
9817
  if (!silent) {
9541
- this.sendRuntimeProfileReportFor(agentId, ap.config, ap.sessionId, ap.launchId);
9818
+ this.sendRuntimeProfileReportFor(agentId, ap.config, ap.sessionId, ap.launchId, "stop");
9542
9819
  this.sendAgentStatus(agentId, "inactive", ap.launchId);
9543
9820
  this.broadcastActivity(agentId, "offline", "Stopped");
9544
9821
  logger.info(`[Agent ${agentId}] Stopped by request`);
@@ -9681,20 +9958,37 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
9681
9958
  }));
9682
9959
  return true;
9683
9960
  }
9684
- ap.inbox.push(message);
9685
- this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
9686
- outcome: "queued_terminal_runtime_error",
9687
- accepted: true,
9688
- process_present: true,
9689
- runtime: ap.config.runtime,
9690
- session_id_present: Boolean(ap.sessionId),
9691
- launchId: ap.launchId || void 0,
9692
- is_idle: ap.isIdle,
9693
- inbox_count: ap.inbox.length
9694
- }));
9695
- this.sendAgentStatus(agentId, "inactive", ap.launchId);
9696
- this.broadcastActivity(agentId, "error", stickyTerminalFailure.detail);
9697
- return true;
9961
+ if (stickyTerminalFailure.actionRequired) {
9962
+ if (ap.lastRuntimeError && isAuthClassTerminalLine(ap.lastRuntimeError)) {
9963
+ ap.lastRuntimeError = null;
9964
+ }
9965
+ ap.recentStderr = ap.recentStderr.filter((line) => !isAuthClassTerminalLine(line));
9966
+ this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
9967
+ outcome: "user_turn_recover_from_sticky_terminal_error",
9968
+ accepted: true,
9969
+ process_present: true,
9970
+ runtime: ap.config.runtime,
9971
+ session_id_present: Boolean(ap.sessionId),
9972
+ launchId: ap.launchId || void 0,
9973
+ is_idle: ap.isIdle,
9974
+ inbox_count: ap.inbox.length
9975
+ }));
9976
+ } else {
9977
+ ap.inbox.push(message);
9978
+ this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
9979
+ outcome: "queued_terminal_runtime_error",
9980
+ accepted: true,
9981
+ process_present: true,
9982
+ runtime: ap.config.runtime,
9983
+ session_id_present: Boolean(ap.sessionId),
9984
+ launchId: ap.launchId || void 0,
9985
+ is_idle: ap.isIdle,
9986
+ inbox_count: ap.inbox.length
9987
+ }));
9988
+ this.sendAgentStatus(agentId, "inactive", ap.launchId);
9989
+ this.broadcastActivity(agentId, "error", stickyTerminalFailure.detail);
9990
+ return true;
9991
+ }
9698
9992
  }
9699
9993
  if (ap.isIdle && ap.driver.supportsStdinNotification && ap.sessionId) {
9700
9994
  if (transientDelivery) {
@@ -9862,7 +10156,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
9862
10156
  return true;
9863
10157
  }
9864
10158
  async resetWorkspace(agentId) {
9865
- const agentDataDir = path13.join(this.dataDir, agentId);
10159
+ const agentDataDir = path11.join(this.dataDir, agentId);
9866
10160
  try {
9867
10161
  await rm2(agentDataDir, { recursive: true, force: true });
9868
10162
  logger.info(`[Agent ${agentId}] Workspace reset complete (${agentDataDir})`);
@@ -9923,7 +10217,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
9923
10217
  return result;
9924
10218
  }
9925
10219
  buildRuntimeProfileReport(agentId, config, sessionId, launchId) {
9926
- const workspacePath = path13.join(this.dataDir, agentId);
10220
+ const workspacePath = path11.join(this.dataDir, agentId);
9927
10221
  return {
9928
10222
  agentId,
9929
10223
  launchId,
@@ -10141,7 +10435,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
10141
10435
  });
10142
10436
  span.end("ok", { attrs: { outcome: "agent_config_ack_sent" } });
10143
10437
  }
10144
- sendRuntimeProfileWireReport(report) {
10438
+ sendRuntimeProfileWireReport(report, source) {
10145
10439
  const span = this.tracer.startSpan("daemon.runtime_profile.report.sent", {
10146
10440
  surface: "daemon",
10147
10441
  kind: "producer",
@@ -10149,6 +10443,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
10149
10443
  agentId: report.agentId,
10150
10444
  launchId: report.launchId || void 0,
10151
10445
  runtime: report.facts.runtime,
10446
+ report_source: source,
10152
10447
  model_present: Boolean(report.facts.model),
10153
10448
  session_ref_present: Boolean(report.facts.sessionRef),
10154
10449
  workspace_ref_present: Boolean(report.facts.workspaceRef || report.facts.workspacePathRef)
@@ -10159,17 +10454,18 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
10159
10454
  agentId: report.agentId,
10160
10455
  facts: report.facts,
10161
10456
  launchId: report.launchId || void 0,
10162
- traceparent: formatTraceparent(span.context)
10457
+ traceparent: formatTraceparent(span.context),
10458
+ source
10163
10459
  });
10164
10460
  span.end("ok");
10165
10461
  }
10166
- sendRuntimeProfileReportFor(agentId, config, sessionId, launchId) {
10167
- this.sendRuntimeProfileWireReport(this.buildRuntimeProfileReport(agentId, config, sessionId, launchId));
10462
+ sendRuntimeProfileReportFor(agentId, config, sessionId, launchId, source) {
10463
+ this.sendRuntimeProfileWireReport(this.buildRuntimeProfileReport(agentId, config, sessionId, launchId), source);
10168
10464
  }
10169
- sendRuntimeProfileReport(agentId) {
10465
+ sendRuntimeProfileReport(agentId, source) {
10170
10466
  const report = this.getAgentRuntimeProfileReport(agentId);
10171
10467
  if (!report) return;
10172
- this.sendRuntimeProfileWireReport(report);
10468
+ this.sendRuntimeProfileWireReport(report, source);
10173
10469
  }
10174
10470
  // Machine-level workspace scanning
10175
10471
  async scanAllWorkspaces() {
@@ -10180,7 +10476,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
10180
10476
  }
10181
10477
  // Workspace file browsing
10182
10478
  async getFileTree(agentId, dirPath) {
10183
- const agentDir = path13.join(this.dataDir, agentId);
10479
+ const agentDir = path11.join(this.dataDir, agentId);
10184
10480
  try {
10185
10481
  await stat2(agentDir);
10186
10482
  } catch {
@@ -10188,8 +10484,8 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
10188
10484
  }
10189
10485
  let targetDir = agentDir;
10190
10486
  if (dirPath) {
10191
- const resolved = path13.resolve(agentDir, dirPath);
10192
- if (!resolved.startsWith(agentDir + path13.sep) && resolved !== agentDir) {
10487
+ const resolved = path11.resolve(agentDir, dirPath);
10488
+ if (!resolved.startsWith(agentDir + path11.sep) && resolved !== agentDir) {
10193
10489
  return [];
10194
10490
  }
10195
10491
  targetDir = resolved;
@@ -10197,14 +10493,14 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
10197
10493
  return this.listDirectoryChildren(targetDir, agentDir);
10198
10494
  }
10199
10495
  async readFile(agentId, filePath) {
10200
- const agentDir = path13.join(this.dataDir, agentId);
10201
- const resolved = path13.resolve(agentDir, filePath);
10202
- if (!resolved.startsWith(agentDir + path13.sep) && resolved !== agentDir) {
10496
+ const agentDir = path11.join(this.dataDir, agentId);
10497
+ const resolved = path11.resolve(agentDir, filePath);
10498
+ if (!resolved.startsWith(agentDir + path11.sep) && resolved !== agentDir) {
10203
10499
  throw new Error("Access denied");
10204
10500
  }
10205
10501
  const info = await stat2(resolved);
10206
10502
  if (info.isDirectory()) throw new Error("Cannot read a directory");
10207
- const ext = path13.extname(resolved).toLowerCase();
10503
+ const ext = path11.extname(resolved).toLowerCase();
10208
10504
  if (WORKSPACE_TEXT_EXTENSIONS.has(ext) || ext === "") {
10209
10505
  if (info.size > WORKSPACE_TEXT_FILE_MAX_BYTES) throw new Error("File too large");
10210
10506
  const content = await readFile(resolved, "utf-8");
@@ -10239,13 +10535,13 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
10239
10535
  const agent = this.agents.get(agentId);
10240
10536
  const runtime = runtimeHint || agent?.config.runtime || "claude";
10241
10537
  const home = os5.homedir();
10242
- const workspaceDir = path13.join(this.dataDir, agentId);
10538
+ const workspaceDir = path11.join(this.dataDir, agentId);
10243
10539
  const paths = _AgentProcessManager.SKILL_PATHS[runtime] || _AgentProcessManager.SKILL_PATHS.claude;
10244
10540
  const globalResults = await Promise.all(
10245
- paths.global.map((p) => this.scanSkillsDir(path13.join(home, p)))
10541
+ paths.global.map((p) => this.scanSkillsDir(path11.join(home, p)))
10246
10542
  );
10247
10543
  const workspaceResults = await Promise.all(
10248
- paths.workspace.map((p) => this.scanSkillsDir(path13.join(workspaceDir, p)))
10544
+ paths.workspace.map((p) => this.scanSkillsDir(path11.join(workspaceDir, p)))
10249
10545
  );
10250
10546
  const dedup = (skills) => {
10251
10547
  const seen = /* @__PURE__ */ new Set();
@@ -10274,7 +10570,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
10274
10570
  const skills = [];
10275
10571
  for (const entry of entries) {
10276
10572
  if (entry.isDirectory() || entry.isSymbolicLink()) {
10277
- const skillMd = path13.join(dir, entry.name, "SKILL.md");
10573
+ const skillMd = path11.join(dir, entry.name, "SKILL.md");
10278
10574
  try {
10279
10575
  const content = await readFile(skillMd, "utf-8");
10280
10576
  const skill = this.parseSkillMd(entry.name, content);
@@ -10285,7 +10581,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
10285
10581
  } else if (entry.name.endsWith(".md")) {
10286
10582
  const cmdName = entry.name.replace(/\.md$/, "");
10287
10583
  try {
10288
- const content = await readFile(path13.join(dir, entry.name), "utf-8");
10584
+ const content = await readFile(path11.join(dir, entry.name), "utf-8");
10289
10585
  const skill = this.parseSkillMd(cmdName, content);
10290
10586
  skill.sourcePath = dir;
10291
10587
  skills.push(skill);
@@ -10334,7 +10630,6 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
10334
10630
  if (!hasToolStart) {
10335
10631
  entries.push({ kind: "status", activity, detail });
10336
10632
  }
10337
- if (ap) ap.activityClientSeq += 1;
10338
10633
  this.sendToServer({
10339
10634
  type: "agent:activity",
10340
10635
  agentId,
@@ -10342,7 +10637,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
10342
10637
  detail,
10343
10638
  entries,
10344
10639
  launchId: launchIdOverride || ap?.launchId || void 0,
10345
- clientSeq: ap?.activityClientSeq
10640
+ clientSeq: ap ? this.nextActivityClientSeq(agentId) : void 0
10346
10641
  });
10347
10642
  if (ap) {
10348
10643
  ap.lastActivity = activity;
@@ -10354,14 +10649,13 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
10354
10649
  this.recordRuntimeTraceEvent(agentId, ap, "activity.heartbeat.sent", {
10355
10650
  activity: ap.lastActivity
10356
10651
  });
10357
- ap.activityClientSeq += 1;
10358
10652
  this.sendToServer({
10359
10653
  type: "agent:activity",
10360
10654
  agentId,
10361
10655
  activity: ap.lastActivity,
10362
10656
  detail: ap.lastActivityDetail,
10363
10657
  launchId: launchIdOverride || ap.launchId || void 0,
10364
- clientSeq: ap.activityClientSeq
10658
+ clientSeq: this.nextActivityClientSeq(agentId)
10365
10659
  });
10366
10660
  }, ACTIVITY_HEARTBEAT_MS);
10367
10661
  }
@@ -10398,7 +10692,6 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
10398
10692
  const ap = this.agents.get(agentId);
10399
10693
  const activity = ap?.lastActivity || "offline";
10400
10694
  const detail = ap?.lastActivityDetail || (ap ? "" : "Agent not running");
10401
- if (ap) ap.activityClientSeq += 1;
10402
10695
  this.sendToServer({
10403
10696
  type: "agent:activity",
10404
10697
  agentId,
@@ -10406,7 +10699,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
10406
10699
  detail,
10407
10700
  launchId: ap?.launchId || void 0,
10408
10701
  probeId,
10409
- clientSeq: ap?.activityClientSeq
10702
+ clientSeq: ap ? this.nextActivityClientSeq(agentId) : void 0
10410
10703
  });
10411
10704
  }
10412
10705
  flushPendingTrajectory(agentId) {
@@ -10608,8 +10901,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
10608
10901
  runtime_profile_key_hash: control.keyHash || void 0,
10609
10902
  runtime_profile_key_present: control.keyPresent,
10610
10903
  runtime_profile_pending_age_ms: pendingAgeMs,
10611
- runtime_profile_requires_ack: false,
10612
- runtime_profile_migration_done_observed: control.kind === "migration" ? control.migrationDoneToolObserved : void 0
10904
+ runtime_profile_requires_ack: false
10613
10905
  };
10614
10906
  }
10615
10907
  activateRuntimeProfileTurnControl(ap, control) {
@@ -10621,16 +10913,6 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
10621
10913
  if (!notification) return null;
10622
10914
  return runtimeProfileTurnControl(notification.kind, notification.key, source);
10623
10915
  }
10624
- noteRuntimeProfileToolCall(agentId, ap, toolName) {
10625
- const control = ap.runtimeProfileTurnControl;
10626
- if (!control || control.kind !== "migration") return;
10627
- if (!toolName.includes("runtime_profile_migration_done")) return;
10628
- control.migrationDoneToolObserved = true;
10629
- this.recordRuntimeTraceEvent(agentId, ap, "runtime_profile.migration_done_tool.observed", {
10630
- ...this.runtimeProfileTurnControlTraceAttrs(control),
10631
- tool: toolName
10632
- });
10633
- }
10634
10916
  finalizeRuntimeProfileTurnControl(agentId, ap, terminal) {
10635
10917
  const control = ap.runtimeProfileTurnControl;
10636
10918
  if (!control) return {};
@@ -10639,7 +10921,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
10639
10921
  return {
10640
10922
  ...attrs,
10641
10923
  runtime_profile_turn_terminal: terminal,
10642
- runtime_profile_turn_outcome: control.kind === "migration" ? control.migrationDoneToolObserved ? "deprecated_migration_done_observed" : "reset_session_notice" : "notice_only"
10924
+ runtime_profile_turn_outcome: control.kind === "migration" ? "reset_session_notice" : "notice_only"
10643
10925
  };
10644
10926
  }
10645
10927
  startRuntimeTrace(agentId, ap, reason, messages, inputTraceAttrs = {}) {
@@ -11018,7 +11300,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
11018
11300
  case "session_init":
11019
11301
  if (ap) ap.sessionId = event.sessionId;
11020
11302
  this.sendToServer({ type: "agent:session", agentId, sessionId: event.sessionId, launchId: ap?.launchId || void 0 });
11021
- this.sendRuntimeProfileReport(agentId);
11303
+ this.sendRuntimeProfileReport(agentId, "session_init");
11022
11304
  break;
11023
11305
  case "thinking": {
11024
11306
  this.completeCompactionIfActive(agentId, "Context compaction finished (inferred from resumed output)");
@@ -11044,7 +11326,6 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
11044
11326
  const invocation = normalizeToolDisplayInvocation(event.name, event.input);
11045
11327
  if (ap) {
11046
11328
  const reduction = reduceApmGatedToolUse(ap.gatedSteering, { kind: "tool_call" });
11047
- this.noteRuntimeProfileToolCall(agentId, ap, invocation.toolName);
11048
11329
  this.recordRuntimeTraceEvent(agentId, ap, "tool.call.started", { tool: invocation.toolName });
11049
11330
  this.commitGatedSteeringDecisionState(agentId, ap, reduction.nextState, {
11050
11331
  event: "tool_call",
@@ -11157,7 +11438,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
11157
11438
  }
11158
11439
  if (event.sessionId) {
11159
11440
  this.sendToServer({ type: "agent:session", agentId, sessionId: event.sessionId, launchId: ap?.launchId || void 0 });
11160
- this.sendRuntimeProfileReport(agentId);
11441
+ this.sendRuntimeProfileReport(agentId, "turn_end");
11161
11442
  }
11162
11443
  break;
11163
11444
  case "error": {
@@ -11625,8 +11906,8 @@ ${RESPONSE_TARGET_HINT}`);
11625
11906
  const nodes = [];
11626
11907
  for (const entry of entries) {
11627
11908
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
11628
- const fullPath = path13.join(dir, entry.name);
11629
- const relativePath = path13.relative(rootDir, fullPath);
11909
+ const fullPath = path11.join(dir, entry.name);
11910
+ const relativePath = path11.relative(rootDir, fullPath);
11630
11911
  let info;
11631
11912
  try {
11632
11913
  info = await stat2(fullPath);
@@ -11917,7 +12198,7 @@ var ReminderCache = class {
11917
12198
  logger.warn(`[ReminderCache] Invalid fireAt for ${job.reminderId}: ${job.fireAt}`);
11918
12199
  return null;
11919
12200
  }
11920
- const delay = Math.max(0, Math.min(this.maxDelayMs, fireAt - this.clock.now()));
12201
+ const delay2 = Math.max(0, Math.min(this.maxDelayMs, fireAt - this.clock.now()));
11921
12202
  return this.clock.setTimeout(() => {
11922
12203
  this.entries.delete(job.reminderId);
11923
12204
  try {
@@ -11925,15 +12206,15 @@ var ReminderCache = class {
11925
12206
  } catch (err) {
11926
12207
  logger.error(`[ReminderCache] onFire threw for ${job.reminderId}`, err);
11927
12208
  }
11928
- }, delay);
12209
+ }, delay2);
11929
12210
  }
11930
12211
  };
11931
12212
 
11932
12213
  // src/machineLock.ts
11933
12214
  import { createHash as createHash4, randomUUID as randomUUID4 } from "crypto";
11934
- import { mkdirSync as mkdirSync6, readFileSync as readFileSync5, rmSync as rmSync3, statSync as statSync2, writeFileSync as writeFileSync8 } from "fs";
12215
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync5, rmSync as rmSync3, statSync as statSync2, writeFileSync as writeFileSync5 } from "fs";
11935
12216
  import os6 from "os";
11936
- import path14 from "path";
12217
+ import path12 from "path";
11937
12218
  var INCOMPLETE_LOCK_STALE_MS = 3e4;
11938
12219
  var DaemonMachineLockConflictError = class extends Error {
11939
12220
  code = "DAEMON_MACHINE_LOCK_HELD";
@@ -11955,7 +12236,7 @@ function resolveDefaultMachineStateRoot() {
11955
12236
  return resolveSlockHomePath("machines");
11956
12237
  }
11957
12238
  function ownerPath(lockDir) {
11958
- return path14.join(lockDir, "owner.json");
12239
+ return path12.join(lockDir, "owner.json");
11959
12240
  }
11960
12241
  function readOwner(lockDir) {
11961
12242
  try {
@@ -11985,13 +12266,13 @@ function acquireDaemonMachineLock(options) {
11985
12266
  const rootDir = options.rootDir ?? resolveDefaultMachineStateRoot();
11986
12267
  const fingerprint = apiKeyFingerprint(options.apiKey);
11987
12268
  const lockId = getDaemonMachineLockId(options.apiKey);
11988
- const machineDir = path14.join(rootDir, lockId);
11989
- const lockDir = path14.join(machineDir, "daemon.lock");
12269
+ const machineDir = path12.join(rootDir, lockId);
12270
+ const lockDir = path12.join(machineDir, "daemon.lock");
11990
12271
  const token = randomUUID4();
11991
- mkdirSync6(machineDir, { recursive: true });
12272
+ mkdirSync4(machineDir, { recursive: true });
11992
12273
  for (let attempt = 0; attempt < 2; attempt += 1) {
11993
12274
  try {
11994
- mkdirSync6(lockDir);
12275
+ mkdirSync4(lockDir);
11995
12276
  const owner = {
11996
12277
  pid: process.pid,
11997
12278
  token,
@@ -12001,7 +12282,7 @@ function acquireDaemonMachineLock(options) {
12001
12282
  apiKeyFingerprint: fingerprint.slice(0, 16)
12002
12283
  };
12003
12284
  try {
12004
- writeFileSync8(ownerPath(lockDir), `${JSON.stringify(owner, null, 2)}
12285
+ writeFileSync5(ownerPath(lockDir), `${JSON.stringify(owner, null, 2)}
12005
12286
  `, { mode: 384 });
12006
12287
  } catch (err) {
12007
12288
  rmSync3(lockDir, { recursive: true, force: true });
@@ -12016,7 +12297,7 @@ function acquireDaemonMachineLock(options) {
12016
12297
  if (currentOwner?.pid === process.pid && currentOwner.token === token) {
12017
12298
  const released = { ...currentOwner, pid: 0 };
12018
12299
  try {
12019
- writeFileSync8(ownerPath(lockDir), `${JSON.stringify(released, null, 2)}
12300
+ writeFileSync5(ownerPath(lockDir), `${JSON.stringify(released, null, 2)}
12020
12301
  `, {
12021
12302
  mode: 384
12022
12303
  });
@@ -12046,8 +12327,8 @@ function acquireDaemonMachineLock(options) {
12046
12327
  }
12047
12328
 
12048
12329
  // src/localTraceSink.ts
12049
- import { appendFileSync, mkdirSync as mkdirSync7, readdirSync as readdirSync3, rmSync as rmSync4, statSync as statSync3, writeFileSync as writeFileSync9 } from "fs";
12050
- import path15 from "path";
12330
+ import { appendFileSync, mkdirSync as mkdirSync5, readdirSync as readdirSync3, rmSync as rmSync4, statSync as statSync3, writeFileSync as writeFileSync6 } from "fs";
12331
+ import path13 from "path";
12051
12332
  var DEFAULT_MAX_FILE_BYTES = 5 * 1024 * 1024;
12052
12333
  var DEFAULT_MAX_FILE_AGE_MS = 5 * 60 * 1e3;
12053
12334
  var DEFAULT_MAX_FILES = 8;
@@ -12084,7 +12365,7 @@ var LocalRotatingTraceSink = class {
12084
12365
  currentSize = 0;
12085
12366
  sequence = 0;
12086
12367
  constructor(options) {
12087
- this.traceDir = path15.join(options.machineDir, "traces");
12368
+ this.traceDir = path13.join(options.machineDir, "traces");
12088
12369
  this.maxFileBytes = Math.max(1024, Math.floor(options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES));
12089
12370
  const baseAgeMs = Math.max(1e3, Math.floor(options.maxFileAgeMs ?? DEFAULT_MAX_FILE_AGE_MS));
12090
12371
  const ageJitterMs = Math.max(0, Math.floor(options.maxFileAgeJitterMs ?? 0));
@@ -12110,15 +12391,15 @@ var LocalRotatingTraceSink = class {
12110
12391
  return this.currentFile;
12111
12392
  }
12112
12393
  ensureFile(nextBytes) {
12113
- mkdirSync7(this.traceDir, { recursive: true, mode: 448 });
12394
+ mkdirSync5(this.traceDir, { recursive: true, mode: 448 });
12114
12395
  const nowMs = this.nowMsProvider();
12115
12396
  const shouldRotateForAge = this.currentFileOpenedAtMs !== null && nowMs - this.currentFileOpenedAtMs >= this.maxFileAgeMs;
12116
12397
  if (!this.currentFile || this.currentSize + nextBytes > this.maxFileBytes || shouldRotateForAge) {
12117
- this.currentFile = path15.join(
12398
+ this.currentFile = path13.join(
12118
12399
  this.traceDir,
12119
12400
  `daemon-trace-${safeTimestamp(nowMs)}-${process.pid}-${String(this.sequence++).padStart(4, "0")}.jsonl`
12120
12401
  );
12121
- writeFileSync9(this.currentFile, "", { flag: "a", mode: 384 });
12402
+ writeFileSync6(this.currentFile, "", { flag: "a", mode: 384 });
12122
12403
  this.currentSize = statSync3(this.currentFile).size;
12123
12404
  this.currentFileOpenedAtMs = nowMs;
12124
12405
  this.pruneOldFiles();
@@ -12129,7 +12410,7 @@ var LocalRotatingTraceSink = class {
12129
12410
  const excess = files.length - this.maxFiles;
12130
12411
  if (excess <= 0) return;
12131
12412
  for (const file of files.slice(0, excess)) {
12132
- rmSync4(path15.join(this.traceDir, file), { force: true });
12413
+ rmSync4(path13.join(this.traceDir, file), { force: true });
12133
12414
  }
12134
12415
  }
12135
12416
  };
@@ -12220,11 +12501,108 @@ function isDiagnosticErrorAttr(key) {
12220
12501
  import { createHash as createHash6, randomUUID as randomUUID5 } from "crypto";
12221
12502
  import { gzipSync } from "zlib";
12222
12503
  import { mkdir as mkdir2, readFile as readFile2, readdir as readdir3, stat as stat3, writeFile as writeFile2 } from "fs/promises";
12223
- import path16 from "path";
12504
+ import path14 from "path";
12505
+
12506
+ // src/chatBridgeRequest.ts
12507
+ var DEFAULT_CHAT_BRIDGE_TOOL_TIMEOUT_MS = Number.parseInt(
12508
+ process.env.SLOCK_CHAT_BRIDGE_TOOL_TIMEOUT_MS || "",
12509
+ 10
12510
+ ) || 6e4;
12511
+ var ChatBridgeToolTimeoutError = class extends Error {
12512
+ toolName;
12513
+ target;
12514
+ timeoutMs;
12515
+ durationMs;
12516
+ constructor(toolName, target, timeoutMs, durationMs) {
12517
+ super(`${toolName} timed out after ${timeoutMs}ms${target ? ` (target: ${target})` : ""}`);
12518
+ this.name = "ChatBridgeToolTimeoutError";
12519
+ this.toolName = toolName;
12520
+ this.target = target;
12521
+ this.timeoutMs = timeoutMs;
12522
+ this.durationMs = durationMs;
12523
+ }
12524
+ };
12525
+ function describeError(err) {
12526
+ if (err instanceof Error) return `${err.name}: ${err.message}`;
12527
+ return String(err);
12528
+ }
12529
+ async function executeJsonRequest(url, init, {
12530
+ toolName,
12531
+ target = null,
12532
+ timeoutMs = DEFAULT_CHAT_BRIDGE_TOOL_TIMEOUT_MS,
12533
+ fetchImpl,
12534
+ now = () => Date.now(),
12535
+ warn = (message) => logger.warn(message)
12536
+ }) {
12537
+ const startedAt = now();
12538
+ const timeoutController = new AbortController();
12539
+ const signals = [timeoutController.signal];
12540
+ if (init.signal) signals.push(init.signal);
12541
+ const signal = signals.length === 1 ? signals[0] : AbortSignal.any(signals);
12542
+ const timeout = setTimeout(() => {
12543
+ timeoutController.abort();
12544
+ }, timeoutMs);
12545
+ timeout.unref?.();
12546
+ try {
12547
+ const response = await fetchImpl(url, { ...init, signal });
12548
+ const data = await response.json();
12549
+ return { response, data, durationMs: now() - startedAt };
12550
+ } catch (err) {
12551
+ const durationMs = now() - startedAt;
12552
+ if (timeoutController.signal.aborted && !init.signal?.aborted) {
12553
+ warn(
12554
+ `[ChatBridgeTimeout] tool=${toolName} target=${target ?? "-"} duration_ms=${durationMs} timeout_ms=${timeoutMs} outcome=timeout`
12555
+ );
12556
+ throw new ChatBridgeToolTimeoutError(toolName, target, timeoutMs, durationMs);
12557
+ }
12558
+ warn(
12559
+ `[ChatBridgeError] tool=${toolName} target=${target ?? "-"} duration_ms=${durationMs} outcome=error error=${describeError(err)}`
12560
+ );
12561
+ throw err;
12562
+ } finally {
12563
+ clearTimeout(timeout);
12564
+ }
12565
+ }
12566
+ async function executeResponseRequest(url, init, {
12567
+ toolName,
12568
+ target = null,
12569
+ timeoutMs = DEFAULT_CHAT_BRIDGE_TOOL_TIMEOUT_MS,
12570
+ fetchImpl,
12571
+ now = () => Date.now(),
12572
+ warn = (message) => logger.warn(message)
12573
+ }) {
12574
+ const startedAt = now();
12575
+ const timeoutController = new AbortController();
12576
+ const signals = [timeoutController.signal];
12577
+ if (init.signal) signals.push(init.signal);
12578
+ const signal = signals.length === 1 ? signals[0] : AbortSignal.any(signals);
12579
+ const timeout = setTimeout(() => {
12580
+ timeoutController.abort();
12581
+ }, timeoutMs);
12582
+ timeout.unref?.();
12583
+ try {
12584
+ const response = await fetchImpl(url, { ...init, signal });
12585
+ return { response, durationMs: now() - startedAt };
12586
+ } catch (err) {
12587
+ const durationMs = now() - startedAt;
12588
+ if (timeoutController.signal.aborted && !init.signal?.aborted) {
12589
+ warn(
12590
+ `[ChatBridgeTimeout] tool=${toolName} target=${target ?? "-"} duration_ms=${durationMs} timeout_ms=${timeoutMs} outcome=timeout`
12591
+ );
12592
+ throw new ChatBridgeToolTimeoutError(toolName, target, timeoutMs, durationMs);
12593
+ }
12594
+ warn(
12595
+ `[ChatBridgeError] tool=${toolName} target=${target ?? "-"} duration_ms=${durationMs} outcome=error error=${describeError(err)}`
12596
+ );
12597
+ throw err;
12598
+ } finally {
12599
+ clearTimeout(timeout);
12600
+ }
12601
+ }
12224
12602
 
12225
12603
  // src/directUploadCapability.ts
12226
- function joinUrl(base, path18) {
12227
- return `${base.replace(/\/+$/, "")}${path18}`;
12604
+ function joinUrl(base, path16) {
12605
+ return `${base.replace(/\/+$/, "")}${path16}`;
12228
12606
  }
12229
12607
  function jsonHeaders(apiKey) {
12230
12608
  return {
@@ -12443,7 +12821,7 @@ var DaemonTraceBundleUploader = class {
12443
12821
  }, nextMs);
12444
12822
  }
12445
12823
  async findUploadCandidates() {
12446
- const traceDir = path16.join(this.options.machineDir, "traces");
12824
+ const traceDir = path14.join(this.options.machineDir, "traces");
12447
12825
  let names;
12448
12826
  try {
12449
12827
  names = await readdir3(traceDir);
@@ -12455,8 +12833,8 @@ var DaemonTraceBundleUploader = class {
12455
12833
  const currentFile = this.options.currentFileProvider?.();
12456
12834
  const candidates = [];
12457
12835
  for (const name of names.filter((entry) => entry.startsWith("daemon-trace-") && entry.endsWith(".jsonl")).sort()) {
12458
- const file = path16.join(traceDir, name);
12459
- if (currentFile && path16.resolve(file) === path16.resolve(currentFile)) continue;
12836
+ const file = path14.join(traceDir, name);
12837
+ if (currentFile && path14.resolve(file) === path14.resolve(currentFile)) continue;
12460
12838
  if (await this.isUploaded(file)) continue;
12461
12839
  try {
12462
12840
  const info = await stat3(file);
@@ -12530,8 +12908,8 @@ var DaemonTraceBundleUploader = class {
12530
12908
  }
12531
12909
  }
12532
12910
  uploadStatePath(file) {
12533
- const stateDir = path16.join(this.options.machineDir, "trace-uploads");
12534
- return path16.join(stateDir, `${path16.basename(file)}.uploaded.json`);
12911
+ const stateDir = path14.join(this.options.machineDir, "trace-uploads");
12912
+ return path14.join(stateDir, `${path14.basename(file)}.uploaded.json`);
12535
12913
  }
12536
12914
  async isUploaded(file) {
12537
12915
  try {
@@ -12543,9 +12921,9 @@ var DaemonTraceBundleUploader = class {
12543
12921
  }
12544
12922
  async markUploaded(file, metadata) {
12545
12923
  const stateFile = this.uploadStatePath(file);
12546
- await mkdir2(path16.dirname(stateFile), { recursive: true, mode: 448 });
12924
+ await mkdir2(path14.dirname(stateFile), { recursive: true, mode: 448 });
12547
12925
  await writeFile2(stateFile, `${JSON.stringify({
12548
- file: path16.basename(file),
12926
+ file: path14.basename(file),
12549
12927
  uploadedAt: (/* @__PURE__ */ new Date()).toISOString(),
12550
12928
  ...metadata
12551
12929
  }, null, 2)}
@@ -12567,7 +12945,7 @@ var DEFAULT_TRACE_UPLOAD_URL = "https://slock-trace-upload.botiverse.dev";
12567
12945
  var RUNNER_CREDENTIAL_SCOPES = ["send", "read", "mentions", "tasks", "reactions", "server", "channels", "knowledge"];
12568
12946
  var RUNNER_CREDENTIAL_MINT_MAX_ATTEMPTS2 = 3;
12569
12947
  var RUNNER_CREDENTIAL_MINT_RETRY_DELAY_MS2 = 250;
12570
- var DAEMON_CLI_USAGE = "Usage: slock-daemon --server-url <url> --api-key <key>";
12948
+ var DAEMON_CLI_USAGE = `Usage: slock-daemon --server-url <url> (--api-key <key> or ${DAEMON_API_KEY_ENV}=<key>)`;
12571
12949
  var RunnerCredentialMintError2 = class extends Error {
12572
12950
  code;
12573
12951
  retryable;
@@ -12603,9 +12981,9 @@ function runnerCredentialErrorDetail2(error) {
12603
12981
  async function waitForRunnerCredentialRetry2() {
12604
12982
  await new Promise((resolve) => setTimeout(resolve, RUNNER_CREDENTIAL_MINT_RETRY_DELAY_MS2));
12605
12983
  }
12606
- function parseDaemonCliArgs(args) {
12984
+ function parseDaemonCliArgs(args, env = {}) {
12607
12985
  let serverUrl = "";
12608
- let apiKey = "";
12986
+ let apiKey = env[DAEMON_API_KEY_ENV] ?? "";
12609
12987
  for (let i = 0; i < args.length; i++) {
12610
12988
  if (args[i] === "--server-url" && args[i + 1]) serverUrl = args[++i];
12611
12989
  if (args[i] === "--api-key" && args[i + 1]) apiKey = args[++i];
@@ -12621,24 +12999,14 @@ function readDaemonVersion(moduleUrl = import.meta.url) {
12621
12999
  return "0.0.0-dev";
12622
13000
  }
12623
13001
  }
12624
- function resolveChatBridgePath(moduleUrl = import.meta.url) {
12625
- const dirname = path17.dirname(fileURLToPath(moduleUrl));
12626
- const jsPath = path17.resolve(dirname, "chat-bridge.js");
12627
- try {
12628
- accessSync(jsPath);
12629
- return jsPath;
12630
- } catch {
12631
- return path17.resolve(dirname, "chat-bridge.ts");
12632
- }
12633
- }
12634
13002
  function resolveSlockCliPath(moduleUrl = import.meta.url) {
12635
- const thisDir = path17.dirname(fileURLToPath(moduleUrl));
12636
- const bundledDistPath = path17.resolve(thisDir, "cli", "index.js");
13003
+ const thisDir = path15.dirname(fileURLToPath(moduleUrl));
13004
+ const bundledDistPath = path15.resolve(thisDir, "cli", "index.js");
12637
13005
  try {
12638
13006
  accessSync(bundledDistPath);
12639
13007
  return bundledDistPath;
12640
13008
  } catch {
12641
- const workspaceDistPath = path17.resolve(thisDir, "..", "..", "cli", "dist", "index.js");
13009
+ const workspaceDistPath = path15.resolve(thisDir, "..", "..", "cli", "dist", "index.js");
12642
13010
  accessSync(workspaceDistPath);
12643
13011
  return workspaceDistPath;
12644
13012
  }
@@ -12739,6 +13107,8 @@ function summarizeIncomingMessage(msg) {
12739
13107
  return `(agent=${msg.agentId})`;
12740
13108
  case "agent:deliver":
12741
13109
  return `(agent=${msg.agentId}, seq=${msg.seq}, from=@${msg.message.sender_name}, target=${formatChannelTarget(msg)})`;
13110
+ case "agent:inbox:purge":
13111
+ return `(agent=${msg.agentId}, channels=${msg.channelIds.length}, reason=${msg.reason || "server_purge"})`;
12742
13112
  case "agent:runtime_profile:migration":
12743
13113
  return `(agent=${msg.agentId}, migration=${msg.migrationKey})`;
12744
13114
  case "agent:runtime_profile:daemon_release_notice":
@@ -12773,7 +13143,6 @@ var DaemonCore = class {
12773
13143
  // bundle version). Reported in `ready` so the server can surface the
12774
13144
  // Computer's own version (distinct from the underlying daemonVersion).
12775
13145
  computerVersion;
12776
- chatBridgePath;
12777
13146
  slockCliPath;
12778
13147
  slockHome;
12779
13148
  runtimeDetector;
@@ -12789,7 +13158,6 @@ var DaemonCore = class {
12789
13158
  this.options = options;
12790
13159
  this.daemonVersion = options.daemonVersion ?? readDaemonVersion();
12791
13160
  this.computerVersion = (process.env.SLOCK_COMPUTER_VERSION || "").trim() || null;
12792
- this.chatBridgePath = options.chatBridgePath ?? resolveChatBridgePath();
12793
13161
  this.slockCliPath = options.slockCliPath ?? resolveSlockCliPath();
12794
13162
  this.slockHome = resolveSlockHome();
12795
13163
  process.env[SLOCK_HOME_ENV] = this.slockHome;
@@ -12810,7 +13178,7 @@ var DaemonCore = class {
12810
13178
  daemonVersion: this.daemonVersion,
12811
13179
  computerVersion: this.computerVersion
12812
13180
  };
12813
- this.agentManager = options.agentManagerFactory ? options.agentManagerFactory(this.chatBridgePath, (msg) => connection.send(msg), options.apiKey, agentManagerOptions) : new AgentProcessManager(this.chatBridgePath, (msg) => connection.send(msg), options.apiKey, agentManagerOptions);
13181
+ this.agentManager = options.agentManagerFactory ? options.agentManagerFactory((msg) => connection.send(msg), options.apiKey, agentManagerOptions) : new AgentProcessManager((msg) => connection.send(msg), options.apiKey, agentManagerOptions);
12814
13182
  const connectionFactory = options.connectionFactory ?? ((connOptions) => new DaemonConnection(connOptions));
12815
13183
  connection = connectionFactory({
12816
13184
  serverUrl: options.serverUrl,
@@ -12825,7 +13193,7 @@ var DaemonCore = class {
12825
13193
  }
12826
13194
  resolveMachineStateRoot() {
12827
13195
  if (this.options.machineStateDir) return this.options.machineStateDir;
12828
- if (this.options.dataDir) return path17.join(path17.dirname(this.options.dataDir), "machines");
13196
+ if (this.options.dataDir) return path15.join(path15.dirname(this.options.dataDir), "machines");
12829
13197
  return resolveDefaultMachineStateRoot();
12830
13198
  }
12831
13199
  shouldEnableLocalTrace() {
@@ -12851,7 +13219,7 @@ var DaemonCore = class {
12851
13219
  sink: this.localTraceSink
12852
13220
  }));
12853
13221
  this.agentManager.setTracer(this.tracer);
12854
- this.agentManager.setCliTransportTraceDir(path17.join(machineDir, "traces"));
13222
+ this.agentManager.setCliTransportTraceDir(path15.join(machineDir, "traces"));
12855
13223
  }
12856
13224
  installTraceBundleUploader(machineDir) {
12857
13225
  if (!this.shouldEnableLocalTrace()) return;
@@ -13093,6 +13461,10 @@ var DaemonCore = class {
13093
13461
  logger.info(`[Agent ${msg.agentId}] Workspace reset requested`);
13094
13462
  this.agentManager.resetWorkspace(msg.agentId);
13095
13463
  break;
13464
+ case "agent:inbox:purge":
13465
+ logger.info(`[Agent ${msg.agentId}] Inbox purge requested (${msg.channelIds.length} channels, reason=${msg.reason || "server_purge"})`);
13466
+ this.agentManager.purgeInboxMessagesForChannels(msg.agentId, msg.channelIds, msg.reason || "server_purge");
13467
+ break;
13096
13468
  case "agent:deliver": {
13097
13469
  const parent = parseTraceparent(msg.traceparent);
13098
13470
  const span = this.tracer.startSpan("daemon.agent.delivery", {
@@ -13344,6 +13716,7 @@ var DaemonCore = class {
13344
13716
  agentId: report.agentId,
13345
13717
  launchId: report.launchId || void 0,
13346
13718
  runtime: report.facts.runtime,
13719
+ report_source: "connect",
13347
13720
  model_present: Boolean(report.facts.model),
13348
13721
  session_ref_present: Boolean(report.facts.sessionRef),
13349
13722
  workspace_ref_present: Boolean(report.facts.workspaceRef || report.facts.workspacePathRef)
@@ -13354,7 +13727,8 @@ var DaemonCore = class {
13354
13727
  agentId: report.agentId,
13355
13728
  facts: report.facts,
13356
13729
  launchId: report.launchId || void 0,
13357
- traceparent: formatTraceparent(span.context)
13730
+ traceparent: formatTraceparent(span.context),
13731
+ source: "connect"
13358
13732
  });
13359
13733
  span.end("ok");
13360
13734
  }
@@ -13378,13 +13752,15 @@ var DaemonCore = class {
13378
13752
  };
13379
13753
 
13380
13754
  export {
13755
+ DAEMON_API_KEY_ENV,
13756
+ scrubDaemonAuthEnv,
13757
+ subscribeDaemonLogs,
13381
13758
  resolveWorkspaceDirectoryPath,
13382
13759
  scanWorkspaceDirectories,
13383
13760
  deleteWorkspaceDirectory,
13384
13761
  DAEMON_CLI_USAGE,
13385
13762
  parseDaemonCliArgs,
13386
13763
  readDaemonVersion,
13387
- resolveChatBridgePath,
13388
13764
  resolveSlockCliPath,
13389
13765
  detectRuntimes,
13390
13766
  DaemonCore