@slock-ai/daemon 0.56.1 → 0.57.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
 
@@ -1633,150 +1848,54 @@ Messages you receive have a single RFC 5424-style structured data header followe
1633
1848
  [target=dm:@richard msg=22222222 time=2026-03-15T01:00:02 type=human] @richard: hey, can you help?
1634
1849
  [target=#general:00000000 msg=33333333 time=2026-03-15T01:00:03 type=human] @richard: thread reply
1635
1850
  [target=dm:@richard:22222222 msg=44444444 time=2026-03-15T01:00:04 type=human] @richard: DM thread reply
1636
- \`\`\`
1637
-
1638
- Prompt examples use obvious placeholder IDs such as \`00000000\`, \`11111111\`,
1639
- and \`22222222\`. They show the shape of a real message ID but are not actual
1640
- messages. Do not cite them as evidence; use only IDs from messages you actually
1641
- received or read.
1642
-
1643
- Header fields:
1644
- - \`target=\` \u2014 where the message came from. Reuse as the \`target\` parameter when replying.
1645
- - \`msg=\` \u2014 message short ID (first 8 chars of UUID). Use as thread suffix to start/reply in a thread.
1646
- - \`time=\` \u2014 timestamp.
1647
- - \`type=\` \u2014 sender kind. Values are \`human\`, \`agent\`, or \`system\`.
1648
-
1649
- \`type=system\` messages announce state changes in the channel (task events, channel archived/unarchived, etc.). They are informational \u2014 don't reply to them unless they clearly request action (e.g. a task was just assigned to you). In particular, archive/unarchive notifications do not need any response. If a channel is archived, further writes there will be rejected.
1650
-
1651
- ${sendingMessagesSection}
1652
-
1653
- ${reminderSection}
1654
-
1655
- ${threadsSection}
1656
-
1657
- ${discoverySection}
1658
-
1659
- ${channelAwarenessSection}
1660
-
1661
- ${thirdPartyIntegrationsSection}
1662
-
1663
- ${readingHistorySection}
1664
-
1665
- ${historicalReferenceSection}
1666
-
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.
1851
+ \`\`\`
1713
1852
 
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.
1853
+ Prompt examples use obvious placeholder IDs such as \`00000000\`, \`11111111\`,
1854
+ and \`22222222\`. They show the shape of a real message ID but are not actual
1855
+ messages. Do not cite them as evidence; use only IDs from messages you actually
1856
+ received or read.
1715
1857
 
1716
- ### Formatting \u2014 URLs in non-English text
1858
+ Header fields:
1859
+ - \`target=\` \u2014 where the message came from. Reuse as the \`target\` parameter when replying.
1860
+ - \`msg=\` \u2014 message short ID (first 8 chars of UUID). Use as thread suffix to start/reply in a thread.
1861
+ - \`time=\` \u2014 timestamp.
1862
+ - \`type=\` \u2014 sender kind. Values are \`human\`, \`agent\`, or \`system\`.
1717
1863
 
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.
1864
+ \`type=system\` messages announce state changes in the channel (task events, channel archived/unarchived, etc.). They are informational \u2014 don't reply to them unless they clearly request action (e.g. a task was just assigned to you). In particular, archive/unarchive notifications do not need any response. If a channel is archived, further writes there will be rejected.
1719
1865
 
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\`
1866
+ ${sendingMessagesSection}
1723
1867
 
1724
- ## Workspace & Memory
1868
+ ${reminderSection}
1725
1869
 
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.
1870
+ ${threadsSection}
1727
1871
 
1728
- ### MEMORY.md \u2014 Your Memory Index (CRITICAL)
1872
+ ${discoverySection}
1729
1873
 
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.
1874
+ ${channelAwarenessSection}
1731
1875
 
1732
- \`\`\`markdown
1733
- # <Your Name>
1876
+ ${thirdPartyIntegrationsSection}
1734
1877
 
1735
- ## Role
1736
- <your role definition, evolved over time>
1878
+ ${readingHistorySection}
1737
1879
 
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
- - ...
1880
+ ${historicalReferenceSection}
1743
1881
 
1744
- ## Active Context
1745
- - Currently working on: <brief summary>
1746
- - Last interaction: <brief summary>
1747
- \`\`\`
1882
+ ${tasksSection}
1748
1883
 
1749
- ### What to memorize
1884
+ ${cliGuideSections.splittingTasks}
1750
1885
 
1751
- **Actively observe and record** the following kinds of knowledge as you encounter them in conversations:
1886
+ ${cliGuideSections.mentions}
1752
1887
 
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.
1888
+ ${cliGuideSections.communicationStyle}
1759
1889
 
1760
- ### How to organize memory
1890
+ ${cliGuideSections.conversationEtiquette}
1761
1891
 
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.
1892
+ ${cliGuideSections.formattingMentionsChannels}
1771
1893
 
1772
- ### Compaction safety (CRITICAL)
1894
+ ${cliGuideSections.formattingUrls}
1773
1895
 
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:
1896
+ ${cliGuideSections.workspaceAndMemory}
1775
1897
 
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.
1898
+ ${cliGuideSections.compactionSafety}
1780
1899
 
1781
1900
  ## Capabilities
1782
1901
 
@@ -2190,6 +2309,160 @@ function stripUndefined(value) {
2190
2309
  return value;
2191
2310
  }
2192
2311
 
2312
+ // src/proxy.ts
2313
+ import { HttpsProxyAgent } from "https-proxy-agent";
2314
+ import { ProxyAgent } from "undici";
2315
+ var fetchDispatcherCache = /* @__PURE__ */ new Map();
2316
+ function getFetchPreResponseTimeoutMs(env) {
2317
+ const parsed = Number.parseInt(env.SLOCK_DAEMON_FETCH_PRE_RESPONSE_TIMEOUT_MS || "", 10);
2318
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 3e4;
2319
+ }
2320
+ function getDefaultPort(protocol) {
2321
+ switch (protocol) {
2322
+ case "https:":
2323
+ case "wss:":
2324
+ return "443";
2325
+ case "http:":
2326
+ case "ws:":
2327
+ return "80";
2328
+ default:
2329
+ return "";
2330
+ }
2331
+ }
2332
+ function hostMatchesNoProxyEntry(hostname, ruleHost) {
2333
+ if (!ruleHost) return false;
2334
+ const normalizedRule = ruleHost.replace(/^\*\./, ".").replace(/^\./, "").toLowerCase();
2335
+ const normalizedHost = hostname.toLowerCase();
2336
+ return normalizedHost === normalizedRule || normalizedHost.endsWith(`.${normalizedRule}`);
2337
+ }
2338
+ function getProxyUrlForTarget(targetUrl, env) {
2339
+ const protocol = new URL(targetUrl).protocol;
2340
+ switch (protocol) {
2341
+ case "wss:":
2342
+ return env.WSS_PROXY || env.wss_proxy || env.HTTPS_PROXY || env.https_proxy || env.ALL_PROXY || env.all_proxy;
2343
+ case "ws:":
2344
+ return env.WS_PROXY || env.ws_proxy || env.HTTP_PROXY || env.http_proxy || env.ALL_PROXY || env.all_proxy;
2345
+ case "https:":
2346
+ return env.HTTPS_PROXY || env.https_proxy || env.ALL_PROXY || env.all_proxy;
2347
+ case "http:":
2348
+ return env.HTTP_PROXY || env.http_proxy || env.ALL_PROXY || env.all_proxy;
2349
+ default:
2350
+ return env.ALL_PROXY || env.all_proxy;
2351
+ }
2352
+ }
2353
+ function shouldBypassProxy(targetUrl, env) {
2354
+ const rawNoProxy = env.NO_PROXY || env.no_proxy;
2355
+ if (!rawNoProxy) return false;
2356
+ const url = new URL(targetUrl);
2357
+ const hostname = url.hostname.toLowerCase();
2358
+ const port = url.port || getDefaultPort(url.protocol);
2359
+ return rawNoProxy.split(",").map((entry) => entry.trim()).filter(Boolean).some((entry) => {
2360
+ if (entry === "*") return true;
2361
+ const [ruleHost, rulePort] = entry.split(":", 2);
2362
+ if (rulePort && rulePort !== port) return false;
2363
+ return hostMatchesNoProxyEntry(hostname, ruleHost);
2364
+ });
2365
+ }
2366
+ function buildWebSocketOptions(wsUrl, env) {
2367
+ const proxyUrl = getProxyUrlForTarget(wsUrl, env);
2368
+ if (!proxyUrl) return void 0;
2369
+ if (shouldBypassProxy(wsUrl, env)) return void 0;
2370
+ return {
2371
+ agent: new HttpsProxyAgent(proxyUrl)
2372
+ };
2373
+ }
2374
+ function resolveProxyUrl(targetUrl, env) {
2375
+ const proxyUrl = getProxyUrlForTarget(targetUrl, env);
2376
+ if (!proxyUrl) return void 0;
2377
+ if (shouldBypassProxy(targetUrl, env)) return void 0;
2378
+ return proxyUrl;
2379
+ }
2380
+ function buildFetchDispatcher(targetUrl, env) {
2381
+ const proxyUrl = resolveProxyUrl(targetUrl, env);
2382
+ if (!proxyUrl) return void 0;
2383
+ const cached = fetchDispatcherCache.get(proxyUrl);
2384
+ if (cached) return cached;
2385
+ const timeoutMs = getFetchPreResponseTimeoutMs(env);
2386
+ const dispatcher = new ProxyAgent({
2387
+ uri: proxyUrl,
2388
+ // All three are pre-response and body-agnostic (see getFetchPreResponseTimeoutMs):
2389
+ // headersTimeout = headers-hang leg; requestTls.timeout = CONNECT-tunnel
2390
+ // establish leg; connect.timeout = socket to the proxy itself (defensive).
2391
+ connect: { timeout: timeoutMs },
2392
+ requestTls: { timeout: timeoutMs },
2393
+ headersTimeout: timeoutMs
2394
+ });
2395
+ fetchDispatcherCache.set(proxyUrl, dispatcher);
2396
+ return dispatcher;
2397
+ }
2398
+ function evictFetchDispatcher(targetUrl, env) {
2399
+ const proxyUrl = resolveProxyUrl(targetUrl, env);
2400
+ if (!proxyUrl) return false;
2401
+ const cached = fetchDispatcherCache.get(proxyUrl);
2402
+ if (!cached) return false;
2403
+ fetchDispatcherCache.delete(proxyUrl);
2404
+ void Promise.resolve().then(() => cached.close()).catch(() => cached.destroy?.(new Error("evicted"))).catch(() => {
2405
+ });
2406
+ return true;
2407
+ }
2408
+
2409
+ // src/daemonFetch.ts
2410
+ function withDaemonFetchProxy(input, init = {}, env = process.env) {
2411
+ const dispatcher = buildFetchDispatcher(input.toString(), env);
2412
+ return dispatcher ? { ...init, dispatcher } : init;
2413
+ }
2414
+ async function daemonFetch(input, init, env = process.env) {
2415
+ try {
2416
+ return await fetch(input, withDaemonFetchProxy(input, init, env));
2417
+ } catch (err) {
2418
+ evictFetchDispatcher(input.toString(), env);
2419
+ throw err;
2420
+ }
2421
+ }
2422
+
2423
+ // src/logger.ts
2424
+ var listeners = /* @__PURE__ */ new Set();
2425
+ function timestamp() {
2426
+ const d = /* @__PURE__ */ new Date();
2427
+ const pad = (n) => String(n).padStart(2, "0");
2428
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
2429
+ }
2430
+ function format(level, msg) {
2431
+ return `${timestamp()} [${level}] ${msg}`;
2432
+ }
2433
+ function emit(event) {
2434
+ for (const listener of listeners) {
2435
+ listener(event);
2436
+ }
2437
+ }
2438
+ function subscribeDaemonLogs(listener) {
2439
+ listeners.add(listener);
2440
+ return () => {
2441
+ listeners.delete(listener);
2442
+ };
2443
+ }
2444
+ var logger = {
2445
+ info(msg) {
2446
+ const line = format("INFO", msg);
2447
+ console.log(line);
2448
+ emit({ level: "INFO", line, message: msg });
2449
+ },
2450
+ warn(msg) {
2451
+ const line = format("WARN", msg);
2452
+ console.warn(line);
2453
+ emit({ level: "WARN", line, message: msg });
2454
+ },
2455
+ error(msg, err) {
2456
+ const line = format("ERROR", msg);
2457
+ if (err) {
2458
+ console.error(line, err);
2459
+ } else {
2460
+ console.error(line);
2461
+ }
2462
+ emit({ level: "ERROR", line, message: msg, error: err });
2463
+ }
2464
+ };
2465
+
2193
2466
  // src/agentCredentialProxy.ts
2194
2467
  var registrations = /* @__PURE__ */ new Map();
2195
2468
  var proxyServerState = null;
@@ -4383,7 +4656,7 @@ var CodexDriver = class {
4383
4656
  };
4384
4657
  communication = {
4385
4658
  chat: "slock_cli",
4386
- runtimeControl: "mcp_runtime_actions"
4659
+ runtimeControl: "none"
4387
4660
  };
4388
4661
  session = {
4389
4662
  recovery: "resume_or_fresh"
@@ -4400,50 +4673,6 @@ var CodexDriver = class {
4400
4673
  probe() {
4401
4674
  return probeCodex();
4402
4675
  }
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
4676
  buildThreadRequest(ctx) {
4448
4677
  const threadParams = {
4449
4678
  cwd: ctx.workingDirectory,
@@ -4497,7 +4726,6 @@ var CodexDriver = class {
4497
4726
  this.initialTurnStarted = false;
4498
4727
  this.normalizer.reset({ threadId: ctx.config.sessionId || null });
4499
4728
  const args = ["app-server", "--listen", "stdio://"];
4500
- args.push(...this.buildRuntimeActionsConfigArgs(ctx));
4501
4729
  const { command, args: spawnArgs } = resolveCodexSpawn(args);
4502
4730
  const proc = spawn2(command, spawnArgs, {
4503
4731
  cwd: ctx.workingDirectory,
@@ -4784,8 +5012,6 @@ var AntigravityDriver = class {
4784
5012
 
4785
5013
  // src/drivers/copilot.ts
4786
5014
  import { spawn as spawn4 } from "child_process";
4787
- import path6 from "path";
4788
- import { writeFileSync as writeFileSync3 } from "fs";
4789
5015
  async function buildCopilotSpawnEnv(ctx) {
4790
5016
  return (await prepareCliTransport(ctx, { NO_COLOR: "1" })).spawnEnv;
4791
5017
  }
@@ -4799,7 +5025,7 @@ var CopilotDriver = class {
4799
5025
  };
4800
5026
  communication = {
4801
5027
  chat: "slock_cli",
4802
- runtimeControl: "mcp_runtime_actions"
5028
+ runtimeControl: "none"
4803
5029
  };
4804
5030
  session = {
4805
5031
  recovery: "resume_or_fresh"
@@ -4814,43 +5040,14 @@ var CopilotDriver = class {
4814
5040
  usesSlockCliForCommunication = true;
4815
5041
  sessionId = null;
4816
5042
  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
5043
  async spawn(ctx) {
4843
5044
  this.sessionId = ctx.config.sessionId || null;
4844
5045
  this.sessionAnnounced = false;
4845
- const mcpConfigPath = path6.join(ctx.workingDirectory, ".slock-copilot-mcp.json");
4846
- writeFileSync3(mcpConfigPath, this.buildRuntimeActionsMcpConfig(ctx), "utf8");
4847
5046
  const args = [
4848
5047
  "--output-format",
4849
5048
  "json",
4850
5049
  "--allow-all-tools",
4851
5050
  "--allow-all-paths",
4852
- "--additional-mcp-config",
4853
- `@${mcpConfigPath}`,
4854
5051
  "-p",
4855
5052
  ctx.prompt
4856
5053
  ];
@@ -4959,8 +5156,6 @@ var CopilotDriver = class {
4959
5156
 
4960
5157
  // src/drivers/cursor.ts
4961
5158
  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
5159
  async function buildCursorSpawnEnv(ctx, deps = {}) {
4965
5160
  const { spawnEnv } = await prepareCliTransport(ctx, { NO_COLOR: "1" });
4966
5161
  return withWindowsUserEnvironment(spawnEnv, deps);
@@ -4975,7 +5170,7 @@ var CursorDriver = class {
4975
5170
  };
4976
5171
  communication = {
4977
5172
  chat: "slock_cli",
4978
- runtimeControl: "mcp_runtime_actions"
5173
+ runtimeControl: "none"
4979
5174
  };
4980
5175
  session = {
4981
5176
  recovery: "resume_or_fresh"
@@ -4988,38 +5183,7 @@ var CursorDriver = class {
4988
5183
  mcpToolPrefix = "mcp__chat__";
4989
5184
  busyDeliveryMode = "none";
4990
5185
  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
5186
  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
5187
  const args = [
5024
5188
  "--print",
5025
5189
  "--output-format",
@@ -5165,8 +5329,8 @@ function runCursorModelsCommand() {
5165
5329
 
5166
5330
  // src/drivers/gemini.ts
5167
5331
  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";
5332
+ import { existsSync as existsSync4 } from "fs";
5333
+ import path6 from "path";
5170
5334
  async function buildGeminiSpawnEnv(ctx, platform = process.platform) {
5171
5335
  const { spawnEnv } = await prepareCliTransport(ctx, { NO_COLOR: "1" }, platform);
5172
5336
  const launchEnvVars = runtimeConfigToLaunchFields(ctx.config).envVars;
@@ -5208,9 +5372,9 @@ function resolveGeminiSpawn(commandArgs, deps = {}) {
5208
5372
  return { command: resolveCommandOnPath("gemini", deps) ?? "gemini", args: commandArgs };
5209
5373
  }
5210
5374
  const execFileSyncFn = deps.execFileSyncFn ?? execFileSync3;
5211
- const existsSyncFn = deps.existsSyncFn ?? existsSync5;
5375
+ const existsSyncFn = deps.existsSyncFn ?? existsSync4;
5212
5376
  const env = deps.env ?? process.env;
5213
- const winPath = path8.win32;
5377
+ const winPath = path6.win32;
5214
5378
  let geminiEntry = null;
5215
5379
  try {
5216
5380
  const globalRoot = normalizeExecOutput2(execFileSyncFn("npm", ["root", "-g"], {
@@ -5254,7 +5418,7 @@ var GeminiDriver = class {
5254
5418
  };
5255
5419
  communication = {
5256
5420
  chat: "slock_cli",
5257
- runtimeControl: "mcp_runtime_actions"
5421
+ runtimeControl: "none"
5258
5422
  };
5259
5423
  session = {
5260
5424
  recovery: "resume_or_fresh"
@@ -5272,7 +5436,6 @@ var GeminiDriver = class {
5272
5436
  async spawn(ctx) {
5273
5437
  this.sessionId = ctx.config.sessionId || null;
5274
5438
  this.sessionAnnounced = false;
5275
- this.writeGeminiSettings(ctx);
5276
5439
  const { command, args } = resolveGeminiSpawn(buildGeminiArgs(ctx.config));
5277
5440
  const spawnEnv = await buildGeminiSpawnEnv(ctx);
5278
5441
  const proc = spawn6(command, args, {
@@ -5345,49 +5508,17 @@ var GeminiDriver = class {
5345
5508
  messageNotificationStyle: "poll"
5346
5509
  });
5347
5510
  }
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
5511
  };
5380
5512
 
5381
5513
  // src/drivers/kimi.ts
5382
5514
  import { randomUUID as randomUUID2 } from "crypto";
5383
5515
  import { spawn as spawn7 } from "child_process";
5384
- import { existsSync as existsSync6, readFileSync as readFileSync3, writeFileSync as writeFileSync6 } from "fs";
5516
+ import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
5385
5517
  import os3 from "os";
5386
- import path9 from "path";
5518
+ import path7 from "path";
5387
5519
  var KIMI_WIRE_PROTOCOL_VERSION = "1.3";
5388
5520
  var KIMI_SYSTEM_PROMPT_FILE = ".slock-kimi-system.md";
5389
5521
  var KIMI_AGENT_FILE = ".slock-kimi-agent.yaml";
5390
- var KIMI_MCP_FILE = ".slock-kimi-mcp.json";
5391
5522
  function parseToolArguments(raw) {
5392
5523
  if (typeof raw !== "string") return raw;
5393
5524
  try {
@@ -5412,7 +5543,7 @@ var KimiDriver = class {
5412
5543
  };
5413
5544
  communication = {
5414
5545
  chat: "slock_cli",
5415
- runtimeControl: "mcp_runtime_actions"
5546
+ runtimeControl: "none"
5416
5547
  };
5417
5548
  session = {
5418
5549
  recovery: "resume_or_fresh"
@@ -5428,58 +5559,28 @@ var KimiDriver = class {
5428
5559
  sessionId = null;
5429
5560
  sessionAnnounced = false;
5430
5561
  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
5562
  async spawn(ctx) {
5448
5563
  const isResume = !!ctx.config.sessionId;
5449
5564
  this.sessionId = ctx.config.sessionId || randomUUID2();
5450
5565
  this.sessionAnnounced = false;
5451
5566
  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, [
5567
+ const systemPromptPath = path7.join(ctx.workingDirectory, KIMI_SYSTEM_PROMPT_FILE);
5568
+ const agentFilePath = path7.join(ctx.workingDirectory, KIMI_AGENT_FILE);
5569
+ if (!isResume || !existsSync5(systemPromptPath)) {
5570
+ writeFileSync3(systemPromptPath, ctx.prompt, "utf8");
5571
+ }
5572
+ writeFileSync3(agentFilePath, [
5462
5573
  "version: 1",
5463
5574
  "agent:",
5464
5575
  " extend: default",
5465
5576
  ` system_prompt_path: ./${KIMI_SYSTEM_PROMPT_FILE}`,
5466
5577
  ""
5467
5578
  ].join("\n"), "utf8");
5468
- writeFileSync6(mcpConfigPath, JSON.stringify({
5469
- mcpServers: {
5470
- chat: {
5471
- command,
5472
- args: bridgeArgs
5473
- }
5474
- }
5475
- }), "utf8");
5476
5579
  const args = [
5477
5580
  "--wire",
5478
5581
  "--yolo",
5479
5582
  "--agent-file",
5480
5583
  agentFilePath,
5481
- "--mcp-config-file",
5482
- mcpConfigPath,
5483
5584
  "--session",
5484
5585
  this.sessionId
5485
5586
  ];
@@ -5612,7 +5713,7 @@ var KimiDriver = class {
5612
5713
  }
5613
5714
  };
5614
5715
  function detectKimiModels(home = os3.homedir()) {
5615
- const configPath = path9.join(home, ".kimi", "config.toml");
5716
+ const configPath = path7.join(home, ".kimi", "config.toml");
5616
5717
  let raw;
5617
5718
  try {
5618
5719
  raw = readFileSync3(configPath, "utf8");
@@ -5639,9 +5740,9 @@ function detectKimiModels(home = os3.homedir()) {
5639
5740
 
5640
5741
  // src/drivers/opencode.ts
5641
5742
  import { spawn as spawn8, spawnSync as spawnSync2 } from "child_process";
5642
- import { existsSync as existsSync7, readFileSync as readFileSync4 } from "fs";
5743
+ import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
5643
5744
  import os4 from "os";
5644
- import path10 from "path";
5745
+ import path8 from "path";
5645
5746
  var CHAT_MCP_SERVER_NAME = "chat";
5646
5747
  var CHAT_MCP_TOOL_PREFIX = `${CHAT_MCP_SERVER_NAME}_`;
5647
5748
  var SLOCK_AGENT_NAME = "slock";
@@ -5656,23 +5757,6 @@ var OPENCODE_PROVIDER_LABELS = {
5656
5757
  deepseek: "DeepSeek",
5657
5758
  fusecode: "FuseCode"
5658
5759
  };
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
5760
  function parseOpenCodeConfigContent(raw) {
5677
5761
  if (!raw) return {};
5678
5762
  try {
@@ -5689,7 +5773,7 @@ function parseUserOpenCodeConfig(ctx) {
5689
5773
  return parseOpenCodeConfigContent(raw);
5690
5774
  }
5691
5775
  function readLocalOpenCodeConfig(home = os4.homedir()) {
5692
- const configPath = path10.join(home, ".config", "opencode", "opencode.json");
5776
+ const configPath = path8.join(home, ".config", "opencode", "opencode.json");
5693
5777
  try {
5694
5778
  return parseOpenCodeConfigContent(readFileSync4(configPath, "utf8"));
5695
5779
  } catch {
@@ -5764,14 +5848,7 @@ function buildOpenCodeConfig(ctx, home = os4.homedir()) {
5764
5848
  prompt: ctx.standingPrompt
5765
5849
  }
5766
5850
  },
5767
- mcp: {
5768
- ...recordField(userConfig.mcp),
5769
- [CHAT_MCP_SERVER_NAME]: {
5770
- type: "local",
5771
- command: buildChatBridgeCommand(ctx),
5772
- enabled: true
5773
- }
5774
- }
5851
+ mcp: recordField(userConfig.mcp)
5775
5852
  };
5776
5853
  }
5777
5854
  async function buildOpenCodeLaunchOptions(ctx, home = os4.homedir(), version = null) {
@@ -5894,11 +5971,11 @@ function runOpenCodeModelsCommand(home, deps = {}) {
5894
5971
  };
5895
5972
  }
5896
5973
  function isWindowsCommandShim(commandPath) {
5897
- const ext = path10.win32.extname(commandPath).toLowerCase();
5974
+ const ext = path8.win32.extname(commandPath).toLowerCase();
5898
5975
  return ext === ".cmd" || ext === ".bat";
5899
5976
  }
5900
5977
  function opencodePackageEntryCandidates(packageRoot) {
5901
- const winPath = path10.win32;
5978
+ const winPath = path8.win32;
5902
5979
  return [
5903
5980
  winPath.join(packageRoot, "bin", "opencode.exe"),
5904
5981
  winPath.join(packageRoot, "bin", "opencode.js"),
@@ -5907,16 +5984,16 @@ function opencodePackageEntryCandidates(packageRoot) {
5907
5984
  ];
5908
5985
  }
5909
5986
  function openCodeSpecForEntry(entry, commandArgs) {
5910
- if (path10.win32.extname(entry).toLowerCase() === ".exe") {
5987
+ if (path8.win32.extname(entry).toLowerCase() === ".exe") {
5911
5988
  return { command: entry, args: commandArgs, shell: false };
5912
5989
  }
5913
5990
  return { command: process.execPath, args: [entry, ...commandArgs], shell: false };
5914
5991
  }
5915
5992
  function resolveWindowsOpenCodePackageEntry(commandPath, deps = {}) {
5916
- const existsSyncFn = deps.existsSyncFn ?? existsSync7;
5993
+ const existsSyncFn = deps.existsSyncFn ?? existsSync6;
5917
5994
  const execFileSyncFn = deps.execFileSyncFn;
5918
5995
  const env = deps.env ?? process.env;
5919
- const winPath = path10.win32;
5996
+ const winPath = path8.win32;
5920
5997
  const candidates = [];
5921
5998
  if (execFileSyncFn) {
5922
5999
  try {
@@ -5944,7 +6021,7 @@ function resolveWindowsOpenCodePackageEntry(commandPath, deps = {}) {
5944
6021
  function extractWindowsShimTargets(commandPath, deps = {}) {
5945
6022
  if (!isWindowsCommandShim(commandPath)) return [];
5946
6023
  const readFileSyncFn = deps.readFileSyncFn ?? readFileSync4;
5947
- const commandDir = path10.win32.dirname(commandPath);
6024
+ const commandDir = path8.win32.dirname(commandPath);
5948
6025
  let raw;
5949
6026
  try {
5950
6027
  raw = String(readFileSyncFn(commandPath, "utf8"));
@@ -5955,7 +6032,7 @@ function extractWindowsShimTargets(commandPath, deps = {}) {
5955
6032
  const dp0Pattern = /%~dp0\\?([^"\r\n]*?opencode\.(?:exe|js|mjs|cjs))/gi;
5956
6033
  for (const match of raw.matchAll(dp0Pattern)) {
5957
6034
  const relative = match[1]?.replace(/^\\+/, "");
5958
- if (relative) candidates.push(path10.win32.normalize(path10.win32.join(commandDir, relative)));
6035
+ if (relative) candidates.push(path8.win32.normalize(path8.win32.join(commandDir, relative)));
5959
6036
  }
5960
6037
  return candidates;
5961
6038
  }
@@ -5969,7 +6046,7 @@ function resolveOpenCodeSpawn(commandArgs, deps = {}) {
5969
6046
  };
5970
6047
  }
5971
6048
  const command = resolveCommandOnPath("opencode", deps);
5972
- if (command && path10.win32.extname(command).toLowerCase() === ".exe") {
6049
+ if (command && path8.win32.extname(command).toLowerCase() === ".exe") {
5973
6050
  return { command, args: commandArgs, shell: false };
5974
6051
  }
5975
6052
  const packageEntry = resolveWindowsOpenCodePackageEntry(command, deps);
@@ -6013,7 +6090,7 @@ var OpenCodeDriver = class {
6013
6090
  };
6014
6091
  communication = {
6015
6092
  chat: "slock_cli",
6016
- runtimeControl: "mcp_runtime_actions"
6093
+ runtimeControl: "none"
6017
6094
  };
6018
6095
  session = {
6019
6096
  recovery: "resume_or_fresh"
@@ -6144,8 +6221,8 @@ var OpenCodeDriver = class {
6144
6221
  // src/drivers/pi.ts
6145
6222
  import { randomUUID as randomUUID3 } from "crypto";
6146
6223
  import { EventEmitter } from "events";
6147
- import { mkdirSync as mkdirSync4, readdirSync } from "fs";
6148
- import path11 from "path";
6224
+ import { mkdirSync as mkdirSync2, readdirSync } from "fs";
6225
+ import path9 from "path";
6149
6226
  import {
6150
6227
  AuthStorage,
6151
6228
  createBashTool,
@@ -6171,7 +6248,7 @@ function createPiSdkEventMappingState(sessionId = null) {
6171
6248
  };
6172
6249
  }
6173
6250
  function buildPiSessionDir(workingDirectory) {
6174
- return path11.join(workingDirectory, PI_SESSION_DIR);
6251
+ return path9.join(workingDirectory, PI_SESSION_DIR);
6175
6252
  }
6176
6253
  async function buildPiSpawnEnv(ctx) {
6177
6254
  return (await prepareCliTransport(ctx, { NO_COLOR: "1" })).spawnEnv;
@@ -6193,7 +6270,7 @@ function findPiSessionFile(sessionDir, sessionId) {
6193
6270
  }
6194
6271
  const suffix = `_${sessionId}.jsonl`;
6195
6272
  const match = entries.find((entry) => entry.endsWith(suffix));
6196
- return match ? path11.join(sessionDir, match) : null;
6273
+ return match ? path9.join(sessionDir, match) : null;
6197
6274
  }
6198
6275
  function detectPiModelsFromRegistry(modelRegistry) {
6199
6276
  const models = [];
@@ -6342,13 +6419,15 @@ var PI_RUNTIME_SESSION_DESCRIPTOR = {
6342
6419
  busyDelivery: "direct",
6343
6420
  postTurn: "keep_alive"
6344
6421
  };
6422
+ var PI_IDLE_PROMPT_RETRY_MS = 25;
6423
+ var PI_IDLE_PROMPT_MAX_WAIT_MS = 1e3;
6345
6424
  async function createPiAgentSessionForContext(ctx, sessionId) {
6346
6425
  const sessionDir = buildPiSessionDir(ctx.workingDirectory);
6347
- mkdirSync4(sessionDir, { recursive: true });
6426
+ mkdirSync2(sessionDir, { recursive: true });
6348
6427
  const spawnEnv = await buildPiSpawnEnv(ctx);
6349
6428
  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"));
6429
+ const authStorage = AuthStorage.create(path9.join(agentDir, "auth.json"));
6430
+ const modelRegistry = ModelRegistry.create(authStorage, path9.join(agentDir, "models.json"));
6352
6431
  const launchRuntimeFields = runtimeConfigToLaunchFields(ctx.config);
6353
6432
  const model = resolvePiModelFromRegistry(launchRuntimeFields.model, modelRegistry);
6354
6433
  if (launchRuntimeFields.model && launchRuntimeFields.model !== "default" && !model) {
@@ -6459,7 +6538,8 @@ var PiSdkRuntimeSession = class {
6459
6538
  return { ok: true, acceptedAs: "steer" };
6460
6539
  }
6461
6540
  if (session.isStreaming) {
6462
- return { ok: false, reason: "busy_rejected", error: "Pi session is still streaming" };
6541
+ this.launchPromptAfterStreaming(input.text);
6542
+ return { ok: true, acceptedAs: "prompt" };
6463
6543
  }
6464
6544
  this.launchPrompt(input.text);
6465
6545
  return { ok: true, acceptedAs: "prompt" };
@@ -6501,6 +6581,27 @@ var PiSdkRuntimeSession = class {
6501
6581
  }
6502
6582
  this.deferSdkCall(() => session.prompt(text));
6503
6583
  }
6584
+ launchPromptAfterStreaming(text) {
6585
+ this.deferSdkCall(async () => {
6586
+ const session = this.session;
6587
+ if (!session) return;
6588
+ const ready = await this.waitForStreamingToClear(session);
6589
+ if (this.didClose || this.session !== session) {
6590
+ return;
6591
+ }
6592
+ await (ready ? session.prompt(text) : session.steer(text));
6593
+ });
6594
+ }
6595
+ async waitForStreamingToClear(session) {
6596
+ const deadline = Date.now() + PI_IDLE_PROMPT_MAX_WAIT_MS;
6597
+ while (!this.didClose && this.session === session && session.isStreaming) {
6598
+ if (Date.now() >= deadline) {
6599
+ return false;
6600
+ }
6601
+ await delay(PI_IDLE_PROMPT_RETRY_MS);
6602
+ }
6603
+ return !this.didClose && this.session === session && !session.isStreaming;
6604
+ }
6504
6605
  deferSdkCall(invoke) {
6505
6606
  setImmediate(() => {
6506
6607
  if (this.didClose) return;
@@ -6612,6 +6713,12 @@ var PiDriver = class {
6612
6713
  });
6613
6714
  }
6614
6715
  };
6716
+ function delay(ms) {
6717
+ return new Promise((resolve) => {
6718
+ const timer = setTimeout(resolve, ms);
6719
+ timer.unref?.();
6720
+ });
6721
+ }
6615
6722
 
6616
6723
  // src/drivers/runtimeSession.ts
6617
6724
  import { EventEmitter as EventEmitter2 } from "events";
@@ -6770,7 +6877,7 @@ function getDriver(runtimeId) {
6770
6877
 
6771
6878
  // src/workspaces.ts
6772
6879
  import { readdir, rm, stat } from "fs/promises";
6773
- import path12 from "path";
6880
+ import path10 from "path";
6774
6881
  function isValidWorkspaceDirectoryName(directoryName) {
6775
6882
  return !directoryName.includes("/") && !directoryName.includes("\\") && !directoryName.includes("..");
6776
6883
  }
@@ -6778,7 +6885,7 @@ function resolveWorkspaceDirectoryPath(dataDir, directoryName) {
6778
6885
  if (!isValidWorkspaceDirectoryName(directoryName)) {
6779
6886
  return null;
6780
6887
  }
6781
- return path12.join(dataDir, directoryName);
6888
+ return path10.join(dataDir, directoryName);
6782
6889
  }
6783
6890
  function emptyWorkspaceDirectorySummary(latestMtime = /* @__PURE__ */ new Date(0)) {
6784
6891
  return {
@@ -6827,7 +6934,7 @@ async function summarizeWorkspaceDirectory(dirPath) {
6827
6934
  return summary;
6828
6935
  }
6829
6936
  const childSummaries = await Promise.all(
6830
- entries.map((entry) => summarizeWorkspaceEntry(path12.join(dirPath, entry.name), entry))
6937
+ entries.map((entry) => summarizeWorkspaceEntry(path10.join(dirPath, entry.name), entry))
6831
6938
  );
6832
6939
  for (const childSummary of childSummaries) {
6833
6940
  summary = mergeWorkspaceDirectorySummaries(summary, childSummary);
@@ -6846,7 +6953,7 @@ async function scanWorkspaceDirectories(dataDir) {
6846
6953
  if (!entry.isDirectory()) {
6847
6954
  return null;
6848
6955
  }
6849
- const dirPath = path12.join(dataDir, entry.name);
6956
+ const dirPath = path10.join(dataDir, entry.name);
6850
6957
  try {
6851
6958
  const summary = await summarizeWorkspaceDirectory(dirPath);
6852
6959
  return {
@@ -7116,6 +7223,11 @@ var RuntimeNotificationState = class {
7116
7223
  clearPending() {
7117
7224
  this.pendingCountValue = 0;
7118
7225
  }
7226
+ remove(count) {
7227
+ if (!Number.isFinite(count) || count <= 0) return this.pendingCountValue;
7228
+ this.pendingCountValue = Math.max(0, this.pendingCountValue - Math.floor(count));
7229
+ return this.pendingCountValue;
7230
+ }
7119
7231
  clearTimer() {
7120
7232
  if (this.timerValue) {
7121
7233
  clearTimeout(this.timerValue);
@@ -7304,12 +7416,12 @@ function findSessionJsonl(root, predicate) {
7304
7416
  for (const entry of entries) {
7305
7417
  if (++visited > maxEntries) return null;
7306
7418
  if (!entry.isFile() || !predicate(entry.name)) continue;
7307
- return path13.join(dir, entry.name);
7419
+ return path11.join(dir, entry.name);
7308
7420
  }
7309
7421
  for (const entry of entries) {
7310
7422
  if (++visited > maxEntries) return null;
7311
7423
  if (!entry.isDirectory()) continue;
7312
- const found = visit(path13.join(dir, entry.name), depth - 1);
7424
+ const found = visit(path11.join(dir, entry.name), depth - 1);
7313
7425
  if (found) return found;
7314
7426
  }
7315
7427
  return null;
@@ -7322,10 +7434,10 @@ function safeSessionFilename(value) {
7322
7434
  }
7323
7435
  function writeRuntimeSessionHandoff(runtime, sessionId, fallbackDir) {
7324
7436
  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({
7437
+ const dir = path11.join(fallbackDir, ".slock", "runtime-sessions");
7438
+ mkdirSync3(dir, { recursive: true });
7439
+ const filePath = path11.join(dir, `${runtime}-${safeSessionFilename(sessionId)}.jsonl`);
7440
+ writeFileSync4(filePath, JSON.stringify({
7329
7441
  type: "runtime_session_handoff",
7330
7442
  runtime,
7331
7443
  sessionId,
@@ -7344,7 +7456,7 @@ function writeRuntimeSessionHandoff(runtime, sessionId, fallbackDir) {
7344
7456
  }
7345
7457
  }
7346
7458
  function resolveRuntimeSessionRef(runtime, sessionId, homeDir = os5.homedir(), fallbackDir) {
7347
- const directPath = path13.isAbsolute(sessionId) ? sessionId : null;
7459
+ const directPath = path11.isAbsolute(sessionId) ? sessionId : null;
7348
7460
  if (directPath) {
7349
7461
  try {
7350
7462
  if (statSync(directPath).isFile()) {
@@ -7353,7 +7465,7 @@ function resolveRuntimeSessionRef(runtime, sessionId, homeDir = os5.homedir(), f
7353
7465
  } catch {
7354
7466
  }
7355
7467
  }
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;
7468
+ 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
7469
  if (!resolvedPath && fallbackDir) {
7358
7470
  const fallback = writeRuntimeSessionHandoff(runtime, sessionId, fallbackDir);
7359
7471
  if (fallback) return fallback;
@@ -7956,8 +8068,7 @@ function runtimeProfileTurnControl(kind, key, source) {
7956
8068
  source,
7957
8069
  keyHash: hashRuntimeProfileKey(key) ?? null,
7958
8070
  keyPresent: Boolean(key),
7959
- injectedAtMs: Date.now(),
7960
- migrationDoneToolObserved: false
8071
+ injectedAtMs: Date.now()
7961
8072
  };
7962
8073
  }
7963
8074
  function pushRecentLines(lines, chunk, maxLines, maxLineLength) {
@@ -8033,6 +8144,9 @@ function classifyStickyTerminalFailure(ap) {
8033
8144
  if (isRuntimeStartTimeoutText(terminalFailure.detail)) return null;
8034
8145
  return null;
8035
8146
  }
8147
+ function isAuthClassTerminalLine(text) {
8148
+ return buildRuntimeErrorDiagnosticEnvelope(text).spanAttrs.runtime_error_action_required === true;
8149
+ }
8036
8150
  function isProviderStreamFailureText(text) {
8037
8151
  return /stream closed before response\.completed|error decoding response body/i.test(text);
8038
8152
  }
@@ -8314,6 +8428,29 @@ var NATIVE_STANDING_PROMPT_STARTUP_INPUT = (
8314
8428
  );
8315
8429
  var AgentProcessManager = class _AgentProcessManager {
8316
8430
  agents = /* @__PURE__ */ new Map();
8431
+ /**
8432
+ * Per-agent monotonic clientSeq for `agent:activity` upstream dedup.
8433
+ * Server dedupes ingest by `(launchId, clientSeq)` (#engineering:72283cf7
8434
+ * task #340 PR B). This counter is keyed by agentId at the MANAGER level —
8435
+ * NOT on the per-process `AgentProcess` — so it SURVIVES process recreation
8436
+ * (cold-start-resume / idle-auto-restart / queued-continuation /
8437
+ * runtime-profile restart). Those daemon-internal restarts reuse the
8438
+ * server-issued launchId but spawn a fresh process; if the counter reset to
8439
+ * 0 (as the old per-`ap` field did) the reused launchId's `(launchId,
8440
+ * clientSeq)` window would collide and the server would drop the
8441
+ * post-restart activity as stale — the "activity log stops updating after
8442
+ * error→restart" bug. Keeping it monotonic-per-agent and never resetting on
8443
+ * respawn guarantees a fresh dedup key under the same launchId. launchId is
8444
+ * decoupled: it stays the identity/gate token, this is the clock.
8445
+ * (#proj-o11y:a1e54b59 — RS dedup-epoch ⊥ launchId-identity separation.)
8446
+ */
8447
+ activityClientSeqByAgent = /* @__PURE__ */ new Map();
8448
+ /** Next monotonic `agent:activity` clientSeq for this agent (never resets on respawn). */
8449
+ nextActivityClientSeq(agentId) {
8450
+ const next = (this.activityClientSeqByAgent.get(agentId) ?? 0) + 1;
8451
+ this.activityClientSeqByAgent.set(agentId, next);
8452
+ return next;
8453
+ }
8317
8454
  agentsStarting = /* @__PURE__ */ new Set();
8318
8455
  // Prevent concurrent starts of same agent
8319
8456
  queuedAgentStarts = /* @__PURE__ */ new Map();
@@ -8327,7 +8464,6 @@ var AgentProcessManager = class _AgentProcessManager {
8327
8464
  startingInboxes = /* @__PURE__ */ new Map();
8328
8465
  /** Cached configs for agents whose process exited normally — enables auto-restart on next message */
8329
8466
  idleAgentConfigs = /* @__PURE__ */ new Map();
8330
- chatBridgePath;
8331
8467
  slockCliPath;
8332
8468
  sendToServer;
8333
8469
  daemonApiKey;
@@ -8345,8 +8481,7 @@ var AgentProcessManager = class _AgentProcessManager {
8345
8481
  agentVisibleMessageIds = /* @__PURE__ */ new Map();
8346
8482
  daemonVersion;
8347
8483
  computerVersion;
8348
- constructor(chatBridgePath, sendToServer, daemonApiKey, opts) {
8349
- this.chatBridgePath = chatBridgePath;
8484
+ constructor(sendToServer, daemonApiKey, opts) {
8350
8485
  this.slockCliPath = opts.slockCliPath ?? "";
8351
8486
  this.sendToServer = sendToServer;
8352
8487
  this.daemonApiKey = daemonApiKey;
@@ -8501,6 +8636,48 @@ var AgentProcessManager = class _AgentProcessManager {
8501
8636
  suppressed_pending_count: removedActive + removedStarting
8502
8637
  });
8503
8638
  }
8639
+ purgeInboxMessagesForChannels(agentId, channelIds, reason = "server_purge") {
8640
+ const channelIdSet = new Set(channelIds.filter((id) => typeof id === "string" && id.length > 0));
8641
+ if (channelIdSet.size === 0) return { removedCount: 0 };
8642
+ const purge = (messages) => {
8643
+ if (!messages || messages.length === 0) return 0;
8644
+ let removed = 0;
8645
+ const retained = messages.filter((message) => {
8646
+ const shouldRemove = channelIdSet.has(message.channel_id);
8647
+ if (shouldRemove) removed += 1;
8648
+ return !shouldRemove;
8649
+ });
8650
+ if (removed > 0) {
8651
+ messages.splice(0, messages.length, ...retained);
8652
+ }
8653
+ return removed;
8654
+ };
8655
+ const active = this.agents.get(agentId);
8656
+ const removedActive = purge(active?.inbox);
8657
+ const startingInbox = this.startingInboxes.get(agentId);
8658
+ const removedStarting = purge(startingInbox);
8659
+ if (startingInbox && startingInbox.length === 0) {
8660
+ this.startingInboxes.delete(agentId);
8661
+ }
8662
+ if (active && removedActive > 0) {
8663
+ active.notifications.remove(removedActive);
8664
+ if (active.inbox.length === 0) {
8665
+ active.notifications.clear();
8666
+ }
8667
+ }
8668
+ const removedCount = removedActive + removedStarting;
8669
+ this.recordDaemonTrace("daemon.agent.inbox.purged", {
8670
+ agentId,
8671
+ reason,
8672
+ channel_count: channelIdSet.size,
8673
+ removed_active_count: removedActive,
8674
+ removed_starting_count: removedStarting,
8675
+ removed_count: removedCount,
8676
+ active_inbox_count: active?.inbox.length ?? 0,
8677
+ starting_inbox_count: this.startingInboxes.get(agentId)?.length ?? 0
8678
+ });
8679
+ return { removedCount };
8680
+ }
8504
8681
  createAgentProxyInboxCoordinator(agentId) {
8505
8682
  return {
8506
8683
  getBoundary: (target) => this.getVisibleBoundary(agentId, target),
@@ -8578,7 +8755,6 @@ var AgentProcessManager = class _AgentProcessManager {
8578
8755
  target: input.target,
8579
8756
  messageCount
8580
8757
  }).entry;
8581
- if (ap) ap.activityClientSeq += 1;
8582
8758
  this.sendToServer({
8583
8759
  type: "agent:activity",
8584
8760
  agentId,
@@ -8586,7 +8762,7 @@ var AgentProcessManager = class _AgentProcessManager {
8586
8762
  detail: ap?.lastActivityDetail || "",
8587
8763
  entries: [entry],
8588
8764
  launchId: ap?.launchId || void 0,
8589
- clientSeq: ap?.activityClientSeq
8765
+ clientSeq: ap ? this.nextActivityClientSeq(agentId) : void 0
8590
8766
  });
8591
8767
  }
8592
8768
  recordDaemonTrace(name, attrs, status = "ok", parentTraceparent) {
@@ -8832,7 +9008,7 @@ var AgentProcessManager = class _AgentProcessManager {
8832
9008
  );
8833
9009
  wakeMessage = void 0;
8834
9010
  }
8835
- const agentDataDir = path13.join(this.dataDir, agentId);
9011
+ const agentDataDir = path11.join(this.dataDir, agentId);
8836
9012
  await mkdir(agentDataDir, { recursive: true });
8837
9013
  let runtimeConfig = withLocalRuntimeContext(config, agentId, agentDataDir);
8838
9014
  const legacyRuntimeProfileControl = runtimeConfig.runtimeProfileControl?.kind === "migration" ? runtimeConfig.runtimeProfileControl : null;
@@ -8846,23 +9022,23 @@ var AgentProcessManager = class _AgentProcessManager {
8846
9022
  );
8847
9023
  runtimeConfig = { ...runtimeConfig, runtimeProfileControl: null };
8848
9024
  }
8849
- const memoryMdPath = path13.join(agentDataDir, "MEMORY.md");
9025
+ const memoryMdPath = path11.join(agentDataDir, "MEMORY.md");
8850
9026
  try {
8851
9027
  await access(memoryMdPath);
8852
9028
  } catch {
8853
9029
  const initialMemoryMd = buildInitialMemoryMd(runtimeConfig);
8854
9030
  await writeFile(memoryMdPath, initialMemoryMd);
8855
9031
  }
8856
- const notesDir = path13.join(agentDataDir, "notes");
9032
+ const notesDir = path11.join(agentDataDir, "notes");
8857
9033
  await mkdir(notesDir, { recursive: true });
8858
9034
  if (getOnboardingSeedMode(config) === FIRST_CINDY_SEED_MODE) {
8859
9035
  const seedFiles = buildOnboardingSeedFiles();
8860
9036
  for (const { relativePath, content } of seedFiles) {
8861
- const fullPath = path13.join(agentDataDir, relativePath);
9037
+ const fullPath = path11.join(agentDataDir, relativePath);
8862
9038
  try {
8863
9039
  await access(fullPath);
8864
9040
  } catch {
8865
- await mkdir(path13.dirname(fullPath), { recursive: true });
9041
+ await mkdir(path11.dirname(fullPath), { recursive: true });
8866
9042
  await writeFile(fullPath, content);
8867
9043
  }
8868
9044
  }
@@ -8970,7 +9146,6 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
8970
9146
  standingPrompt,
8971
9147
  prompt,
8972
9148
  workingDirectory: agentDataDir,
8973
- chatBridgePath: this.chatBridgePath,
8974
9149
  slockCliPath: this.slockCliPath,
8975
9150
  daemonApiKey: this.daemonApiKey,
8976
9151
  launchId: launchId || null,
@@ -8999,7 +9174,6 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
8999
9174
  runtimeTraceCounters: createRuntimeTraceCounters(),
9000
9175
  lastActivity: "",
9001
9176
  lastActivityDetail: "",
9002
- activityClientSeq: 0,
9003
9177
  recentStdout: [],
9004
9178
  recentStderr: [],
9005
9179
  lastRuntimeError: null,
@@ -9118,7 +9292,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
9118
9292
  const startupTimeoutTermination = ap.expectedTerminationReason === "startup_timeout";
9119
9293
  const expectedTermination = Boolean(ap.expectedTerminationReason);
9120
9294
  const stickyTerminalFailureDetail = classifyStickyTerminalFailure(ap);
9121
- const processEndedCleanly = !stickyTerminalFailureDetail && (finalCode === 0 || expectedTermination && !ap.lastRuntimeError);
9295
+ const processEndedCleanly = !stickyTerminalFailureDetail && !startupTimeoutTermination && (finalCode === 0 || expectedTermination && !ap.lastRuntimeError);
9122
9296
  const terminalFailureDetail = processEndedCleanly ? null : stickyTerminalFailureDetail ?? classifyTerminalFailure(ap);
9123
9297
  const resumeRecoveryReason = resumeSessionRecoveryReason(ap);
9124
9298
  const shouldColdStartResumeSession = resumeRecoveryReason !== null;
@@ -9527,6 +9701,9 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
9527
9701
  cleanupAgentCredentialProxy(agentId, ap.launchId);
9528
9702
  this.revokeManagedRunnerCredential(agentId, ap.config, ap.launchId);
9529
9703
  this.agents.delete(agentId);
9704
+ if (!silent) {
9705
+ this.activityClientSeqByAgent.delete(agentId);
9706
+ }
9530
9707
  this.runtimeExitTraceAttrs.set(ap.runtime, {
9531
9708
  stop_source: silent ? "daemon_internal" : "explicit_request",
9532
9709
  stop_wait_requested: wait,
@@ -9538,7 +9715,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
9538
9715
  reason: silent ? "daemon_internal" : "explicit_request"
9539
9716
  });
9540
9717
  if (!silent) {
9541
- this.sendRuntimeProfileReportFor(agentId, ap.config, ap.sessionId, ap.launchId);
9718
+ this.sendRuntimeProfileReportFor(agentId, ap.config, ap.sessionId, ap.launchId, "stop");
9542
9719
  this.sendAgentStatus(agentId, "inactive", ap.launchId);
9543
9720
  this.broadcastActivity(agentId, "offline", "Stopped");
9544
9721
  logger.info(`[Agent ${agentId}] Stopped by request`);
@@ -9681,20 +9858,37 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
9681
9858
  }));
9682
9859
  return true;
9683
9860
  }
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;
9861
+ if (stickyTerminalFailure.actionRequired) {
9862
+ if (ap.lastRuntimeError && isAuthClassTerminalLine(ap.lastRuntimeError)) {
9863
+ ap.lastRuntimeError = null;
9864
+ }
9865
+ ap.recentStderr = ap.recentStderr.filter((line) => !isAuthClassTerminalLine(line));
9866
+ this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
9867
+ outcome: "user_turn_recover_from_sticky_terminal_error",
9868
+ accepted: true,
9869
+ process_present: true,
9870
+ runtime: ap.config.runtime,
9871
+ session_id_present: Boolean(ap.sessionId),
9872
+ launchId: ap.launchId || void 0,
9873
+ is_idle: ap.isIdle,
9874
+ inbox_count: ap.inbox.length
9875
+ }));
9876
+ } else {
9877
+ ap.inbox.push(message);
9878
+ this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
9879
+ outcome: "queued_terminal_runtime_error",
9880
+ accepted: true,
9881
+ process_present: true,
9882
+ runtime: ap.config.runtime,
9883
+ session_id_present: Boolean(ap.sessionId),
9884
+ launchId: ap.launchId || void 0,
9885
+ is_idle: ap.isIdle,
9886
+ inbox_count: ap.inbox.length
9887
+ }));
9888
+ this.sendAgentStatus(agentId, "inactive", ap.launchId);
9889
+ this.broadcastActivity(agentId, "error", stickyTerminalFailure.detail);
9890
+ return true;
9891
+ }
9698
9892
  }
9699
9893
  if (ap.isIdle && ap.driver.supportsStdinNotification && ap.sessionId) {
9700
9894
  if (transientDelivery) {
@@ -9862,7 +10056,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
9862
10056
  return true;
9863
10057
  }
9864
10058
  async resetWorkspace(agentId) {
9865
- const agentDataDir = path13.join(this.dataDir, agentId);
10059
+ const agentDataDir = path11.join(this.dataDir, agentId);
9866
10060
  try {
9867
10061
  await rm2(agentDataDir, { recursive: true, force: true });
9868
10062
  logger.info(`[Agent ${agentId}] Workspace reset complete (${agentDataDir})`);
@@ -9923,7 +10117,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
9923
10117
  return result;
9924
10118
  }
9925
10119
  buildRuntimeProfileReport(agentId, config, sessionId, launchId) {
9926
- const workspacePath = path13.join(this.dataDir, agentId);
10120
+ const workspacePath = path11.join(this.dataDir, agentId);
9927
10121
  return {
9928
10122
  agentId,
9929
10123
  launchId,
@@ -10141,7 +10335,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
10141
10335
  });
10142
10336
  span.end("ok", { attrs: { outcome: "agent_config_ack_sent" } });
10143
10337
  }
10144
- sendRuntimeProfileWireReport(report) {
10338
+ sendRuntimeProfileWireReport(report, source) {
10145
10339
  const span = this.tracer.startSpan("daemon.runtime_profile.report.sent", {
10146
10340
  surface: "daemon",
10147
10341
  kind: "producer",
@@ -10149,6 +10343,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
10149
10343
  agentId: report.agentId,
10150
10344
  launchId: report.launchId || void 0,
10151
10345
  runtime: report.facts.runtime,
10346
+ report_source: source,
10152
10347
  model_present: Boolean(report.facts.model),
10153
10348
  session_ref_present: Boolean(report.facts.sessionRef),
10154
10349
  workspace_ref_present: Boolean(report.facts.workspaceRef || report.facts.workspacePathRef)
@@ -10159,17 +10354,18 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
10159
10354
  agentId: report.agentId,
10160
10355
  facts: report.facts,
10161
10356
  launchId: report.launchId || void 0,
10162
- traceparent: formatTraceparent(span.context)
10357
+ traceparent: formatTraceparent(span.context),
10358
+ source
10163
10359
  });
10164
10360
  span.end("ok");
10165
10361
  }
10166
- sendRuntimeProfileReportFor(agentId, config, sessionId, launchId) {
10167
- this.sendRuntimeProfileWireReport(this.buildRuntimeProfileReport(agentId, config, sessionId, launchId));
10362
+ sendRuntimeProfileReportFor(agentId, config, sessionId, launchId, source) {
10363
+ this.sendRuntimeProfileWireReport(this.buildRuntimeProfileReport(agentId, config, sessionId, launchId), source);
10168
10364
  }
10169
- sendRuntimeProfileReport(agentId) {
10365
+ sendRuntimeProfileReport(agentId, source) {
10170
10366
  const report = this.getAgentRuntimeProfileReport(agentId);
10171
10367
  if (!report) return;
10172
- this.sendRuntimeProfileWireReport(report);
10368
+ this.sendRuntimeProfileWireReport(report, source);
10173
10369
  }
10174
10370
  // Machine-level workspace scanning
10175
10371
  async scanAllWorkspaces() {
@@ -10180,7 +10376,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
10180
10376
  }
10181
10377
  // Workspace file browsing
10182
10378
  async getFileTree(agentId, dirPath) {
10183
- const agentDir = path13.join(this.dataDir, agentId);
10379
+ const agentDir = path11.join(this.dataDir, agentId);
10184
10380
  try {
10185
10381
  await stat2(agentDir);
10186
10382
  } catch {
@@ -10188,8 +10384,8 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
10188
10384
  }
10189
10385
  let targetDir = agentDir;
10190
10386
  if (dirPath) {
10191
- const resolved = path13.resolve(agentDir, dirPath);
10192
- if (!resolved.startsWith(agentDir + path13.sep) && resolved !== agentDir) {
10387
+ const resolved = path11.resolve(agentDir, dirPath);
10388
+ if (!resolved.startsWith(agentDir + path11.sep) && resolved !== agentDir) {
10193
10389
  return [];
10194
10390
  }
10195
10391
  targetDir = resolved;
@@ -10197,14 +10393,14 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
10197
10393
  return this.listDirectoryChildren(targetDir, agentDir);
10198
10394
  }
10199
10395
  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) {
10396
+ const agentDir = path11.join(this.dataDir, agentId);
10397
+ const resolved = path11.resolve(agentDir, filePath);
10398
+ if (!resolved.startsWith(agentDir + path11.sep) && resolved !== agentDir) {
10203
10399
  throw new Error("Access denied");
10204
10400
  }
10205
10401
  const info = await stat2(resolved);
10206
10402
  if (info.isDirectory()) throw new Error("Cannot read a directory");
10207
- const ext = path13.extname(resolved).toLowerCase();
10403
+ const ext = path11.extname(resolved).toLowerCase();
10208
10404
  if (WORKSPACE_TEXT_EXTENSIONS.has(ext) || ext === "") {
10209
10405
  if (info.size > WORKSPACE_TEXT_FILE_MAX_BYTES) throw new Error("File too large");
10210
10406
  const content = await readFile(resolved, "utf-8");
@@ -10239,13 +10435,13 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
10239
10435
  const agent = this.agents.get(agentId);
10240
10436
  const runtime = runtimeHint || agent?.config.runtime || "claude";
10241
10437
  const home = os5.homedir();
10242
- const workspaceDir = path13.join(this.dataDir, agentId);
10438
+ const workspaceDir = path11.join(this.dataDir, agentId);
10243
10439
  const paths = _AgentProcessManager.SKILL_PATHS[runtime] || _AgentProcessManager.SKILL_PATHS.claude;
10244
10440
  const globalResults = await Promise.all(
10245
- paths.global.map((p) => this.scanSkillsDir(path13.join(home, p)))
10441
+ paths.global.map((p) => this.scanSkillsDir(path11.join(home, p)))
10246
10442
  );
10247
10443
  const workspaceResults = await Promise.all(
10248
- paths.workspace.map((p) => this.scanSkillsDir(path13.join(workspaceDir, p)))
10444
+ paths.workspace.map((p) => this.scanSkillsDir(path11.join(workspaceDir, p)))
10249
10445
  );
10250
10446
  const dedup = (skills) => {
10251
10447
  const seen = /* @__PURE__ */ new Set();
@@ -10274,7 +10470,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
10274
10470
  const skills = [];
10275
10471
  for (const entry of entries) {
10276
10472
  if (entry.isDirectory() || entry.isSymbolicLink()) {
10277
- const skillMd = path13.join(dir, entry.name, "SKILL.md");
10473
+ const skillMd = path11.join(dir, entry.name, "SKILL.md");
10278
10474
  try {
10279
10475
  const content = await readFile(skillMd, "utf-8");
10280
10476
  const skill = this.parseSkillMd(entry.name, content);
@@ -10285,7 +10481,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
10285
10481
  } else if (entry.name.endsWith(".md")) {
10286
10482
  const cmdName = entry.name.replace(/\.md$/, "");
10287
10483
  try {
10288
- const content = await readFile(path13.join(dir, entry.name), "utf-8");
10484
+ const content = await readFile(path11.join(dir, entry.name), "utf-8");
10289
10485
  const skill = this.parseSkillMd(cmdName, content);
10290
10486
  skill.sourcePath = dir;
10291
10487
  skills.push(skill);
@@ -10334,7 +10530,6 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
10334
10530
  if (!hasToolStart) {
10335
10531
  entries.push({ kind: "status", activity, detail });
10336
10532
  }
10337
- if (ap) ap.activityClientSeq += 1;
10338
10533
  this.sendToServer({
10339
10534
  type: "agent:activity",
10340
10535
  agentId,
@@ -10342,7 +10537,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
10342
10537
  detail,
10343
10538
  entries,
10344
10539
  launchId: launchIdOverride || ap?.launchId || void 0,
10345
- clientSeq: ap?.activityClientSeq
10540
+ clientSeq: ap ? this.nextActivityClientSeq(agentId) : void 0
10346
10541
  });
10347
10542
  if (ap) {
10348
10543
  ap.lastActivity = activity;
@@ -10354,14 +10549,13 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
10354
10549
  this.recordRuntimeTraceEvent(agentId, ap, "activity.heartbeat.sent", {
10355
10550
  activity: ap.lastActivity
10356
10551
  });
10357
- ap.activityClientSeq += 1;
10358
10552
  this.sendToServer({
10359
10553
  type: "agent:activity",
10360
10554
  agentId,
10361
10555
  activity: ap.lastActivity,
10362
10556
  detail: ap.lastActivityDetail,
10363
10557
  launchId: launchIdOverride || ap.launchId || void 0,
10364
- clientSeq: ap.activityClientSeq
10558
+ clientSeq: this.nextActivityClientSeq(agentId)
10365
10559
  });
10366
10560
  }, ACTIVITY_HEARTBEAT_MS);
10367
10561
  }
@@ -10398,7 +10592,6 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
10398
10592
  const ap = this.agents.get(agentId);
10399
10593
  const activity = ap?.lastActivity || "offline";
10400
10594
  const detail = ap?.lastActivityDetail || (ap ? "" : "Agent not running");
10401
- if (ap) ap.activityClientSeq += 1;
10402
10595
  this.sendToServer({
10403
10596
  type: "agent:activity",
10404
10597
  agentId,
@@ -10406,7 +10599,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
10406
10599
  detail,
10407
10600
  launchId: ap?.launchId || void 0,
10408
10601
  probeId,
10409
- clientSeq: ap?.activityClientSeq
10602
+ clientSeq: ap ? this.nextActivityClientSeq(agentId) : void 0
10410
10603
  });
10411
10604
  }
10412
10605
  flushPendingTrajectory(agentId) {
@@ -10608,8 +10801,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
10608
10801
  runtime_profile_key_hash: control.keyHash || void 0,
10609
10802
  runtime_profile_key_present: control.keyPresent,
10610
10803
  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
10804
+ runtime_profile_requires_ack: false
10613
10805
  };
10614
10806
  }
10615
10807
  activateRuntimeProfileTurnControl(ap, control) {
@@ -10621,16 +10813,6 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
10621
10813
  if (!notification) return null;
10622
10814
  return runtimeProfileTurnControl(notification.kind, notification.key, source);
10623
10815
  }
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
10816
  finalizeRuntimeProfileTurnControl(agentId, ap, terminal) {
10635
10817
  const control = ap.runtimeProfileTurnControl;
10636
10818
  if (!control) return {};
@@ -10639,7 +10821,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
10639
10821
  return {
10640
10822
  ...attrs,
10641
10823
  runtime_profile_turn_terminal: terminal,
10642
- runtime_profile_turn_outcome: control.kind === "migration" ? control.migrationDoneToolObserved ? "deprecated_migration_done_observed" : "reset_session_notice" : "notice_only"
10824
+ runtime_profile_turn_outcome: control.kind === "migration" ? "reset_session_notice" : "notice_only"
10643
10825
  };
10644
10826
  }
10645
10827
  startRuntimeTrace(agentId, ap, reason, messages, inputTraceAttrs = {}) {
@@ -11018,7 +11200,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
11018
11200
  case "session_init":
11019
11201
  if (ap) ap.sessionId = event.sessionId;
11020
11202
  this.sendToServer({ type: "agent:session", agentId, sessionId: event.sessionId, launchId: ap?.launchId || void 0 });
11021
- this.sendRuntimeProfileReport(agentId);
11203
+ this.sendRuntimeProfileReport(agentId, "session_init");
11022
11204
  break;
11023
11205
  case "thinking": {
11024
11206
  this.completeCompactionIfActive(agentId, "Context compaction finished (inferred from resumed output)");
@@ -11044,7 +11226,6 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
11044
11226
  const invocation = normalizeToolDisplayInvocation(event.name, event.input);
11045
11227
  if (ap) {
11046
11228
  const reduction = reduceApmGatedToolUse(ap.gatedSteering, { kind: "tool_call" });
11047
- this.noteRuntimeProfileToolCall(agentId, ap, invocation.toolName);
11048
11229
  this.recordRuntimeTraceEvent(agentId, ap, "tool.call.started", { tool: invocation.toolName });
11049
11230
  this.commitGatedSteeringDecisionState(agentId, ap, reduction.nextState, {
11050
11231
  event: "tool_call",
@@ -11157,7 +11338,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
11157
11338
  }
11158
11339
  if (event.sessionId) {
11159
11340
  this.sendToServer({ type: "agent:session", agentId, sessionId: event.sessionId, launchId: ap?.launchId || void 0 });
11160
- this.sendRuntimeProfileReport(agentId);
11341
+ this.sendRuntimeProfileReport(agentId, "turn_end");
11161
11342
  }
11162
11343
  break;
11163
11344
  case "error": {
@@ -11625,8 +11806,8 @@ ${RESPONSE_TARGET_HINT}`);
11625
11806
  const nodes = [];
11626
11807
  for (const entry of entries) {
11627
11808
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
11628
- const fullPath = path13.join(dir, entry.name);
11629
- const relativePath = path13.relative(rootDir, fullPath);
11809
+ const fullPath = path11.join(dir, entry.name);
11810
+ const relativePath = path11.relative(rootDir, fullPath);
11630
11811
  let info;
11631
11812
  try {
11632
11813
  info = await stat2(fullPath);
@@ -11917,7 +12098,7 @@ var ReminderCache = class {
11917
12098
  logger.warn(`[ReminderCache] Invalid fireAt for ${job.reminderId}: ${job.fireAt}`);
11918
12099
  return null;
11919
12100
  }
11920
- const delay = Math.max(0, Math.min(this.maxDelayMs, fireAt - this.clock.now()));
12101
+ const delay2 = Math.max(0, Math.min(this.maxDelayMs, fireAt - this.clock.now()));
11921
12102
  return this.clock.setTimeout(() => {
11922
12103
  this.entries.delete(job.reminderId);
11923
12104
  try {
@@ -11925,15 +12106,15 @@ var ReminderCache = class {
11925
12106
  } catch (err) {
11926
12107
  logger.error(`[ReminderCache] onFire threw for ${job.reminderId}`, err);
11927
12108
  }
11928
- }, delay);
12109
+ }, delay2);
11929
12110
  }
11930
12111
  };
11931
12112
 
11932
12113
  // src/machineLock.ts
11933
12114
  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";
12115
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync5, rmSync as rmSync3, statSync as statSync2, writeFileSync as writeFileSync5 } from "fs";
11935
12116
  import os6 from "os";
11936
- import path14 from "path";
12117
+ import path12 from "path";
11937
12118
  var INCOMPLETE_LOCK_STALE_MS = 3e4;
11938
12119
  var DaemonMachineLockConflictError = class extends Error {
11939
12120
  code = "DAEMON_MACHINE_LOCK_HELD";
@@ -11955,7 +12136,7 @@ function resolveDefaultMachineStateRoot() {
11955
12136
  return resolveSlockHomePath("machines");
11956
12137
  }
11957
12138
  function ownerPath(lockDir) {
11958
- return path14.join(lockDir, "owner.json");
12139
+ return path12.join(lockDir, "owner.json");
11959
12140
  }
11960
12141
  function readOwner(lockDir) {
11961
12142
  try {
@@ -11985,13 +12166,13 @@ function acquireDaemonMachineLock(options) {
11985
12166
  const rootDir = options.rootDir ?? resolveDefaultMachineStateRoot();
11986
12167
  const fingerprint = apiKeyFingerprint(options.apiKey);
11987
12168
  const lockId = getDaemonMachineLockId(options.apiKey);
11988
- const machineDir = path14.join(rootDir, lockId);
11989
- const lockDir = path14.join(machineDir, "daemon.lock");
12169
+ const machineDir = path12.join(rootDir, lockId);
12170
+ const lockDir = path12.join(machineDir, "daemon.lock");
11990
12171
  const token = randomUUID4();
11991
- mkdirSync6(machineDir, { recursive: true });
12172
+ mkdirSync4(machineDir, { recursive: true });
11992
12173
  for (let attempt = 0; attempt < 2; attempt += 1) {
11993
12174
  try {
11994
- mkdirSync6(lockDir);
12175
+ mkdirSync4(lockDir);
11995
12176
  const owner = {
11996
12177
  pid: process.pid,
11997
12178
  token,
@@ -12001,7 +12182,7 @@ function acquireDaemonMachineLock(options) {
12001
12182
  apiKeyFingerprint: fingerprint.slice(0, 16)
12002
12183
  };
12003
12184
  try {
12004
- writeFileSync8(ownerPath(lockDir), `${JSON.stringify(owner, null, 2)}
12185
+ writeFileSync5(ownerPath(lockDir), `${JSON.stringify(owner, null, 2)}
12005
12186
  `, { mode: 384 });
12006
12187
  } catch (err) {
12007
12188
  rmSync3(lockDir, { recursive: true, force: true });
@@ -12016,7 +12197,7 @@ function acquireDaemonMachineLock(options) {
12016
12197
  if (currentOwner?.pid === process.pid && currentOwner.token === token) {
12017
12198
  const released = { ...currentOwner, pid: 0 };
12018
12199
  try {
12019
- writeFileSync8(ownerPath(lockDir), `${JSON.stringify(released, null, 2)}
12200
+ writeFileSync5(ownerPath(lockDir), `${JSON.stringify(released, null, 2)}
12020
12201
  `, {
12021
12202
  mode: 384
12022
12203
  });
@@ -12046,8 +12227,8 @@ function acquireDaemonMachineLock(options) {
12046
12227
  }
12047
12228
 
12048
12229
  // 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";
12230
+ import { appendFileSync, mkdirSync as mkdirSync5, readdirSync as readdirSync3, rmSync as rmSync4, statSync as statSync3, writeFileSync as writeFileSync6 } from "fs";
12231
+ import path13 from "path";
12051
12232
  var DEFAULT_MAX_FILE_BYTES = 5 * 1024 * 1024;
12052
12233
  var DEFAULT_MAX_FILE_AGE_MS = 5 * 60 * 1e3;
12053
12234
  var DEFAULT_MAX_FILES = 8;
@@ -12084,7 +12265,7 @@ var LocalRotatingTraceSink = class {
12084
12265
  currentSize = 0;
12085
12266
  sequence = 0;
12086
12267
  constructor(options) {
12087
- this.traceDir = path15.join(options.machineDir, "traces");
12268
+ this.traceDir = path13.join(options.machineDir, "traces");
12088
12269
  this.maxFileBytes = Math.max(1024, Math.floor(options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES));
12089
12270
  const baseAgeMs = Math.max(1e3, Math.floor(options.maxFileAgeMs ?? DEFAULT_MAX_FILE_AGE_MS));
12090
12271
  const ageJitterMs = Math.max(0, Math.floor(options.maxFileAgeJitterMs ?? 0));
@@ -12110,15 +12291,15 @@ var LocalRotatingTraceSink = class {
12110
12291
  return this.currentFile;
12111
12292
  }
12112
12293
  ensureFile(nextBytes) {
12113
- mkdirSync7(this.traceDir, { recursive: true, mode: 448 });
12294
+ mkdirSync5(this.traceDir, { recursive: true, mode: 448 });
12114
12295
  const nowMs = this.nowMsProvider();
12115
12296
  const shouldRotateForAge = this.currentFileOpenedAtMs !== null && nowMs - this.currentFileOpenedAtMs >= this.maxFileAgeMs;
12116
12297
  if (!this.currentFile || this.currentSize + nextBytes > this.maxFileBytes || shouldRotateForAge) {
12117
- this.currentFile = path15.join(
12298
+ this.currentFile = path13.join(
12118
12299
  this.traceDir,
12119
12300
  `daemon-trace-${safeTimestamp(nowMs)}-${process.pid}-${String(this.sequence++).padStart(4, "0")}.jsonl`
12120
12301
  );
12121
- writeFileSync9(this.currentFile, "", { flag: "a", mode: 384 });
12302
+ writeFileSync6(this.currentFile, "", { flag: "a", mode: 384 });
12122
12303
  this.currentSize = statSync3(this.currentFile).size;
12123
12304
  this.currentFileOpenedAtMs = nowMs;
12124
12305
  this.pruneOldFiles();
@@ -12129,7 +12310,7 @@ var LocalRotatingTraceSink = class {
12129
12310
  const excess = files.length - this.maxFiles;
12130
12311
  if (excess <= 0) return;
12131
12312
  for (const file of files.slice(0, excess)) {
12132
- rmSync4(path15.join(this.traceDir, file), { force: true });
12313
+ rmSync4(path13.join(this.traceDir, file), { force: true });
12133
12314
  }
12134
12315
  }
12135
12316
  };
@@ -12220,11 +12401,108 @@ function isDiagnosticErrorAttr(key) {
12220
12401
  import { createHash as createHash6, randomUUID as randomUUID5 } from "crypto";
12221
12402
  import { gzipSync } from "zlib";
12222
12403
  import { mkdir as mkdir2, readFile as readFile2, readdir as readdir3, stat as stat3, writeFile as writeFile2 } from "fs/promises";
12223
- import path16 from "path";
12404
+ import path14 from "path";
12405
+
12406
+ // src/chatBridgeRequest.ts
12407
+ var DEFAULT_CHAT_BRIDGE_TOOL_TIMEOUT_MS = Number.parseInt(
12408
+ process.env.SLOCK_CHAT_BRIDGE_TOOL_TIMEOUT_MS || "",
12409
+ 10
12410
+ ) || 6e4;
12411
+ var ChatBridgeToolTimeoutError = class extends Error {
12412
+ toolName;
12413
+ target;
12414
+ timeoutMs;
12415
+ durationMs;
12416
+ constructor(toolName, target, timeoutMs, durationMs) {
12417
+ super(`${toolName} timed out after ${timeoutMs}ms${target ? ` (target: ${target})` : ""}`);
12418
+ this.name = "ChatBridgeToolTimeoutError";
12419
+ this.toolName = toolName;
12420
+ this.target = target;
12421
+ this.timeoutMs = timeoutMs;
12422
+ this.durationMs = durationMs;
12423
+ }
12424
+ };
12425
+ function describeError(err) {
12426
+ if (err instanceof Error) return `${err.name}: ${err.message}`;
12427
+ return String(err);
12428
+ }
12429
+ async function executeJsonRequest(url, init, {
12430
+ toolName,
12431
+ target = null,
12432
+ timeoutMs = DEFAULT_CHAT_BRIDGE_TOOL_TIMEOUT_MS,
12433
+ fetchImpl,
12434
+ now = () => Date.now(),
12435
+ warn = (message) => logger.warn(message)
12436
+ }) {
12437
+ const startedAt = now();
12438
+ const timeoutController = new AbortController();
12439
+ const signals = [timeoutController.signal];
12440
+ if (init.signal) signals.push(init.signal);
12441
+ const signal = signals.length === 1 ? signals[0] : AbortSignal.any(signals);
12442
+ const timeout = setTimeout(() => {
12443
+ timeoutController.abort();
12444
+ }, timeoutMs);
12445
+ timeout.unref?.();
12446
+ try {
12447
+ const response = await fetchImpl(url, { ...init, signal });
12448
+ const data = await response.json();
12449
+ return { response, data, durationMs: now() - startedAt };
12450
+ } catch (err) {
12451
+ const durationMs = now() - startedAt;
12452
+ if (timeoutController.signal.aborted && !init.signal?.aborted) {
12453
+ warn(
12454
+ `[ChatBridgeTimeout] tool=${toolName} target=${target ?? "-"} duration_ms=${durationMs} timeout_ms=${timeoutMs} outcome=timeout`
12455
+ );
12456
+ throw new ChatBridgeToolTimeoutError(toolName, target, timeoutMs, durationMs);
12457
+ }
12458
+ warn(
12459
+ `[ChatBridgeError] tool=${toolName} target=${target ?? "-"} duration_ms=${durationMs} outcome=error error=${describeError(err)}`
12460
+ );
12461
+ throw err;
12462
+ } finally {
12463
+ clearTimeout(timeout);
12464
+ }
12465
+ }
12466
+ async function executeResponseRequest(url, init, {
12467
+ toolName,
12468
+ target = null,
12469
+ timeoutMs = DEFAULT_CHAT_BRIDGE_TOOL_TIMEOUT_MS,
12470
+ fetchImpl,
12471
+ now = () => Date.now(),
12472
+ warn = (message) => logger.warn(message)
12473
+ }) {
12474
+ const startedAt = now();
12475
+ const timeoutController = new AbortController();
12476
+ const signals = [timeoutController.signal];
12477
+ if (init.signal) signals.push(init.signal);
12478
+ const signal = signals.length === 1 ? signals[0] : AbortSignal.any(signals);
12479
+ const timeout = setTimeout(() => {
12480
+ timeoutController.abort();
12481
+ }, timeoutMs);
12482
+ timeout.unref?.();
12483
+ try {
12484
+ const response = await fetchImpl(url, { ...init, signal });
12485
+ return { response, durationMs: now() - startedAt };
12486
+ } catch (err) {
12487
+ const durationMs = now() - startedAt;
12488
+ if (timeoutController.signal.aborted && !init.signal?.aborted) {
12489
+ warn(
12490
+ `[ChatBridgeTimeout] tool=${toolName} target=${target ?? "-"} duration_ms=${durationMs} timeout_ms=${timeoutMs} outcome=timeout`
12491
+ );
12492
+ throw new ChatBridgeToolTimeoutError(toolName, target, timeoutMs, durationMs);
12493
+ }
12494
+ warn(
12495
+ `[ChatBridgeError] tool=${toolName} target=${target ?? "-"} duration_ms=${durationMs} outcome=error error=${describeError(err)}`
12496
+ );
12497
+ throw err;
12498
+ } finally {
12499
+ clearTimeout(timeout);
12500
+ }
12501
+ }
12224
12502
 
12225
12503
  // src/directUploadCapability.ts
12226
- function joinUrl(base, path18) {
12227
- return `${base.replace(/\/+$/, "")}${path18}`;
12504
+ function joinUrl(base, path16) {
12505
+ return `${base.replace(/\/+$/, "")}${path16}`;
12228
12506
  }
12229
12507
  function jsonHeaders(apiKey) {
12230
12508
  return {
@@ -12443,7 +12721,7 @@ var DaemonTraceBundleUploader = class {
12443
12721
  }, nextMs);
12444
12722
  }
12445
12723
  async findUploadCandidates() {
12446
- const traceDir = path16.join(this.options.machineDir, "traces");
12724
+ const traceDir = path14.join(this.options.machineDir, "traces");
12447
12725
  let names;
12448
12726
  try {
12449
12727
  names = await readdir3(traceDir);
@@ -12455,8 +12733,8 @@ var DaemonTraceBundleUploader = class {
12455
12733
  const currentFile = this.options.currentFileProvider?.();
12456
12734
  const candidates = [];
12457
12735
  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;
12736
+ const file = path14.join(traceDir, name);
12737
+ if (currentFile && path14.resolve(file) === path14.resolve(currentFile)) continue;
12460
12738
  if (await this.isUploaded(file)) continue;
12461
12739
  try {
12462
12740
  const info = await stat3(file);
@@ -12530,8 +12808,8 @@ var DaemonTraceBundleUploader = class {
12530
12808
  }
12531
12809
  }
12532
12810
  uploadStatePath(file) {
12533
- const stateDir = path16.join(this.options.machineDir, "trace-uploads");
12534
- return path16.join(stateDir, `${path16.basename(file)}.uploaded.json`);
12811
+ const stateDir = path14.join(this.options.machineDir, "trace-uploads");
12812
+ return path14.join(stateDir, `${path14.basename(file)}.uploaded.json`);
12535
12813
  }
12536
12814
  async isUploaded(file) {
12537
12815
  try {
@@ -12543,9 +12821,9 @@ var DaemonTraceBundleUploader = class {
12543
12821
  }
12544
12822
  async markUploaded(file, metadata) {
12545
12823
  const stateFile = this.uploadStatePath(file);
12546
- await mkdir2(path16.dirname(stateFile), { recursive: true, mode: 448 });
12824
+ await mkdir2(path14.dirname(stateFile), { recursive: true, mode: 448 });
12547
12825
  await writeFile2(stateFile, `${JSON.stringify({
12548
- file: path16.basename(file),
12826
+ file: path14.basename(file),
12549
12827
  uploadedAt: (/* @__PURE__ */ new Date()).toISOString(),
12550
12828
  ...metadata
12551
12829
  }, null, 2)}
@@ -12621,24 +12899,14 @@ function readDaemonVersion(moduleUrl = import.meta.url) {
12621
12899
  return "0.0.0-dev";
12622
12900
  }
12623
12901
  }
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
12902
  function resolveSlockCliPath(moduleUrl = import.meta.url) {
12635
- const thisDir = path17.dirname(fileURLToPath(moduleUrl));
12636
- const bundledDistPath = path17.resolve(thisDir, "cli", "index.js");
12903
+ const thisDir = path15.dirname(fileURLToPath(moduleUrl));
12904
+ const bundledDistPath = path15.resolve(thisDir, "cli", "index.js");
12637
12905
  try {
12638
12906
  accessSync(bundledDistPath);
12639
12907
  return bundledDistPath;
12640
12908
  } catch {
12641
- const workspaceDistPath = path17.resolve(thisDir, "..", "..", "cli", "dist", "index.js");
12909
+ const workspaceDistPath = path15.resolve(thisDir, "..", "..", "cli", "dist", "index.js");
12642
12910
  accessSync(workspaceDistPath);
12643
12911
  return workspaceDistPath;
12644
12912
  }
@@ -12739,6 +13007,8 @@ function summarizeIncomingMessage(msg) {
12739
13007
  return `(agent=${msg.agentId})`;
12740
13008
  case "agent:deliver":
12741
13009
  return `(agent=${msg.agentId}, seq=${msg.seq}, from=@${msg.message.sender_name}, target=${formatChannelTarget(msg)})`;
13010
+ case "agent:inbox:purge":
13011
+ return `(agent=${msg.agentId}, channels=${msg.channelIds.length}, reason=${msg.reason || "server_purge"})`;
12742
13012
  case "agent:runtime_profile:migration":
12743
13013
  return `(agent=${msg.agentId}, migration=${msg.migrationKey})`;
12744
13014
  case "agent:runtime_profile:daemon_release_notice":
@@ -12773,7 +13043,6 @@ var DaemonCore = class {
12773
13043
  // bundle version). Reported in `ready` so the server can surface the
12774
13044
  // Computer's own version (distinct from the underlying daemonVersion).
12775
13045
  computerVersion;
12776
- chatBridgePath;
12777
13046
  slockCliPath;
12778
13047
  slockHome;
12779
13048
  runtimeDetector;
@@ -12789,7 +13058,6 @@ var DaemonCore = class {
12789
13058
  this.options = options;
12790
13059
  this.daemonVersion = options.daemonVersion ?? readDaemonVersion();
12791
13060
  this.computerVersion = (process.env.SLOCK_COMPUTER_VERSION || "").trim() || null;
12792
- this.chatBridgePath = options.chatBridgePath ?? resolveChatBridgePath();
12793
13061
  this.slockCliPath = options.slockCliPath ?? resolveSlockCliPath();
12794
13062
  this.slockHome = resolveSlockHome();
12795
13063
  process.env[SLOCK_HOME_ENV] = this.slockHome;
@@ -12810,7 +13078,7 @@ var DaemonCore = class {
12810
13078
  daemonVersion: this.daemonVersion,
12811
13079
  computerVersion: this.computerVersion
12812
13080
  };
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);
13081
+ this.agentManager = options.agentManagerFactory ? options.agentManagerFactory((msg) => connection.send(msg), options.apiKey, agentManagerOptions) : new AgentProcessManager((msg) => connection.send(msg), options.apiKey, agentManagerOptions);
12814
13082
  const connectionFactory = options.connectionFactory ?? ((connOptions) => new DaemonConnection(connOptions));
12815
13083
  connection = connectionFactory({
12816
13084
  serverUrl: options.serverUrl,
@@ -12825,7 +13093,7 @@ var DaemonCore = class {
12825
13093
  }
12826
13094
  resolveMachineStateRoot() {
12827
13095
  if (this.options.machineStateDir) return this.options.machineStateDir;
12828
- if (this.options.dataDir) return path17.join(path17.dirname(this.options.dataDir), "machines");
13096
+ if (this.options.dataDir) return path15.join(path15.dirname(this.options.dataDir), "machines");
12829
13097
  return resolveDefaultMachineStateRoot();
12830
13098
  }
12831
13099
  shouldEnableLocalTrace() {
@@ -12851,7 +13119,7 @@ var DaemonCore = class {
12851
13119
  sink: this.localTraceSink
12852
13120
  }));
12853
13121
  this.agentManager.setTracer(this.tracer);
12854
- this.agentManager.setCliTransportTraceDir(path17.join(machineDir, "traces"));
13122
+ this.agentManager.setCliTransportTraceDir(path15.join(machineDir, "traces"));
12855
13123
  }
12856
13124
  installTraceBundleUploader(machineDir) {
12857
13125
  if (!this.shouldEnableLocalTrace()) return;
@@ -13093,6 +13361,10 @@ var DaemonCore = class {
13093
13361
  logger.info(`[Agent ${msg.agentId}] Workspace reset requested`);
13094
13362
  this.agentManager.resetWorkspace(msg.agentId);
13095
13363
  break;
13364
+ case "agent:inbox:purge":
13365
+ logger.info(`[Agent ${msg.agentId}] Inbox purge requested (${msg.channelIds.length} channels, reason=${msg.reason || "server_purge"})`);
13366
+ this.agentManager.purgeInboxMessagesForChannels(msg.agentId, msg.channelIds, msg.reason || "server_purge");
13367
+ break;
13096
13368
  case "agent:deliver": {
13097
13369
  const parent = parseTraceparent(msg.traceparent);
13098
13370
  const span = this.tracer.startSpan("daemon.agent.delivery", {
@@ -13344,6 +13616,7 @@ var DaemonCore = class {
13344
13616
  agentId: report.agentId,
13345
13617
  launchId: report.launchId || void 0,
13346
13618
  runtime: report.facts.runtime,
13619
+ report_source: "connect",
13347
13620
  model_present: Boolean(report.facts.model),
13348
13621
  session_ref_present: Boolean(report.facts.sessionRef),
13349
13622
  workspace_ref_present: Boolean(report.facts.workspaceRef || report.facts.workspacePathRef)
@@ -13354,7 +13627,8 @@ var DaemonCore = class {
13354
13627
  agentId: report.agentId,
13355
13628
  facts: report.facts,
13356
13629
  launchId: report.launchId || void 0,
13357
- traceparent: formatTraceparent(span.context)
13630
+ traceparent: formatTraceparent(span.context),
13631
+ source: "connect"
13358
13632
  });
13359
13633
  span.end("ok");
13360
13634
  }
@@ -13378,13 +13652,13 @@ var DaemonCore = class {
13378
13652
  };
13379
13653
 
13380
13654
  export {
13655
+ subscribeDaemonLogs,
13381
13656
  resolveWorkspaceDirectoryPath,
13382
13657
  scanWorkspaceDirectories,
13383
13658
  deleteWorkspaceDirectory,
13384
13659
  DAEMON_CLI_USAGE,
13385
13660
  parseDaemonCliArgs,
13386
13661
  readDaemonVersion,
13387
- resolveChatBridgePath,
13388
13662
  resolveSlockCliPath,
13389
13663
  detectRuntimes,
13390
13664
  DaemonCore