@slock-ai/daemon 0.56.0 → 0.56.1-play.20260604141202

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.
@@ -932,10 +932,22 @@ var channelAddMemberOperationSchema = z.object({
932
932
  agents: z.array(idOrHandleSchema).max(64).optional(),
933
933
  draftHint: draftHintSchema
934
934
  });
935
+ var integrationApproveAgentLoginOperationSchema = z.object({
936
+ type: z.literal("integration:approve_agent_login"),
937
+ requestId: uuidSchema,
938
+ agentId: uuidSchema,
939
+ agentName: z.string().trim().min(1).max(120),
940
+ clientId: uuidSchema,
941
+ clientKey: z.string().trim().min(1).max(120),
942
+ clientName: z.string().trim().min(1).max(120),
943
+ scopes: z.array(z.string().trim().min(1).max(120)).max(64),
944
+ draftHint: draftHintSchema
945
+ });
935
946
  var actionCardActionSchema = z.discriminatedUnion("type", [
936
947
  channelCreateOperationSchema,
937
948
  agentCreateOperationSchema,
938
- channelAddMemberOperationSchema
949
+ channelAddMemberOperationSchema,
950
+ integrationApproveAgentLoginOperationSchema
939
951
  ]);
940
952
 
941
953
  // ../shared/src/agentInbox.ts
@@ -1276,6 +1288,329 @@ import { mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
1276
1288
  import { createRequire } from "module";
1277
1289
  import path2 from "path";
1278
1290
 
1291
+ // src/drivers/slockCliGuide.ts
1292
+ var MESSAGE_HEREDOC_DELIMITER = "SLOCKMSG";
1293
+ function describeSlockInstallation(audience) {
1294
+ if (audience === "managed-runner") {
1295
+ return "The daemon injects a local `slock` wrapper into PATH for you.";
1296
+ }
1297
+ 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>`).";
1298
+ }
1299
+ function buildCommunicationSection(audience) {
1300
+ const installation = describeSlockInstallation(audience);
1301
+ return `## Communication \u2014 slock CLI ONLY
1302
+
1303
+ Use the \`slock\` CLI for chat / task / attachment operations. ${installation} Use ONLY these commands for communication:
1304
+
1305
+ 1. **\`slock message check\`** \u2014 Non-blocking check for new messages. Use freely during work \u2014 at natural breakpoints or after notifications.
1306
+ 2. **\`slock message send\`** \u2014 Send a message to a channel or DM.
1307
+ 3. **\`slock server info\`** \u2014 List channels in this server, which ones you have joined, plus all agents and humans.
1308
+ 4. **\`slock channel members\`** \u2014 List the members (agents and humans) of a specific channel, DM, or thread target.
1309
+ 5. **\`slock channel join\`** \u2014 Join a visible public channel. This only affects your own agent membership.
1310
+ 6. **\`slock channel leave\`** \u2014 Leave a regular channel you have joined. This only affects your own agent membership.
1311
+ 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.
1312
+ 8. **\`slock message read\`** \u2014 Read past messages from a channel, DM, or thread. Supports \`before\` / \`after\` anchors and \`around\` for centered context.
1313
+ 9. **\`slock message search\`** \u2014 Search messages visible to you, then inspect a hit with \`slock message read\`.
1314
+ 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.
1315
+ 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.
1316
+ 12. **\`slock task list\`** \u2014 View a channel's task board.
1317
+ 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).
1318
+ 14. **\`slock task claim\`** \u2014 Claim tasks by number or message ID (supports batch, handles conflicts).
1319
+ 15. **\`slock task unclaim\`** \u2014 Release your claim on a task.
1320
+ 16. **\`slock task update\`** \u2014 Change a task's status (e.g. to in_review or done).
1321
+ 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\`.
1322
+ 18. **\`slock attachment view\`** \u2014 Download an attached file by its attachment ID so you can inspect it locally.
1323
+ 19. **\`slock profile show\`** \u2014 Show your own profile, or another visible profile via \`@handle\`. Mirrors the canonical Slock profile view.
1324
+ 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.
1325
+ 21. **\`slock integration list\`** \u2014 List built-in Slock apps, registered third-party services, and this agent's active Slock Agent Logins.
1326
+ 22. **\`slock integration login\`** \u2014 Provision or reuse this agent's login for a built-in Slock app or registered third-party service.
1327
+ 23. **\`slock integration env\`** \u2014 Print per-agent local CLI environment for a manifest-backed service that requires isolated HOME/XDG state.
1328
+ 24. **\`slock reminder schedule\`** \u2014 Schedule a reminder for yourself later, at a specific time, or on a recurring cadence.
1329
+ 25. **\`slock reminder list\`** \u2014 List your reminders, including lifecycle history for each reminder.
1330
+ 26. **\`slock reminder snooze\`** \u2014 Push a reminder later without replacing it.
1331
+ 27. **\`slock reminder update\`** \u2014 Change a reminder's title, schedule, or recurrence without creating a new reminder.
1332
+ 28. **\`slock reminder cancel\`** \u2014 Cancel one of your reminders by ID.
1333
+ 29. **\`slock reminder log\`** \u2014 Show the event log for a reminder, including fires, dismissals, and reschedules.
1334
+ 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\`).
1335
+
1336
+ 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:
1337
+ - \`Error:\` human-readable error summary
1338
+ - \`Code:\` stable machine-oriented error code
1339
+ - \`Next action:\` optional recovery hint
1340
+
1341
+ Error code prefixes tell you the layer:
1342
+ - \`MISSING_*\` / \`TOKEN_*\` = local auth bootstrap
1343
+ - \`*_FAILED\` = 4xx from server
1344
+ - \`SERVER_5XX\` = server unreachable / crashed`;
1345
+ }
1346
+ function buildCredentialHygieneSection() {
1347
+ return `### Credential hygiene
1348
+
1349
+ **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.
1350
+
1351
+ If a tool or error output contains credential-shaped strings, redact them to \`sk_agent_<redacted>\` / \`sk_machine_<redacted>\` shape before reposting.
1352
+
1353
+ **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.`;
1354
+ }
1355
+ function buildSendingMessagesSection() {
1356
+ const D = MESSAGE_HEREDOC_DELIMITER;
1357
+ return `### Sending messages
1358
+
1359
+ - **Reply to a channel**: \`slock message send --target "#channel-name" <<'${D}'\` followed by the message body and \`${D}\`
1360
+ - **Reply to a DM**: \`slock message send --target dm:@peer-name <<'${D}'\` followed by the message body and \`${D}\`
1361
+ - **Reply in a thread**: \`slock message send --target "#channel:shortid" <<'${D}'\` followed by the message body and \`${D}\`
1362
+ - **Start a NEW DM**: \`slock message send --target dm:@person-name <<'${D}'\` followed by the message body and \`${D}\`
1363
+
1364
+ Message content is always read from stdin. Use a heredoc so quotes, backticks, code blocks, and newlines are not interpreted by the shell:
1365
+ \`\`\`bash
1366
+ slock message send --target "#channel-name" <<'${D}'
1367
+ Long message with "quotes", $vars, \`backticks\`, and code blocks.
1368
+ ${D}
1369
+ \`\`\`
1370
+
1371
+ 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.
1372
+
1373
+ If Slock says a message was not sent and was saved as a draft, choose one path:
1374
+ - To update the draft, use a normal \`slock message send --target <target>\` with the revised content.
1375
+ - 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.
1376
+
1377
+ **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.`;
1378
+ }
1379
+ function buildRemindersSection() {
1380
+ return `### Reminders
1381
+
1382
+ 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.
1383
+ 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.
1384
+ 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.
1385
+ 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.`;
1386
+ }
1387
+ function buildThreadsSection() {
1388
+ const D = MESSAGE_HEREDOC_DELIMITER;
1389
+ return `### Threads
1390
+
1391
+ Threads are sub-conversations attached to a specific message. They let you discuss a topic without cluttering the main channel.
1392
+
1393
+ - **Thread targets** have a colon and short ID suffix: \`#general:00000000\` (thread in #general) or \`dm:@richard:11111111\` (thread in a DM).
1394
+ - 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.
1395
+ - **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.
1396
+ - When you send a message, the response includes the message ID. You can use it to start a thread on your own message.
1397
+ - You can read thread history: \`slock message read --channel "#general:00000000"\`
1398
+ - 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.
1399
+ - Threads cannot be nested \u2014 you cannot start a thread inside a thread.`;
1400
+ }
1401
+ function buildDiscoverySection() {
1402
+ return `### Discovering people and channels
1403
+
1404
+ Call \`slock server info\` to see all channels in this server, which ones you have joined, other agents, and humans.
1405
+ 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"\`.
1406
+ 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.`;
1407
+ }
1408
+ function buildChannelAwarenessSection() {
1409
+ return `### Channel awareness
1410
+
1411
+ Each channel has a **name** and optionally a **description** that define its purpose (visible via \`slock server info\`). Respect them:
1412
+ - **Reply in context** \u2014 always respond in the channel/thread the message came from.
1413
+ - **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.
1414
+ - If unsure where something belongs, call \`slock server info\` to review channel descriptions.`;
1415
+ }
1416
+ function buildReadingHistorySection() {
1417
+ return `### Reading history
1418
+
1419
+ \`slock message read --channel "#channel-name"\` or \`slock message read --channel dm:@peer-name\` or \`slock message read --channel "#channel:shortid"\`
1420
+
1421
+ To jump directly to a specific hit with nearby context, use \`slock message read --channel "..." --around "messageId"\` or \`slock message read --channel "..." --around 12345\`.`;
1422
+ }
1423
+ function buildHistoricalReferencesSection() {
1424
+ return `### Historical references
1425
+
1426
+ 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.`;
1427
+ }
1428
+ function buildTasksSection() {
1429
+ const D = MESSAGE_HEREDOC_DELIMITER;
1430
+ return `### Tasks
1431
+
1432
+ 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.
1433
+
1434
+ **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.
1435
+
1436
+ **What you see in messages:**
1437
+ - A message already marked as a task: \`@Alice: Fix the login bug [task #3 status=in_progress]\`
1438
+ - A regular message (no task suffix): \`@Alice: Can someone look into the login bug?\`
1439
+ - A system notification about task changes: \`\u{1F4CB} Alice converted a message to task #3 "Fix the login bug"\`
1440
+
1441
+ 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.
1442
+
1443
+ \`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.
1444
+
1445
+ **Status flow:** \`todo\` \u2192 \`in_progress\` \u2192 \`in_review\` \u2192 \`done\`
1446
+
1447
+ **Assignee** is independent from status \u2014 a task can be claimed or unclaimed at any status except \`done\`.
1448
+
1449
+ **Workflow:**
1450
+ 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)
1451
+ 2. If the claim fails, someone else is working on it \u2014 move on to another task
1452
+ 3. Post updates in the task's thread: \`slock message send --target "#channel:msgShortId" <<'${D}'\` followed by the message body and \`${D}\`
1453
+ 4. When done, set status to \`in_review\` so a human can validate via \`slock task update\`
1454
+ 5. After approval (e.g. "looks good", "merge it"), set status to \`done\`
1455
+
1456
+ **What \`slock task create\` really means:**
1457
+ - Tasks live in the same chat flow as messages. A task is just a message with task metadata, not a separate source of truth.
1458
+ - \`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.
1459
+ - \`slock task create\` only creates the task \u2014 to own it, call \`slock task claim\` afterward.
1460
+ - 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.
1461
+ - If someone already sent the work item as a message, just claim that existing message/task instead of creating a new one.
1462
+ - If the work already exists as a message, reuse it via \`slock task claim --message-id ...\`.
1463
+
1464
+ **Creating new tasks:**
1465
+ - 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.
1466
+ - If a message already shows a \`[task #N ...]\` suffix, claim \`#N\` if it is yours to take; otherwise move on.
1467
+ - Before calling \`slock task create\`, first check whether the work already exists on the task board or is already being handled.
1468
+ - Reuse existing tasks and threads instead of creating duplicates.
1469
+ - Use \`slock task create\` only for genuinely new subtasks or follow-up work that does not already have a canonical task.`;
1470
+ }
1471
+ function buildSplittingTasksSection() {
1472
+ return `### Splitting tasks for parallel execution
1473
+
1474
+ When you need to break down a large task into subtasks, structure them so agents can work **in parallel**:
1475
+ - **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.
1476
+ - **Prefer independent subtasks** that don't block each other. Each subtask should be completable without waiting for another.
1477
+ - **Avoid creating sequential chains** where each task depends on the previous one \u2014 this forces agents to work one at a time, wasting capacity.
1478
+
1479
+ When you receive a notification about new tasks, check the task board and claim tasks relevant to your skills.`;
1480
+ }
1481
+ function buildMentionsSection(identity) {
1482
+ return `## @Mentions
1483
+
1484
+ In channel group chats, you can @mention people by their unique name (e.g. @alice or @bob).
1485
+ - Your stable Slock @mention handle is \`@${identity.handle}\`.
1486
+ - Your display name is \`${identity.displayName || identity.handle}\`. Treat it as presentation only \u2014 when reasoning about identity and @mentions, prefer your stable \`name\`.
1487
+ - Every human and agent has a unique \`name\` \u2014 this is their stable identifier for @mentions.
1488
+ - Mention others, not yourself \u2014 assign reviews and follow-ups to teammates.
1489
+ - @mentions only reach people inside the channel \u2014 channels are the isolation boundary.`;
1490
+ }
1491
+ function buildCommunicationStyleSection() {
1492
+ return `## Communication style
1493
+
1494
+ Keep the user informed. They cannot see your internal reasoning, so:
1495
+ - When you receive a task, acknowledge it and briefly outline your plan before starting.
1496
+ - For multi-step work, send short progress updates (e.g. "Working on step 2/3\u2026").
1497
+ - When done, summarize the result.
1498
+ - Keep updates concise \u2014 one or two sentences. Don't flood the chat.`;
1499
+ }
1500
+ function buildConversationEtiquetteSection(taskClaimCmd = "`slock task claim`") {
1501
+ return `### Conversation etiquette
1502
+
1503
+ - **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.
1504
+ - **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.
1505
+ - **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.
1506
+ - **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.
1507
+ - **Skip idle narration.** Only send messages when you have actionable content \u2014 avoid broadcasting that you are waiting or idle.`;
1508
+ }
1509
+ function buildFormattingMentionsChannelsSection() {
1510
+ return `### Formatting \u2014 Mentions & Channel Refs
1511
+
1512
+ Slock auto-renders these inline tokens as interactive links whenever they appear as bare text in your message:
1513
+
1514
+ - @alice \u2014 links to a user
1515
+ - #general or #1 \u2014 links to a channel
1516
+ - #engineering:b885b5ae \u2014 links to a specific thread (channel name + msg ID suffix)
1517
+ - task #123 \u2014 links to a task (always write "task #N", not bare "#N" which is ambiguous with PRs/issues)
1518
+
1519
+ 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.
1520
+
1521
+ 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.`;
1522
+ }
1523
+ function buildFormattingUrlsSection() {
1524
+ return `### Formatting \u2014 URLs in non-English text
1525
+
1526
+ 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.
1527
+
1528
+ - **Wrong**: \`\u6D4B\u8BD5\u73AF\u5883\uFF1Ahttp://localhost:3000\uFF0C\u8BF7\u67E5\u770B\` (the \`\uFF0C\` gets swallowed into the link)
1529
+ - **Correct**: \`\u6D4B\u8BD5\u73AF\u5883\uFF1A<http://localhost:3000>\uFF0C\u8BF7\u67E5\u770B\`
1530
+ - **Also correct**: \`\u6D4B\u8BD5\u73AF\u5883\uFF1A[http://localhost:3000](http://localhost:3000)\uFF0C\u8BF7\u67E5\u770B\``;
1531
+ }
1532
+ function buildWorkspaceAndMemorySection() {
1533
+ return `## Workspace & Memory
1534
+
1535
+ 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.
1536
+
1537
+ ### MEMORY.md \u2014 Your Memory Index (CRITICAL)
1538
+
1539
+ \`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.
1540
+
1541
+ \`\`\`markdown
1542
+ # <Your Name>
1543
+
1544
+ ## Role
1545
+ <your role definition, evolved over time>
1546
+
1547
+ ## Key Knowledge
1548
+ - Read notes/user-preferences.md for user preferences and conventions
1549
+ - Read notes/channels.md for what each channel is about and ongoing work
1550
+ - Read notes/domain.md for domain-specific knowledge and conventions
1551
+ - ...
1552
+
1553
+ ## Active Context
1554
+ - Currently working on: <brief summary>
1555
+ - Last interaction: <brief summary>
1556
+ \`\`\`
1557
+
1558
+ ### What to memorize
1559
+
1560
+ **Actively observe and record** the following kinds of knowledge as you encounter them in conversations:
1561
+
1562
+ 1. **User preferences** \u2014 How the user likes things done, communication style, coding conventions, tool preferences, recurring patterns in their requests.
1563
+ 2. **World/project context** \u2014 The project structure, tech stack, architectural decisions, team conventions, deployment patterns.
1564
+ 3. **Domain knowledge** \u2014 Domain-specific terminology, conventions, best practices you learn through tasks.
1565
+ 4. **Work history** \u2014 What has been done, decisions made and why, problems solved, approaches that worked or failed.
1566
+ 5. **Channel context** \u2014 What each channel is about, who participates, what's being discussed, ongoing tasks per channel.
1567
+ 6. **Other agents** \u2014 What other agents do, their specialties, collaboration patterns, how to work with them effectively.
1568
+
1569
+ ### How to organize memory
1570
+
1571
+ - **MEMORY.md** is always the index. Keep it concise but comprehensive as a table of contents.
1572
+ - Create a \`notes/\` directory for detailed knowledge files. Use descriptive names:
1573
+ - \`notes/user-preferences.md\` \u2014 User's preferences and conventions
1574
+ - \`notes/channels.md\` \u2014 Summary of each channel and its purpose
1575
+ - \`notes/work-log.md\` \u2014 Important decisions and completed work
1576
+ - \`notes/<domain>.md\` \u2014 Domain-specific knowledge
1577
+ - You can also create any other files or directories for your work (scripts, notes, data, etc.)
1578
+ - **Update notes proactively** \u2014 Don't wait to be asked. When you learn something important, write it down.
1579
+ - **Keep MEMORY.md current** \u2014 After updating notes, update the index in MEMORY.md if new files were added.`;
1580
+ }
1581
+ function buildCompactionSafetySection() {
1582
+ return `### Compaction safety (CRITICAL)
1583
+
1584
+ 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:
1585
+
1586
+ - **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.
1587
+ - **Before a long task**, write a brief "Active Context" note in MEMORY.md so you can resume if interrupted mid-task.
1588
+ - **After completing work**, update your notes and MEMORY.md index so nothing is lost.
1589
+ - 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.`;
1590
+ }
1591
+ function buildSlockCliGuideSections(options) {
1592
+ return {
1593
+ communication: buildCommunicationSection(options.audience),
1594
+ credentialHygiene: buildCredentialHygieneSection(),
1595
+ sendingMessages: buildSendingMessagesSection(),
1596
+ reminders: buildRemindersSection(),
1597
+ threads: buildThreadsSection(),
1598
+ discovery: buildDiscoverySection(),
1599
+ channelAwareness: buildChannelAwarenessSection(),
1600
+ readingHistory: buildReadingHistorySection(),
1601
+ historicalReferences: buildHistoricalReferencesSection(),
1602
+ tasks: buildTasksSection(),
1603
+ splittingTasks: buildSplittingTasksSection(),
1604
+ mentions: buildMentionsSection(options.identity),
1605
+ communicationStyle: buildCommunicationStyleSection(),
1606
+ conversationEtiquette: buildConversationEtiquetteSection(options.taskClaimCmd),
1607
+ formattingMentionsChannels: buildFormattingMentionsChannelsSection(),
1608
+ formattingUrls: buildFormattingUrlsSection(),
1609
+ workspaceAndMemory: buildWorkspaceAndMemorySection(),
1610
+ compactionSafety: buildCompactionSafetySection()
1611
+ };
1612
+ }
1613
+
1279
1614
  // src/drivers/systemPrompt.ts
1280
1615
  function toolRef(prefix, name) {
1281
1616
  return `${prefix}${name}`;
@@ -1304,11 +1639,22 @@ function runtimeContextLines(config) {
1304
1639
  function buildPrompt(config, variant, opts) {
1305
1640
  const isCli = variant === "cli";
1306
1641
  const t = (name) => toolRef(opts.toolPrefix, name);
1642
+ const taskClaimCmd = isCli ? "`slock task claim`" : `\`${t("claim_tasks")}\``;
1643
+ const cliGuideSections = buildSlockCliGuideSections({
1644
+ // The daemon prompt audience is always managed-runner regardless of
1645
+ // CLI vs MCP variant — both are daemon-spawned. The self-hosted-runner
1646
+ // audience is only used by the manual topic generator script.
1647
+ audience: "managed-runner",
1648
+ identity: {
1649
+ handle: config.name,
1650
+ displayName: config.displayName || config.name
1651
+ },
1652
+ taskClaimCmd
1653
+ });
1307
1654
  const sendCmd = isCli ? "`slock message send`" : `\`${t("send_message")}\``;
1308
1655
  const readCmd = isCli ? "`slock message read`" : `\`${t("read_history")}\``;
1309
1656
  const checkCmd = isCli ? "`slock message check`" : `\`${t("check_messages")}\``;
1310
1657
  const inboxCheckCmd = isCli ? "`slock inbox check`" : checkCmd;
1311
- const taskClaimCmd = isCli ? "`slock task claim`" : `\`${t("claim_tasks")}\``;
1312
1658
  const taskCreateCmd = isCli ? "`slock task create`" : `\`${t("create_tasks")}\``;
1313
1659
  const taskUpdateCmd = isCli ? "`slock task update`" : `\`${t("update_task_status")}\``;
1314
1660
  const serverInfoCmd = isCli ? "`slock server info`" : `\`${t("list_server")}\``;
@@ -1347,48 +1693,7 @@ function buildPrompt(config, variant, opts) {
1347
1693
  `4. When you receive a message, process it and reply with ${sendCmd}.`,
1348
1694
  "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
1695
  ];
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
1696
+ const communicationSection = isCli ? cliGuideSections.communication : `## Communication \u2014 MCP tools ONLY
1392
1697
 
1393
1698
  You have MCP tools from the "chat" server. Use ONLY these for communication:
1394
1699
 
@@ -1409,34 +1714,18 @@ You have MCP tools from the "chat" server. Use ONLY these for communication:
1409
1714
  15. **${scheduleReminderCmd}** \u2014 Schedule a reminder for yourself later, at a specific time, or on a recurring cadence.
1410
1715
  16. **${listRemindersCmd}** \u2014 List your reminders.
1411
1716
  17. **${cancelReminderCmd}** \u2014 Cancel one of your reminders by ID.`;
1412
- const reminderSection = `### Reminders
1717
+ const credentialHygieneSection = isCli ? cliGuideSections.credentialHygiene : `### Credential hygiene
1718
+
1719
+ **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.
1720
+
1721
+ If a tool or error output contains credential-shaped strings, redact them to \`sk_agent_<redacted>\` / \`sk_machine_<redacted>\` shape before reposting.`;
1722
+ const reminderSection = isCli ? cliGuideSections.reminders : `### Reminders
1413
1723
 
1414
1724
  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
1725
  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
1726
  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
1727
  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
1728
+ const sendingMessagesSection = isCli ? cliGuideSections.sendingMessages : `### Sending messages
1440
1729
 
1441
1730
  - **Reply to a channel**: \`${t("send_message")}(target="#channel-name", content="...")\`
1442
1731
  - **Reply to a DM**: \`${t("send_message")}(target="dm:@peer-name", content="...")\`
@@ -1444,17 +1733,7 @@ If Slock says a message was not sent and was saved as a draft, choose one path:
1444
1733
  - **Start a NEW DM**: \`${t("send_message")}(target="dm:@person-name", content="...")\`
1445
1734
 
1446
1735
  **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
1736
+ const threadsSection = isCli ? cliGuideSections.threads : `### Threads
1458
1737
 
1459
1738
  Threads are sub-conversations attached to a specific message. They let you discuss a topic without cluttering the main channel.
1460
1739
 
@@ -1464,21 +1743,12 @@ Threads are sub-conversations attached to a specific message. They let you discu
1464
1743
  - When you send a message, the response includes the message ID. You can use it to start a thread on your own message.
1465
1744
  - You can read thread history via ${readCmd} with the same thread target.
1466
1745
  - 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
1746
+ const discoverySection = isCli ? cliGuideSections.discovery : `### Discovering people and channels
1472
1747
 
1473
1748
  Call ${serverInfoCmd} to see all channels in this server, which ones you have joined, other agents, and humans.
1474
1749
  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
1750
  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
1751
+ const channelAwarenessSection = isCli ? cliGuideSections.channelAwareness : `### Channel awareness
1482
1752
 
1483
1753
  Each channel has a **name** and optionally a **description** that define its purpose (visible via ${serverInfoCmd}). Respect them:
1484
1754
  - **Reply in context** \u2014 always respond in the channel/thread the message came from.
@@ -1489,62 +1759,15 @@ Each channel has a **name** and optionally a **description** that define its pur
1489
1759
  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
1760
 
1491
1761
  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
1762
+ const readingHistorySection = isCli ? cliGuideSections.readingHistory : `### Reading history
1499
1763
 
1500
1764
  Use ${readCmd} with the \`channel\` parameter set to \`"#channel-name"\`, \`"dm:@peer-name"\`, or a thread target like \`"#channel:shortid"\`.
1501
1765
 
1502
1766
  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
1767
+ const historicalReferenceSection = isCli ? cliGuideSections.historicalReferences : `### Historical references
1506
1768
 
1507
1769
  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
1770
+ const tasksSection = isCli ? cliGuideSections.tasks : `### Tasks
1548
1771
 
1549
1772
  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
1773
 
@@ -1584,7 +1807,6 @@ ${readCmd} shows messages in their current state. If a message was later convert
1584
1807
  - Before calling ${taskCreateCmd}, first check whether the work already exists on the task board or is already being handled.
1585
1808
  - Reuse existing tasks and threads instead of creating duplicates.
1586
1809
  - 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
1810
  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
1811
 
1590
1812
  ## Who you are
@@ -1595,6 +1817,8 @@ ${runtimeContextLines(config).join("\n")}
1595
1817
 
1596
1818
  ${communicationSection}
1597
1819
 
1820
+ ${credentialHygieneSection}
1821
+
1598
1822
  CRITICAL RULES:
1599
1823
  ${criticalRules.join("\n")}
1600
1824
 
@@ -1666,117 +1890,21 @@ ${historicalReferenceSection}
1666
1890
 
1667
1891
  ${tasksSection}
1668
1892
 
1669
- ### Splitting tasks for parallel execution
1893
+ ${cliGuideSections.splittingTasks}
1670
1894
 
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.
1895
+ ${cliGuideSections.mentions}
1675
1896
 
1676
- When you receive a notification about new tasks, check the task board and claim tasks relevant to your skills.
1897
+ ${cliGuideSections.communicationStyle}
1677
1898
 
1678
- ## @Mentions
1899
+ ${cliGuideSections.conversationEtiquette}
1679
1900
 
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.
1901
+ ${cliGuideSections.formattingMentionsChannels}
1686
1902
 
1687
- ## Communication style
1903
+ ${cliGuideSections.formattingUrls}
1688
1904
 
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.
1905
+ ${cliGuideSections.workspaceAndMemory}
1713
1906
 
1714
- Markdown markup expresses presentation semantics; do not mix markup delimiters into literal payloads. Code spans are literal, so if text should render as a link or ref, do not wrap that link/ref markup in backticks.
1715
-
1716
- ### Formatting \u2014 URLs in non-English text
1717
-
1718
- When writing a URL next to non-ASCII punctuation (Chinese, Japanese, etc.) and the URL should render as a link, wrap the URL in angle brackets or use markdown link syntax. Otherwise the punctuation may be rendered as part of the URL.
1719
-
1720
- - **Wrong**: \`\u6D4B\u8BD5\u73AF\u5883\uFF1Ahttp://localhost:3000\uFF0C\u8BF7\u67E5\u770B\` (the \`\uFF0C\` gets swallowed into the link)
1721
- - **Correct**: \`\u6D4B\u8BD5\u73AF\u5883\uFF1A<http://localhost:3000>\uFF0C\u8BF7\u67E5\u770B\`
1722
- - **Also correct**: \`\u6D4B\u8BD5\u73AF\u5883\uFF1A[http://localhost:3000](http://localhost:3000)\uFF0C\u8BF7\u67E5\u770B\`
1723
-
1724
- ## Workspace & Memory
1725
-
1726
- Your working directory (cwd) is your **persistent, agent-owned workspace**; files you create here survive across sessions. Use it for memory, notes, artifacts, code checkouts, and task-specific files, but treat it as a flexible workspace rather than a fixed schema. Keep **MEMORY.md** easy to scan as the recovery entry point; if you add important long-lived organization, update **MEMORY.md** or a note index so future sessions can find it. When working in a repository, first choose the specific project directory or worktree inside the workspace, then run git or package-manager commands there.
1727
-
1728
- ### MEMORY.md \u2014 Your Memory Index (CRITICAL)
1729
-
1730
- \`MEMORY.md\` is the **entry point** to all your knowledge. It is the first file read on every startup (including after context compression). Structure it as an index that points to everything you know. This file is called \`MEMORY.md\` (not tied to any specific runtime) \u2014 keep it updated after every significant interaction or learning.
1731
-
1732
- \`\`\`markdown
1733
- # <Your Name>
1734
-
1735
- ## Role
1736
- <your role definition, evolved over time>
1737
-
1738
- ## Key Knowledge
1739
- - Read notes/user-preferences.md for user preferences and conventions
1740
- - Read notes/channels.md for what each channel is about and ongoing work
1741
- - Read notes/domain.md for domain-specific knowledge and conventions
1742
- - ...
1743
-
1744
- ## Active Context
1745
- - Currently working on: <brief summary>
1746
- - Last interaction: <brief summary>
1747
- \`\`\`
1748
-
1749
- ### What to memorize
1750
-
1751
- **Actively observe and record** the following kinds of knowledge as you encounter them in conversations:
1752
-
1753
- 1. **User preferences** \u2014 How the user likes things done, communication style, coding conventions, tool preferences, recurring patterns in their requests.
1754
- 2. **World/project context** \u2014 The project structure, tech stack, architectural decisions, team conventions, deployment patterns.
1755
- 3. **Domain knowledge** \u2014 Domain-specific terminology, conventions, best practices you learn through tasks.
1756
- 4. **Work history** \u2014 What has been done, decisions made and why, problems solved, approaches that worked or failed.
1757
- 5. **Channel context** \u2014 What each channel is about, who participates, what's being discussed, ongoing tasks per channel.
1758
- 6. **Other agents** \u2014 What other agents do, their specialties, collaboration patterns, how to work with them effectively.
1759
-
1760
- ### How to organize memory
1761
-
1762
- - **MEMORY.md** is always the index. Keep it concise but comprehensive as a table of contents.
1763
- - Create a \`notes/\` directory for detailed knowledge files. Use descriptive names:
1764
- - \`notes/user-preferences.md\` \u2014 User's preferences and conventions
1765
- - \`notes/channels.md\` \u2014 Summary of each channel and its purpose
1766
- - \`notes/work-log.md\` \u2014 Important decisions and completed work
1767
- - \`notes/<domain>.md\` \u2014 Domain-specific knowledge
1768
- - You can also create any other files or directories for your work (scripts, notes, data, etc.)
1769
- - **Update notes proactively** \u2014 Don't wait to be asked. When you learn something important, write it down.
1770
- - **Keep MEMORY.md current** \u2014 After updating notes, update the index in MEMORY.md if new files were added.
1771
-
1772
- ### Compaction safety (CRITICAL)
1773
-
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:
1775
-
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.
1907
+ ${cliGuideSections.compactionSafety}
1780
1908
 
1781
1909
  ## Capabilities
1782
1910
 
@@ -1861,6 +1989,19 @@ function listLegacySlockStatePaths(slockHome = resolveSlockHome(), homeDir = os.
1861
1989
  return candidates.filter((candidate) => existsSync(candidate.path));
1862
1990
  }
1863
1991
 
1992
+ // src/authEnv.ts
1993
+ var DAEMON_API_KEY_ENV = "SLOCK_MACHINE_API_KEY";
1994
+ var SLOCK_AGENT_TOKEN_ENV = "SLOCK_AGENT_TOKEN";
1995
+ function scrubDaemonAuthEnv(env) {
1996
+ delete env[DAEMON_API_KEY_ENV];
1997
+ return env;
1998
+ }
1999
+ function scrubDaemonChildEnv(env) {
2000
+ delete env[DAEMON_API_KEY_ENV];
2001
+ delete env[SLOCK_AGENT_TOKEN_ENV];
2002
+ return env;
2003
+ }
2004
+
1864
2005
  // src/agentCredentialProxy.ts
1865
2006
  import { randomBytes } from "crypto";
1866
2007
  import http from "http";
@@ -3016,7 +3157,9 @@ var LOOPBACK_NO_PROXY = "127.0.0.1,localhost";
3016
3157
  var CLI_TRANSPORT_TRACE_DIR_ENV = "SLOCK_CLI_TRANSPORT_TRACE_DIR";
3017
3158
  var safePathPart = (value) => value.replace(/[^a-zA-Z0-9_.-]/g, "_");
3018
3159
  var RAW_CREDENTIAL_ENV_DENYLIST = [
3019
- "SLOCK_AGENT_CREDENTIAL_KEY"
3160
+ "SLOCK_AGENT_TOKEN",
3161
+ "SLOCK_AGENT_CREDENTIAL_KEY",
3162
+ "SLOCK_AGENT_CREDENTIAL_KEY_FILE"
3020
3163
  ];
3021
3164
  var cachedOpencliBinPath;
3022
3165
  function resolveOpencliBinPath() {
@@ -3231,7 +3374,7 @@ exec ${shellSingleQuote(process.execPath)} ${shellSingleQuote(opencliBinPath)} "
3231
3374
  ...agentCredentialProxy ? {} : { SLOCK_AGENT_TOKEN_FILE: tokenFile },
3232
3375
  PATH: `${slockDir}${path2.delimiter}${process.env.PATH ?? ""}`
3233
3376
  };
3234
- delete spawnEnv.SLOCK_AGENT_TOKEN;
3377
+ scrubDaemonChildEnv(spawnEnv);
3235
3378
  for (const key of RAW_CREDENTIAL_ENV_DENYLIST) {
3236
3379
  delete spawnEnv[key];
3237
3380
  }
@@ -3660,7 +3803,7 @@ function resolveCommandOnWindows(command, env, execFileSyncFn, existsSyncFn) {
3660
3803
  }
3661
3804
  function resolveCommandOnPath(command, deps = {}) {
3662
3805
  const platform = deps.platform ?? process.platform;
3663
- const env = withWindowsUserEnvironment(deps.env ?? process.env, deps);
3806
+ const env = scrubDaemonChildEnv({ ...withWindowsUserEnvironment(deps.env ?? process.env, deps) });
3664
3807
  const execFileSyncFn = deps.execFileSyncFn ?? execFileSync;
3665
3808
  const existsSyncFn = deps.existsSyncFn ?? existsSync2;
3666
3809
  if (platform === "win32") {
@@ -3686,7 +3829,7 @@ function firstExistingPath(candidates, deps = {}) {
3686
3829
  return null;
3687
3830
  }
3688
3831
  function readCommandVersion(command, args = [], deps = {}) {
3689
- const env = withWindowsUserEnvironment(deps.env ?? process.env, deps);
3832
+ const env = scrubDaemonChildEnv({ ...withWindowsUserEnvironment(deps.env ?? process.env, deps) });
3690
3833
  const execFileSyncFn = deps.execFileSyncFn ?? execFileSync;
3691
3834
  try {
3692
3835
  const output = normalizeExecOutput(execFileSyncFn(command, [...args, "--version"], {
@@ -5149,11 +5292,11 @@ function detectCursorModels(runCommand = runCursorModelsCommand) {
5149
5292
  return parseCursorModelsOutput(String(result.stdout || ""));
5150
5293
  }
5151
5294
  function buildCursorModelProbeEnv(deps = {}) {
5152
- return withWindowsUserEnvironment({
5295
+ return scrubDaemonChildEnv(withWindowsUserEnvironment({
5153
5296
  ...deps.env ?? process.env,
5154
5297
  FORCE_COLOR: "0",
5155
5298
  NO_COLOR: "1"
5156
- }, deps);
5299
+ }, deps));
5157
5300
  }
5158
5301
  function runCursorModelsCommand() {
5159
5302
  return spawnSync("cursor-agent", ["models"], {
@@ -5209,7 +5352,7 @@ function resolveGeminiSpawn(commandArgs, deps = {}) {
5209
5352
  }
5210
5353
  const execFileSyncFn = deps.execFileSyncFn ?? execFileSync3;
5211
5354
  const existsSyncFn = deps.existsSyncFn ?? existsSync5;
5212
- const env = deps.env ?? process.env;
5355
+ const env = scrubDaemonChildEnv({ ...deps.env ?? process.env });
5213
5356
  const winPath = path8.win32;
5214
5357
  let geminiEntry = null;
5215
5358
  try {
@@ -5381,13 +5524,16 @@ var GeminiDriver = class {
5381
5524
  // src/drivers/kimi.ts
5382
5525
  import { randomUUID as randomUUID2 } from "crypto";
5383
5526
  import { spawn as spawn7 } from "child_process";
5384
- import { existsSync as existsSync6, readFileSync as readFileSync3, writeFileSync as writeFileSync6 } from "fs";
5527
+ import { chmodSync, existsSync as existsSync6, readFileSync as readFileSync3, writeFileSync as writeFileSync6 } from "fs";
5385
5528
  import os3 from "os";
5386
5529
  import path9 from "path";
5387
5530
  var KIMI_WIRE_PROTOCOL_VERSION = "1.3";
5388
5531
  var KIMI_SYSTEM_PROMPT_FILE = ".slock-kimi-system.md";
5389
5532
  var KIMI_AGENT_FILE = ".slock-kimi-agent.yaml";
5390
5533
  var KIMI_MCP_FILE = ".slock-kimi-mcp.json";
5534
+ var KIMI_GENERATED_CONFIG_FILE = ".slock-kimi-config.toml";
5535
+ var SLOCK_KIMI_CONFIG_CONTENT_ENV = "SLOCK_KIMI_CONFIG_CONTENT";
5536
+ var SLOCK_KIMI_CONFIG_FILE_ENV = "SLOCK_KIMI_CONFIG_FILE";
5391
5537
  function parseToolArguments(raw) {
5392
5538
  if (typeof raw !== "string") return raw;
5393
5539
  try {
@@ -5396,6 +5542,73 @@ function parseToolArguments(raw) {
5396
5542
  return raw;
5397
5543
  }
5398
5544
  }
5545
+ function readKimiConfigSource(home = os3.homedir(), env = process.env) {
5546
+ const inlineConfig = env[SLOCK_KIMI_CONFIG_CONTENT_ENV];
5547
+ if (inlineConfig && inlineConfig.trim()) {
5548
+ return {
5549
+ raw: inlineConfig,
5550
+ explicitPath: null,
5551
+ sourcePath: SLOCK_KIMI_CONFIG_CONTENT_ENV
5552
+ };
5553
+ }
5554
+ const explicitPath = env[SLOCK_KIMI_CONFIG_FILE_ENV];
5555
+ const configPath = explicitPath && explicitPath.trim() ? explicitPath : path9.join(home, ".kimi", "config.toml");
5556
+ try {
5557
+ return {
5558
+ raw: readFileSync3(configPath, "utf8"),
5559
+ explicitPath: explicitPath && explicitPath.trim() ? explicitPath : null,
5560
+ sourcePath: configPath
5561
+ };
5562
+ } catch {
5563
+ return {
5564
+ raw: null,
5565
+ explicitPath: explicitPath && explicitPath.trim() ? explicitPath : null,
5566
+ sourcePath: configPath
5567
+ };
5568
+ }
5569
+ }
5570
+ function buildKimiSpawnEnv(env = process.env) {
5571
+ const spawnEnv = { ...env, FORCE_COLOR: "0", NO_COLOR: "1" };
5572
+ delete spawnEnv[SLOCK_KIMI_CONFIG_CONTENT_ENV];
5573
+ delete spawnEnv[SLOCK_KIMI_CONFIG_FILE_ENV];
5574
+ return scrubDaemonChildEnv(spawnEnv);
5575
+ }
5576
+ function buildKimiEffectiveEnv(ctx, overrideEnv) {
5577
+ return {
5578
+ ...process.env,
5579
+ ...ctx.config.envVars || {},
5580
+ ...overrideEnv || {}
5581
+ };
5582
+ }
5583
+ function buildKimiLaunchOptions(ctx, opts = {}) {
5584
+ const env = buildKimiEffectiveEnv(ctx, opts.env);
5585
+ const source = readKimiConfigSource(opts.home ?? os3.homedir(), env);
5586
+ const args = [];
5587
+ let configFilePath = null;
5588
+ let configContent = null;
5589
+ if (source.explicitPath) {
5590
+ configFilePath = source.explicitPath;
5591
+ } else if (source.raw !== null && source.sourcePath === SLOCK_KIMI_CONFIG_CONTENT_ENV) {
5592
+ configFilePath = path9.join(ctx.workingDirectory, KIMI_GENERATED_CONFIG_FILE);
5593
+ configContent = source.raw;
5594
+ if (opts.writeGeneratedConfig !== false) {
5595
+ writeFileSync6(configFilePath, source.raw, { encoding: "utf8", mode: 384 });
5596
+ chmodSync(configFilePath, 384);
5597
+ }
5598
+ }
5599
+ if (configFilePath) {
5600
+ args.push("--config-file", configFilePath);
5601
+ }
5602
+ if (ctx.config.model && ctx.config.model !== "default") {
5603
+ args.push("--model", ctx.config.model);
5604
+ }
5605
+ return {
5606
+ args,
5607
+ env: buildKimiSpawnEnv(env),
5608
+ configFilePath,
5609
+ configContent
5610
+ };
5611
+ }
5399
5612
  function resolveKimiSpawn(commandArgs, deps = {}) {
5400
5613
  return {
5401
5614
  command: resolveCommandOnPath("kimi", deps) ?? "kimi",
@@ -5419,7 +5632,25 @@ var KimiDriver = class {
5419
5632
  };
5420
5633
  model = {
5421
5634
  detectedModelsVerifiedAs: "launchable",
5422
- toLaunchSpec: (modelId) => ({ args: ["--model", modelId] })
5635
+ toLaunchSpec: (modelId, ctx, opts) => {
5636
+ if (!ctx) return { args: ["--model", modelId] };
5637
+ const launchCtx = {
5638
+ ...ctx,
5639
+ config: {
5640
+ ...ctx.config,
5641
+ model: modelId
5642
+ }
5643
+ };
5644
+ const launch = buildKimiLaunchOptions(launchCtx, {
5645
+ home: opts?.home,
5646
+ writeGeneratedConfig: false
5647
+ });
5648
+ return {
5649
+ args: launch.args,
5650
+ env: launch.env,
5651
+ configFiles: launch.configFilePath ? [launch.configFilePath] : void 0
5652
+ };
5653
+ }
5423
5654
  };
5424
5655
  supportsStdinNotification = true;
5425
5656
  mcpToolPrefix = "";
@@ -5473,6 +5704,7 @@ var KimiDriver = class {
5473
5704
  }
5474
5705
  }
5475
5706
  }), "utf8");
5707
+ const launch = buildKimiLaunchOptions(ctx);
5476
5708
  const args = [
5477
5709
  "--wire",
5478
5710
  "--yolo",
@@ -5481,15 +5713,16 @@ var KimiDriver = class {
5481
5713
  "--mcp-config-file",
5482
5714
  mcpConfigPath,
5483
5715
  "--session",
5484
- this.sessionId
5716
+ this.sessionId,
5717
+ ...launch.args
5485
5718
  ];
5486
5719
  const launchRuntimeFields = runtimeConfigToLaunchFields(ctx.config);
5487
5720
  if (launchRuntimeFields.model && launchRuntimeFields.model !== "default") {
5488
5721
  args.push("--model", launchRuntimeFields.model);
5489
5722
  }
5490
5723
  const spawnEnv = (await prepareCliTransport(ctx, { NO_COLOR: "1" })).spawnEnv;
5491
- const launch = resolveKimiSpawn(args);
5492
- const proc = spawn7(launch.command, launch.args, {
5724
+ const spawnTarget = resolveKimiSpawn(args);
5725
+ const proc = spawn7(spawnTarget.command, spawnTarget.args, {
5493
5726
  cwd: ctx.workingDirectory,
5494
5727
  stdio: ["pipe", "pipe", "pipe"],
5495
5728
  env: spawnEnv,
@@ -5497,7 +5730,7 @@ var KimiDriver = class {
5497
5730
  // and has an 8191-character command-line limit. Kimi's official
5498
5731
  // installer/uv entrypoint is an executable, so launch it directly and
5499
5732
  // keep prompts on stdin / files instead of routing through cmd.exe.
5500
- shell: launch.shell
5733
+ shell: spawnTarget.shell
5501
5734
  });
5502
5735
  proc.stdin?.write(JSON.stringify({
5503
5736
  jsonrpc: "2.0",
@@ -5611,14 +5844,9 @@ var KimiDriver = class {
5611
5844
  return detectKimiModels();
5612
5845
  }
5613
5846
  };
5614
- function detectKimiModels(home = os3.homedir()) {
5615
- const configPath = path9.join(home, ".kimi", "config.toml");
5616
- let raw;
5617
- try {
5618
- raw = readFileSync3(configPath, "utf8");
5619
- } catch {
5620
- return null;
5621
- }
5847
+ function detectKimiModels(home = os3.homedir(), opts = {}) {
5848
+ const raw = readKimiConfigSource(home, opts.env).raw;
5849
+ if (raw === null) return null;
5622
5850
  const models = [];
5623
5851
  const sectionRe = /^\s*\[models(?:\.([^\]]+)|"\.[^"]+"|\."[^"]+")\s*\]\s*$/gm;
5624
5852
  const lineRe = /^\s*\[models\.(.+?)\s*\]\s*$/gm;
@@ -5882,7 +6110,7 @@ function runOpenCodeModelsCommand(home, deps = {}) {
5882
6110
  const platform = deps.platform ?? process.platform;
5883
6111
  const spawnSyncFn = deps.spawnSyncFn ?? spawnSync2;
5884
6112
  const result = spawnSyncFn("opencode", ["models"], {
5885
- env: { ...process.env, HOME: home, FORCE_COLOR: "0", NO_COLOR: "1" },
6113
+ env: scrubDaemonChildEnv({ ...process.env, HOME: home, FORCE_COLOR: "0", NO_COLOR: "1" }),
5886
6114
  encoding: "utf8",
5887
6115
  timeout: 5e3,
5888
6116
  shell: platform === "win32"
@@ -6145,7 +6373,6 @@ var OpenCodeDriver = class {
6145
6373
  import { randomUUID as randomUUID3 } from "crypto";
6146
6374
  import { EventEmitter } from "events";
6147
6375
  import { mkdirSync as mkdirSync4, readdirSync } from "fs";
6148
- import { PassThrough, Writable } from "stream";
6149
6376
  import path11 from "path";
6150
6377
  import {
6151
6378
  AuthStorage,
@@ -6164,6 +6391,13 @@ var PI_PROVIDER_LABELS = {
6164
6391
  openai: "OpenAI",
6165
6392
  openrouter: "OpenRouter"
6166
6393
  };
6394
+ function createPiSdkEventMappingState(sessionId = null) {
6395
+ return {
6396
+ sessionId,
6397
+ sessionAnnounced: false,
6398
+ sawTextDelta: false
6399
+ };
6400
+ }
6167
6401
  function buildPiSessionDir(workingDirectory) {
6168
6402
  return path11.join(workingDirectory, PI_SESSION_DIR);
6169
6403
  }
@@ -6189,12 +6423,6 @@ function findPiSessionFile(sessionDir, sessionId) {
6189
6423
  const match = entries.find((entry) => entry.endsWith(suffix));
6190
6424
  return match ? path11.join(sessionDir, match) : null;
6191
6425
  }
6192
- function piSdkEventToJsonLine(event) {
6193
- if (event.type === "agent_end") {
6194
- return JSON.stringify({ type: "agent_end" });
6195
- }
6196
- return JSON.stringify(event);
6197
- }
6198
6426
  function detectPiModelsFromRegistry(modelRegistry) {
6199
6427
  const models = [];
6200
6428
  const seen = /* @__PURE__ */ new Set();
@@ -6244,114 +6472,309 @@ function piErrorMessage(error) {
6244
6472
  }
6245
6473
  return "Unknown Pi error";
6246
6474
  }
6247
- var PiSdkProcess = class extends EventEmitter {
6248
- constructor(session) {
6249
- super();
6250
- this.session = session;
6251
- this.stdin = new Writable({
6252
- write: (chunk, _encoding, callback) => {
6253
- this.handleInput(String(chunk)).then(
6254
- () => callback(),
6255
- (error) => {
6256
- this.writeError(error);
6257
- callback();
6258
- }
6259
- );
6475
+ function pushSessionInitIfNeeded(state, events) {
6476
+ if (!state.sessionAnnounced && state.sessionId) {
6477
+ events.push({ kind: "session_init", sessionId: state.sessionId });
6478
+ state.sessionAnnounced = true;
6479
+ }
6480
+ }
6481
+ function mapPiAssistantMessageEvent(assistantEvent, state) {
6482
+ switch (assistantEvent.type) {
6483
+ case "thinking_delta":
6484
+ return typeof assistantEvent.delta === "string" && assistantEvent.delta.length > 0 ? [{ kind: "thinking", text: assistantEvent.delta }] : [];
6485
+ case "text_delta":
6486
+ if (typeof assistantEvent.delta === "string" && assistantEvent.delta.length > 0) {
6487
+ state.sawTextDelta = true;
6488
+ return [{ kind: "text", text: assistantEvent.delta }];
6260
6489
  }
6261
- });
6262
- this.session.subscribe((event) => {
6263
- this.writeStdout(piSdkEventToJsonLine(event));
6264
- });
6490
+ return [];
6491
+ case "text_end":
6492
+ return !state.sawTextDelta && typeof assistantEvent.content === "string" && assistantEvent.content.length > 0 ? [{ kind: "text", text: assistantEvent.content }] : [];
6493
+ case "error":
6494
+ return [{ kind: "error", message: piErrorMessage(assistantEvent.error.errorMessage || assistantEvent.error) }];
6495
+ case "thinking_start":
6496
+ case "thinking_end":
6497
+ case "text_start":
6498
+ case "toolcall_start":
6499
+ case "toolcall_delta":
6500
+ case "toolcall_end":
6501
+ case "start":
6502
+ case "done":
6503
+ return [];
6504
+ default: {
6505
+ const _exhaustive = assistantEvent;
6506
+ return _exhaustive;
6507
+ }
6508
+ }
6509
+ }
6510
+ function mapPiSdkEventToParsedEvents(event, state) {
6511
+ const events = [];
6512
+ pushSessionInitIfNeeded(state, events);
6513
+ switch (event.type) {
6514
+ case "agent_start":
6515
+ case "turn_start":
6516
+ case "turn_end":
6517
+ case "message_end":
6518
+ case "tool_execution_update":
6519
+ case "queue_update":
6520
+ case "session_info_changed":
6521
+ case "thinking_level_changed":
6522
+ case "auto_retry_start":
6523
+ case "auto_retry_end":
6524
+ return events;
6525
+ case "message_start":
6526
+ if (event.message.role === "assistant") {
6527
+ state.sawTextDelta = false;
6528
+ }
6529
+ return events;
6530
+ case "message_update":
6531
+ events.push(...mapPiAssistantMessageEvent(event.assistantMessageEvent, state));
6532
+ return events;
6533
+ case "tool_execution_start":
6534
+ events.push({
6535
+ kind: "tool_call",
6536
+ name: event.toolName || "unknown_tool",
6537
+ input: event.args ?? {}
6538
+ });
6539
+ return events;
6540
+ case "tool_execution_end":
6541
+ events.push({ kind: "tool_output", name: event.toolName || "unknown_tool" });
6542
+ return events;
6543
+ case "compaction_start":
6544
+ events.push({ kind: "compaction_started" });
6545
+ return events;
6546
+ case "compaction_end":
6547
+ events.push({ kind: "compaction_finished" });
6548
+ return events;
6549
+ case "agent_end":
6550
+ events.push({ kind: "turn_end", sessionId: state.sessionId || void 0 });
6551
+ return events;
6552
+ default: {
6553
+ const _exhaustive = event;
6554
+ return _exhaustive;
6555
+ }
6556
+ }
6557
+ }
6558
+ var PI_RUNTIME_SESSION_DESCRIPTOR = {
6559
+ transport: "sdk",
6560
+ lifecycle: "sdk_session",
6561
+ input: {
6562
+ initial: "start",
6563
+ idle: "sdk_prompt",
6564
+ busy: "sdk_steer"
6565
+ },
6566
+ readiness: "sdk_ready",
6567
+ turnBoundary: "sdk_event",
6568
+ startPolicy: "immediate",
6569
+ inFlightWake: "steer",
6570
+ busyDelivery: "direct",
6571
+ postTurn: "keep_alive"
6572
+ };
6573
+ async function createPiAgentSessionForContext(ctx, sessionId) {
6574
+ const sessionDir = buildPiSessionDir(ctx.workingDirectory);
6575
+ mkdirSync4(sessionDir, { recursive: true });
6576
+ const spawnEnv = await buildPiSpawnEnv(ctx);
6577
+ const agentDir = spawnEnv.PI_CODING_AGENT_DIR || getAgentDir();
6578
+ const authStorage = AuthStorage.create(path11.join(agentDir, "auth.json"));
6579
+ const modelRegistry = ModelRegistry.create(authStorage, path11.join(agentDir, "models.json"));
6580
+ const launchRuntimeFields = runtimeConfigToLaunchFields(ctx.config);
6581
+ const model = resolvePiModelFromRegistry(launchRuntimeFields.model, modelRegistry);
6582
+ if (launchRuntimeFields.model && launchRuntimeFields.model !== "default" && !model) {
6583
+ throw new Error(`Pi model not found: ${launchRuntimeFields.model}`);
6584
+ }
6585
+ const settingsManager = SettingsManager.inMemory({ compaction: { enabled: false } });
6586
+ const resourceLoader = new DefaultResourceLoader({
6587
+ cwd: ctx.workingDirectory,
6588
+ agentDir,
6589
+ settingsManager,
6590
+ systemPromptOverride: () => ctx.standingPrompt
6591
+ });
6592
+ await resourceLoader.reload();
6593
+ const existingSessionFile = ctx.config.sessionId ? findPiSessionFile(sessionDir, ctx.config.sessionId) : null;
6594
+ const sessionManager = existingSessionFile ? SessionManager.open(existingSessionFile, sessionDir, ctx.workingDirectory) : SessionManager.create(ctx.workingDirectory, sessionDir, { id: sessionId });
6595
+ const { session } = await createAgentSession({
6596
+ cwd: ctx.workingDirectory,
6597
+ agentDir,
6598
+ model,
6599
+ thinkingLevel: launchRuntimeFields.reasoningEffort,
6600
+ authStorage,
6601
+ modelRegistry,
6602
+ resourceLoader,
6603
+ customTools: [
6604
+ createBashTool(ctx.workingDirectory, {
6605
+ spawnHook: (spawnContext) => ({
6606
+ ...spawnContext,
6607
+ env: {
6608
+ ...spawnContext.env,
6609
+ ...spawnEnv
6610
+ }
6611
+ })
6612
+ })
6613
+ ],
6614
+ sessionManager,
6615
+ settingsManager
6616
+ });
6617
+ return session;
6618
+ }
6619
+ var PiSdkRuntimeSession = class {
6620
+ constructor(ctx, setCurrentSessionId, sessionFactory = createPiAgentSessionForContext) {
6621
+ this.ctx = ctx;
6622
+ this.setCurrentSessionId = setCurrentSessionId;
6623
+ this.sessionFactory = sessionFactory;
6624
+ this.mappingState = createPiSdkEventMappingState(ctx.config.sessionId || null);
6625
+ }
6626
+ descriptor = PI_RUNTIME_SESSION_DESCRIPTOR;
6627
+ events = new EventEmitter();
6628
+ mappingState;
6629
+ session = null;
6630
+ unsubscribe = null;
6631
+ started = false;
6632
+ didClose = false;
6633
+ requestedStopReason;
6634
+ exitInfo = null;
6635
+ get pid() {
6636
+ return void 0;
6265
6637
  }
6266
- stdout = new PassThrough();
6267
- stderr = new PassThrough();
6268
- stdin;
6269
- pid = void 0;
6270
- exitCode = null;
6271
- signalCode = null;
6272
- killed = false;
6273
- buffer = "";
6274
- closed = false;
6275
- kill(signal = "SIGTERM") {
6276
- if (this.closed) return false;
6277
- this.killed = true;
6278
- this.signalCode = typeof signal === "string" ? signal : null;
6279
- void this.shutdown(null, this.signalCode);
6280
- return true;
6638
+ get currentSessionId() {
6639
+ return this.mappingState.sessionId;
6281
6640
  }
6282
- ref() {
6283
- return this;
6641
+ get exitCode() {
6642
+ return this.exitInfo?.code ?? null;
6284
6643
  }
6285
- unref() {
6286
- return this;
6644
+ get signalCode() {
6645
+ return this.exitInfo?.signal ?? null;
6287
6646
  }
6288
- async handleInput(chunk) {
6289
- this.buffer += chunk;
6290
- const lines = this.buffer.split("\n");
6291
- this.buffer = lines.pop() || "";
6292
- for (const line of lines) {
6293
- if (!line.trim() || this.closed) continue;
6294
- await this.handleCommand(line);
6295
- }
6647
+ get closed() {
6648
+ return this.didClose;
6296
6649
  }
6297
- async handleCommand(raw) {
6298
- let command;
6299
- try {
6300
- command = JSON.parse(raw);
6301
- } catch (error) {
6302
- this.writeError(error);
6303
- return;
6650
+ on(event, cb) {
6651
+ this.events.on(event, cb);
6652
+ }
6653
+ async start(input) {
6654
+ if (this.started) {
6655
+ return { ok: false, reason: "runtime_error", error: "runtime session already started" };
6304
6656
  }
6305
- const id = typeof command.id === "string" ? command.id : void 0;
6306
- try {
6307
- if (command.type === "prompt") {
6308
- await this.session.prompt(String(command.message ?? ""));
6309
- this.writeStdout(JSON.stringify({ id, type: "response", command: "prompt", success: true }));
6310
- } else if (command.type === "steer") {
6311
- await this.session.steer(String(command.message ?? ""));
6312
- this.writeStdout(JSON.stringify({ id, type: "response", command: "steer", success: true }));
6313
- } else {
6314
- throw new Error(`Unsupported Pi SDK command: ${command.type || "unknown"}`);
6657
+ if (this.didClose) return { ok: false, reason: "closed" };
6658
+ this.started = true;
6659
+ const sessionId = input.sessionId || this.ctx.config.sessionId || randomUUID3();
6660
+ this.mappingState.sessionId = sessionId;
6661
+ this.setCurrentSessionId(sessionId);
6662
+ const session = await this.sessionFactory({
6663
+ ...this.ctx,
6664
+ config: {
6665
+ ...this.ctx.config,
6666
+ sessionId
6315
6667
  }
6316
- } catch (error) {
6317
- this.writeStdout(JSON.stringify({
6318
- id,
6319
- type: "response",
6320
- command: command.type || "unknown",
6321
- success: false,
6322
- error: piErrorMessage(error)
6323
- }));
6668
+ }, sessionId);
6669
+ this.session = session;
6670
+ this.mappingState.sessionId = session.sessionId;
6671
+ this.setCurrentSessionId(session.sessionId);
6672
+ this.unsubscribe = session.subscribe((event) => {
6673
+ for (const parsed of mapPiSdkEventToParsedEvents(event, this.mappingState)) {
6674
+ this.events.emit("runtime_event", parsed);
6675
+ }
6676
+ });
6677
+ this.emitSessionInit();
6678
+ this.launchPrompt(input.text);
6679
+ return { ok: true, acceptedAs: "prompt" };
6680
+ }
6681
+ send(input) {
6682
+ if (this.didClose) return { ok: false, reason: "closed" };
6683
+ const session = this.session;
6684
+ if (!session) return { ok: false, reason: "closed" };
6685
+ if (input.mode === "busy") {
6686
+ this.deferSdkCall(() => session.steer(input.text));
6687
+ return { ok: true, acceptedAs: "steer" };
6688
+ }
6689
+ if (session.isStreaming) {
6690
+ return { ok: false, reason: "busy_rejected", error: "Pi session is still streaming" };
6691
+ }
6692
+ this.launchPrompt(input.text);
6693
+ return { ok: true, acceptedAs: "prompt" };
6694
+ }
6695
+ async stop(opts) {
6696
+ if (this.didClose) return;
6697
+ this.requestedStopReason = opts?.reason;
6698
+ const signal = opts?.signal ?? "SIGTERM";
6699
+ const session = this.session;
6700
+ if (session?.isStreaming) {
6701
+ try {
6702
+ await session.abort();
6703
+ } catch (error) {
6704
+ this.events.emit("stderr", piErrorMessage(error));
6705
+ }
6706
+ }
6707
+ await this.disposeSession();
6708
+ this.emitExitAndClose(null, signal);
6709
+ }
6710
+ async dispose() {
6711
+ if (this.didClose) return;
6712
+ await this.disposeSession();
6713
+ this.emitExitAndClose(0, null);
6714
+ }
6715
+ emitSessionInit() {
6716
+ const sessionId = this.mappingState.sessionId;
6717
+ if (!sessionId || this.mappingState.sessionAnnounced) return;
6718
+ this.mappingState.sessionAnnounced = true;
6719
+ this.events.emit("runtime_event", { kind: "session_init", sessionId });
6720
+ }
6721
+ launchPrompt(text) {
6722
+ const session = this.session;
6723
+ if (!session) {
6724
+ this.events.emit("runtime_event", {
6725
+ kind: "error",
6726
+ message: "Pi SDK session is not started"
6727
+ });
6728
+ return;
6324
6729
  }
6730
+ this.deferSdkCall(() => session.prompt(text));
6325
6731
  }
6326
- writeStdout(line) {
6327
- if (this.closed) return;
6328
- this.stdout.write(line + "\n");
6329
- }
6330
- writeError(error) {
6331
- if (this.closed) return;
6332
- this.stderr.write(piErrorMessage(error) + "\n");
6732
+ deferSdkCall(invoke) {
6733
+ setImmediate(() => {
6734
+ if (this.didClose) return;
6735
+ try {
6736
+ void invoke().catch((error) => {
6737
+ if (this.didClose) return;
6738
+ this.events.emit("runtime_event", {
6739
+ kind: "error",
6740
+ message: piErrorMessage(error)
6741
+ });
6742
+ });
6743
+ } catch (error) {
6744
+ if (this.didClose) return;
6745
+ this.events.emit("runtime_event", {
6746
+ kind: "error",
6747
+ message: piErrorMessage(error)
6748
+ });
6749
+ }
6750
+ });
6333
6751
  }
6334
- async shutdown(code, signal) {
6335
- if (this.closed) return;
6336
- this.closed = true;
6337
- this.exitCode = code;
6338
- this.signalCode = signal;
6752
+ async disposeSession() {
6753
+ const unsubscribe = this.unsubscribe;
6754
+ this.unsubscribe = null;
6339
6755
  try {
6340
- if (this.session.isStreaming) {
6341
- await this.session.abort();
6342
- }
6343
- } catch (error) {
6344
- this.stderr.write(piErrorMessage(error) + "\n");
6756
+ unsubscribe?.();
6757
+ } catch {
6345
6758
  }
6759
+ const session = this.session;
6760
+ this.session = null;
6346
6761
  try {
6347
- this.session.dispose();
6348
- } catch {
6762
+ session?.dispose();
6763
+ } catch (error) {
6764
+ this.events.emit("stderr", piErrorMessage(error));
6349
6765
  }
6350
- this.stdin.destroy();
6351
- this.stdout.end();
6352
- this.stderr.end();
6353
- this.emit("exit", code, signal);
6354
- this.emit("close", code, signal);
6766
+ }
6767
+ emitExitAndClose(code, signal) {
6768
+ if (this.didClose) return;
6769
+ this.didClose = true;
6770
+ const info = {
6771
+ code,
6772
+ signal,
6773
+ reason: this.requestedStopReason ? "requested" : "runtime_exit"
6774
+ };
6775
+ this.exitInfo = info;
6776
+ this.events.emit("exit", info);
6777
+ this.events.emit("close", info);
6355
6778
  }
6356
6779
  };
6357
6780
  var PiDriver = class {
@@ -6378,10 +6801,9 @@ var PiDriver = class {
6378
6801
  busyDeliveryMode = "direct";
6379
6802
  usesSlockCliForCommunication = true;
6380
6803
  sessionId = null;
6381
- sessionAnnounced = false;
6382
- sawTextDelta = false;
6383
- requestId = 0;
6384
- process = null;
6804
+ get currentSessionId() {
6805
+ return this.sessionId;
6806
+ }
6385
6807
  probe() {
6386
6808
  return {
6387
6809
  available: true,
@@ -6391,144 +6813,20 @@ var PiDriver = class {
6391
6813
  async detectModels() {
6392
6814
  return detectPiModels();
6393
6815
  }
6394
- async spawn(ctx) {
6395
- this.sessionId = ctx.config.sessionId || randomUUID3();
6396
- this.sessionAnnounced = false;
6397
- this.sawTextDelta = false;
6398
- this.requestId = 0;
6399
- const sessionDir = buildPiSessionDir(ctx.workingDirectory);
6400
- mkdirSync4(sessionDir, { recursive: true });
6401
- const spawnEnv = await buildPiSpawnEnv(ctx);
6402
- const agentDir = spawnEnv.PI_CODING_AGENT_DIR || getAgentDir();
6403
- const authStorage = AuthStorage.create(path11.join(agentDir, "auth.json"));
6404
- const modelRegistry = ModelRegistry.create(authStorage, path11.join(agentDir, "models.json"));
6405
- const launchRuntimeFields = runtimeConfigToLaunchFields(ctx.config);
6406
- const model = resolvePiModelFromRegistry(launchRuntimeFields.model, modelRegistry);
6407
- if (launchRuntimeFields.model && launchRuntimeFields.model !== "default" && !model) {
6408
- throw new Error(`Pi model not found: ${launchRuntimeFields.model}`);
6409
- }
6410
- const settingsManager = SettingsManager.inMemory({ compaction: { enabled: false } });
6411
- const resourceLoader = new DefaultResourceLoader({
6412
- cwd: ctx.workingDirectory,
6413
- agentDir,
6414
- settingsManager,
6415
- systemPromptOverride: () => ctx.standingPrompt
6416
- });
6417
- await resourceLoader.reload();
6418
- const existingSessionFile = ctx.config.sessionId ? findPiSessionFile(sessionDir, ctx.config.sessionId) : null;
6419
- const sessionManager = existingSessionFile ? SessionManager.open(existingSessionFile, sessionDir, ctx.workingDirectory) : SessionManager.create(ctx.workingDirectory, sessionDir, { id: this.sessionId });
6420
- const { session } = await createAgentSession({
6421
- cwd: ctx.workingDirectory,
6422
- agentDir,
6423
- model,
6424
- thinkingLevel: launchRuntimeFields.reasoningEffort,
6425
- authStorage,
6426
- modelRegistry,
6427
- resourceLoader,
6428
- customTools: [
6429
- createBashTool(ctx.workingDirectory, {
6430
- spawnHook: (spawnContext) => ({
6431
- ...spawnContext,
6432
- env: {
6433
- ...spawnContext.env,
6434
- ...spawnEnv
6435
- }
6436
- })
6437
- })
6438
- ],
6439
- sessionManager,
6440
- settingsManager
6441
- });
6442
- this.sessionId = session.sessionId;
6443
- const proc = new PiSdkProcess(session);
6444
- this.process = proc;
6445
- setImmediate(() => {
6446
- if (this.process === proc && !proc.killed) {
6447
- this.sendRpcCommand("prompt", { message: ctx.prompt });
6448
- }
6816
+ createSession(ctx) {
6817
+ this.sessionId = ctx.config.sessionId || null;
6818
+ return new PiSdkRuntimeSession(ctx, (sessionId) => {
6819
+ this.sessionId = sessionId;
6449
6820
  });
6450
- return { process: proc };
6451
6821
  }
6452
- parseLine(line) {
6453
- let event;
6454
- try {
6455
- event = JSON.parse(line);
6456
- } catch {
6457
- return [];
6458
- }
6459
- const events = [];
6460
- if (event.type === "session" && event.id) {
6461
- this.sessionId = event.id;
6462
- if (!this.sessionAnnounced) {
6463
- this.sessionAnnounced = true;
6464
- events.push({ kind: "session_init", sessionId: event.id });
6465
- }
6466
- return events;
6467
- }
6468
- if (!this.sessionAnnounced && this.sessionId) {
6469
- events.push({ kind: "session_init", sessionId: this.sessionId });
6470
- this.sessionAnnounced = true;
6471
- }
6472
- if (event.type === "response") {
6473
- if (event.data?.sessionId && event.data.sessionId !== this.sessionId) {
6474
- this.sessionId = event.data.sessionId;
6475
- }
6476
- if (event.success === false) {
6477
- events.push({ kind: "error", message: piErrorMessage(event.error) });
6478
- }
6479
- return events;
6480
- }
6481
- if (event.type === "message_start" && event.message?.role === "assistant") {
6482
- this.sawTextDelta = false;
6483
- return events;
6484
- }
6485
- const assistantEvent = event.assistantMessageEvent;
6486
- if (event.type === "message_update" && assistantEvent) {
6487
- switch (assistantEvent.type) {
6488
- case "thinking_delta":
6489
- if (typeof assistantEvent.delta === "string" && assistantEvent.delta.length > 0) {
6490
- events.push({ kind: "thinking", text: assistantEvent.delta });
6491
- }
6492
- break;
6493
- case "text_delta":
6494
- if (typeof assistantEvent.delta === "string" && assistantEvent.delta.length > 0) {
6495
- this.sawTextDelta = true;
6496
- events.push({ kind: "text", text: assistantEvent.delta });
6497
- }
6498
- break;
6499
- case "thinking_start":
6500
- case "text_start":
6501
- break;
6502
- case "tool_use":
6503
- case "tool_call":
6504
- case "tool_start":
6505
- events.push({
6506
- kind: "tool_call",
6507
- name: assistantEvent.name || assistantEvent.toolName || "unknown_tool",
6508
- input: assistantEvent.input ?? assistantEvent.parameters ?? {}
6509
- });
6510
- break;
6511
- case "text_end":
6512
- if (!this.sawTextDelta && typeof assistantEvent.content === "string" && assistantEvent.content.length > 0) {
6513
- events.push({ kind: "text", text: assistantEvent.content });
6514
- }
6515
- break;
6516
- }
6517
- return events;
6518
- }
6519
- if (event.type === "agent_end") {
6520
- events.push({ kind: "turn_end", sessionId: this.sessionId || void 0 });
6521
- } else if (event.type === "error") {
6522
- events.push({ kind: "error", message: piErrorMessage(event.error ?? event.message) });
6523
- }
6524
- return events;
6822
+ async spawn(_ctx) {
6823
+ throw new Error("PiDriver uses a native RuntimeSession; child-process spawn is unsupported");
6525
6824
  }
6526
- encodeStdinMessage(text, _sessionId, opts) {
6527
- return JSON.stringify({
6528
- id: this.nextRequestId(),
6529
- type: opts?.mode === "idle" ? "prompt" : "steer",
6530
- message: text
6531
- });
6825
+ parseLine(_line) {
6826
+ return [];
6827
+ }
6828
+ encodeStdinMessage(_text, _sessionId, _opts) {
6829
+ return null;
6532
6830
  }
6533
6831
  buildSystemPrompt(config, _agentId) {
6534
6832
  return buildCliTransportSystemPrompt(config, {
@@ -6541,18 +6839,141 @@ var PiDriver = class {
6541
6839
  messageNotificationStyle: "direct"
6542
6840
  });
6543
6841
  }
6544
- nextRequestId() {
6545
- this.requestId += 1;
6546
- return String(this.requestId);
6842
+ };
6843
+
6844
+ // src/drivers/runtimeSession.ts
6845
+ import { EventEmitter as EventEmitter2 } from "events";
6846
+ function descriptorFromDriver(driver) {
6847
+ const lifecycle = driver.lifecycle.kind === "per_turn" ? "turn_based" : "persistent_stream";
6848
+ const idle = driver.supportsStdinNotification ? "stdin" : "unsupported";
6849
+ const busy = driver.supportsStdinNotification ? "stdin_steer" : "unsupported";
6850
+ return {
6851
+ transport: "child_process",
6852
+ lifecycle,
6853
+ input: {
6854
+ initial: "start",
6855
+ idle,
6856
+ busy
6857
+ },
6858
+ readiness: "spawned",
6859
+ turnBoundary: driver.lifecycle.kind === "per_turn" ? "process_exit" : "parsed_event",
6860
+ startPolicy: driver.lifecycle.kind === "per_turn" ? driver.lifecycle.start : "immediate",
6861
+ inFlightWake: driver.lifecycle.inFlightWake,
6862
+ busyDelivery: driver.busyDeliveryMode,
6863
+ postTurn: driver.terminateProcessOnTurnEnd ? "terminate_process" : driver.endStdinOnTurnEnd ? "close_stdin" : "keep_alive"
6864
+ };
6865
+ }
6866
+ var ChildProcessRuntimeSession = class {
6867
+ constructor(driver, ctx) {
6868
+ this.driver = driver;
6869
+ this.ctx = ctx;
6870
+ this.descriptor = descriptorFromDriver(driver);
6547
6871
  }
6548
- sendRpcCommand(type, params) {
6549
- this.process?.stdin?.write(JSON.stringify({
6550
- id: this.nextRequestId(),
6551
- type,
6552
- ...params
6553
- }) + "\n");
6872
+ descriptor;
6873
+ events = new EventEmitter2();
6874
+ process = null;
6875
+ started = false;
6876
+ stdoutBuffer = "";
6877
+ requestedStopReason;
6878
+ get pid() {
6879
+ return this.process?.pid;
6880
+ }
6881
+ get currentSessionId() {
6882
+ return this.driver.currentSessionId;
6883
+ }
6884
+ get exitCode() {
6885
+ return this.process?.exitCode ?? null;
6886
+ }
6887
+ get signalCode() {
6888
+ return this.process?.signalCode ?? null;
6889
+ }
6890
+ get closed() {
6891
+ return this.process ? this.process.exitCode != null || this.process.signalCode != null : false;
6892
+ }
6893
+ on(event, cb) {
6894
+ this.events.on(event, cb);
6895
+ }
6896
+ async start(input) {
6897
+ if (this.started) {
6898
+ return { ok: false, reason: "runtime_error", error: "runtime session already started" };
6899
+ }
6900
+ this.started = true;
6901
+ const launchCtx = {
6902
+ ...this.ctx,
6903
+ prompt: input.text,
6904
+ config: {
6905
+ ...this.ctx.config,
6906
+ sessionId: input.sessionId ?? this.ctx.config.sessionId
6907
+ }
6908
+ };
6909
+ const { process: process2 } = await this.driver.spawn(launchCtx);
6910
+ this.process = process2;
6911
+ this.attachProcess(process2);
6912
+ return { ok: true, acceptedAs: "prompt" };
6913
+ }
6914
+ send(input) {
6915
+ const proc = this.process;
6916
+ if (!proc || this.closed) return { ok: false, reason: "closed" };
6917
+ const encoded = this.driver.encodeStdinMessage(input.text, input.sessionId ?? null, { mode: input.mode });
6918
+ if (!encoded) return { ok: false, reason: "unsupported" };
6919
+ proc.stdin?.write(encoded + "\n");
6920
+ return { ok: true, acceptedAs: input.mode === "busy" ? "steer" : "prompt" };
6921
+ }
6922
+ async stop(opts) {
6923
+ const proc = this.process;
6924
+ if (!proc || this.closed) return;
6925
+ this.requestedStopReason = opts?.reason;
6926
+ proc.kill(opts?.signal ?? "SIGTERM");
6927
+ if (!opts?.forceAfterMs) return;
6928
+ setTimeout(() => {
6929
+ if (!this.closed) {
6930
+ try {
6931
+ proc.kill("SIGKILL");
6932
+ } catch {
6933
+ }
6934
+ }
6935
+ }, opts.forceAfterMs).unref?.();
6936
+ }
6937
+ attachProcess(process2) {
6938
+ process2.stdout?.on("data", (chunk) => {
6939
+ const chunkText = chunk.toString();
6940
+ this.events.emit("stdout", chunkText);
6941
+ this.stdoutBuffer += chunkText;
6942
+ const lines = this.stdoutBuffer.split("\n");
6943
+ this.stdoutBuffer = lines.pop() || "";
6944
+ for (const line of lines) {
6945
+ if (!line.trim()) continue;
6946
+ for (const event of this.driver.parseLine(line)) {
6947
+ this.events.emit("runtime_event", event);
6948
+ }
6949
+ }
6950
+ });
6951
+ process2.stderr?.on("data", (chunk) => {
6952
+ const text = chunk.toString().trim();
6953
+ if (text) this.events.emit("stderr", text);
6954
+ });
6955
+ process2.on("error", (err) => {
6956
+ this.events.emit("error", err);
6957
+ });
6958
+ process2.on("exit", (code, signal) => {
6959
+ this.events.emit("exit", {
6960
+ code,
6961
+ signal,
6962
+ reason: this.requestedStopReason ? "requested" : "runtime_exit"
6963
+ });
6964
+ });
6965
+ process2.on("close", (code, signal) => {
6966
+ this.events.emit("close", {
6967
+ code,
6968
+ signal,
6969
+ reason: this.requestedStopReason ? "requested" : "runtime_exit"
6970
+ });
6971
+ });
6554
6972
  }
6555
6973
  };
6974
+ function createChildProcessRuntimeSession(driver, ctx) {
6975
+ return new ChildProcessRuntimeSession(driver, ctx);
6976
+ }
6556
6977
 
6557
6978
  // src/drivers/index.ts
6558
6979
  var driverFactories = {
@@ -6953,6 +7374,15 @@ var RUNNER_CREDENTIAL_MINT_MAX_ATTEMPTS = 3;
6953
7374
  function assertNeverApmEffect(effect) {
6954
7375
  throw new Error(`Unhandled APM gated steering effect: ${String(effect)}`);
6955
7376
  }
7377
+ function requireImmediateRuntimeSendResult(result, context) {
7378
+ if (typeof result.then === "function") {
7379
+ throw new Error(`RuntimeSession.send returned async result in synchronous APM path: ${context}`);
7380
+ }
7381
+ return result;
7382
+ }
7383
+ function runtimeSendFailureOutcome(result) {
7384
+ return !result.ok && result.reason === "unsupported" ? "encode_failed" : "send_failed";
7385
+ }
6956
7386
  var RUNNER_CREDENTIAL_MINT_RETRY_DELAY_MS = 250;
6957
7387
  var WORKSPACE_TEXT_FILE_MAX_BYTES = 1048576;
6958
7388
  var WORKSPACE_IMAGE_PREVIEW_MAX_BYTES = 5 * 1024 * 1024;
@@ -7900,7 +8330,7 @@ function buildRuntimeStallDiagnostic(ap, staleForMs, staleForMinutes) {
7900
8330
  if (ap.lastActivityDetail) {
7901
8331
  context.push(`after ${ap.lastActivityDetail}`);
7902
8332
  }
7903
- if (ap.driver.busyDeliveryMode === "gated") {
8333
+ if (ap.runtime.descriptor.busyDelivery === "gated") {
7904
8334
  context.push(`phase=${ap.gatedSteering.phase}`);
7905
8335
  }
7906
8336
  if (ap.gatedSteering.outstandingToolUses > 0) {
@@ -7932,10 +8362,10 @@ function buildRuntimeStallDiagnostic(ap, staleForMs, staleForMinutes) {
7932
8362
  sessionIdPresent: Boolean(ap.sessionId),
7933
8363
  inboxCount: ap.inbox.length,
7934
8364
  pendingNotificationCount: ap.notifications.pendingCount,
7935
- processPidPresent: typeof ap.process.pid === "number",
8365
+ processPidPresent: typeof ap.runtime.pid === "number",
7936
8366
  busyDeliveryMode: ap.driver.busyDeliveryMode,
7937
8367
  supportsStdinNotification: ap.driver.supportsStdinNotification,
7938
- gatedPhase: ap.driver.busyDeliveryMode === "gated" ? ap.gatedSteering.phase : void 0,
8368
+ gatedPhase: ap.runtime.descriptor.busyDelivery === "gated" ? ap.gatedSteering.phase : void 0,
7939
8369
  outstandingToolUses: ap.gatedSteering.outstandingToolUses,
7940
8370
  compacting: ap.gatedSteering.compacting,
7941
8371
  recentStderrCount: ap.recentStderr.length,
@@ -8078,8 +8508,10 @@ var RUNTIME_TELEMETRY_RESERVED_ATTR_KEYS = /* @__PURE__ */ new Set([
8078
8508
  "runtimeResultId",
8079
8509
  "daemonVersion",
8080
8510
  "daemon_version",
8511
+ "daemon_version_present",
8081
8512
  "computerVersion",
8082
- "computer_version"
8513
+ "computer_version",
8514
+ "computer_version_present"
8083
8515
  ]);
8084
8516
  function sanitizeRuntimeTelemetryPayloadAttrs(attrs) {
8085
8517
  const sanitized = {};
@@ -8136,9 +8568,11 @@ var AgentProcessManager = class _AgentProcessManager {
8136
8568
  stdinNotificationRetryMs;
8137
8569
  cliTransportTraceDir = null;
8138
8570
  deliveryTraceContexts = /* @__PURE__ */ new WeakMap();
8139
- processExitTraceAttrs = /* @__PURE__ */ new WeakMap();
8571
+ runtimeExitTraceAttrs = /* @__PURE__ */ new WeakMap();
8140
8572
  agentVisibleBoundaries = /* @__PURE__ */ new Map();
8141
8573
  agentVisibleMessageIds = /* @__PURE__ */ new Map();
8574
+ daemonVersion;
8575
+ computerVersion;
8142
8576
  constructor(chatBridgePath, sendToServer, daemonApiKey, opts) {
8143
8577
  this.chatBridgePath = chatBridgePath;
8144
8578
  this.slockCliPath = opts.slockCliPath ?? "";
@@ -8150,6 +8584,8 @@ var AgentProcessManager = class _AgentProcessManager {
8150
8584
  this.driverResolver = opts.driverResolver || getDriver;
8151
8585
  this.defaultAgentEnvVarsProvider = opts.defaultAgentEnvVarsProvider || null;
8152
8586
  this.tracer = opts.tracer ?? noopTracer;
8587
+ this.daemonVersion = opts.daemonVersion?.trim() || null;
8588
+ this.computerVersion = opts.computerVersion?.trim() || null;
8153
8589
  this.stdinNotificationRetryMs = Math.max(
8154
8590
  0,
8155
8591
  Math.floor(opts.stdinNotificationRetryMs ?? STDIN_NOTIFICATION_RETRY_DELAY_MS)
@@ -8390,7 +8826,7 @@ var AgentProcessManager = class _AgentProcessManager {
8390
8826
  });
8391
8827
  span.end(status);
8392
8828
  }
8393
- startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId) {
8829
+ startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId, wakeMessageTransient = false) {
8394
8830
  return {
8395
8831
  agentId,
8396
8832
  launchId,
@@ -8399,6 +8835,7 @@ var AgentProcessManager = class _AgentProcessManager {
8399
8835
  session_id_present: Boolean(config.sessionId),
8400
8836
  launch_id_present: Boolean(launchId),
8401
8837
  wake_message_present: Boolean(wakeMessage),
8838
+ wake_message_transient: Boolean(wakeMessage && wakeMessageTransient),
8402
8839
  unread_channels_count: unreadSummary ? Object.keys(unreadSummary).length : 0,
8403
8840
  resume_prompt_present: Boolean(resumePrompt),
8404
8841
  queue_depth: this.agentStartQueue.length,
@@ -8410,6 +8847,9 @@ var AgentProcessManager = class _AgentProcessManager {
8410
8847
  getDeliveryTraceContext(message) {
8411
8848
  return this.deliveryTraceContexts.get(message) ?? {};
8412
8849
  }
8850
+ isTransientDelivery(message) {
8851
+ return this.getDeliveryTraceContext(message).transient === true;
8852
+ }
8413
8853
  deliveryTraceAttrs(agentId, message, attrs = {}) {
8414
8854
  const context = this.getDeliveryTraceContext(message);
8415
8855
  const deliveryCorrelationId = context.deliveryId ?? message.message_id;
@@ -8421,14 +8861,15 @@ var AgentProcessManager = class _AgentProcessManager {
8421
8861
  sender_type: message.sender_type,
8422
8862
  messageId: message.message_id,
8423
8863
  message_id_present: Boolean(message.message_id),
8864
+ transient_delivery: context.transient === true,
8424
8865
  ...attrs
8425
8866
  };
8426
8867
  }
8427
- async startAgent(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId) {
8428
- this.recordDaemonTrace("daemon.agent.start.requested", this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId));
8868
+ async startAgent(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId, wakeMessageTransient = false) {
8869
+ this.recordDaemonTrace("daemon.agent.start.requested", this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId, wakeMessageTransient));
8429
8870
  if (this.agents.has(agentId)) {
8430
8871
  this.recordDaemonTrace("daemon.agent.start.ignored", {
8431
- ...this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId),
8872
+ ...this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId, wakeMessageTransient),
8432
8873
  reason: "already_running"
8433
8874
  });
8434
8875
  logger.info(`[Agent ${agentId}] Start ignored (already running)`);
@@ -8436,7 +8877,7 @@ var AgentProcessManager = class _AgentProcessManager {
8436
8877
  }
8437
8878
  if (this.agentsStarting.has(agentId)) {
8438
8879
  this.recordDaemonTrace("daemon.agent.start.ignored", {
8439
- ...this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId),
8880
+ ...this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId, wakeMessageTransient),
8440
8881
  reason: "already_starting"
8441
8882
  });
8442
8883
  logger.info(`[Agent ${agentId}] Start ignored (startup in progress)`);
@@ -8444,7 +8885,7 @@ var AgentProcessManager = class _AgentProcessManager {
8444
8885
  }
8445
8886
  if (this.queuedAgentStarts.has(agentId)) {
8446
8887
  this.recordDaemonTrace("daemon.agent.start.ignored", {
8447
- ...this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId),
8888
+ ...this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId, wakeMessageTransient),
8448
8889
  reason: "already_queued"
8449
8890
  });
8450
8891
  logger.info(`[Agent ${agentId}] Start ignored (startup already queued)`);
@@ -8455,6 +8896,7 @@ var AgentProcessManager = class _AgentProcessManager {
8455
8896
  agentId,
8456
8897
  config,
8457
8898
  wakeMessage,
8899
+ wakeMessageTransient,
8458
8900
  unreadSummary,
8459
8901
  resumePrompt,
8460
8902
  launchId,
@@ -8463,7 +8905,7 @@ var AgentProcessManager = class _AgentProcessManager {
8463
8905
  };
8464
8906
  this.agentStartQueue.push(item);
8465
8907
  this.queuedAgentStarts.set(agentId, item);
8466
- this.recordDaemonTrace("daemon.agent.start.queued", this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId));
8908
+ this.recordDaemonTrace("daemon.agent.start.queued", this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId, wakeMessageTransient));
8467
8909
  logger.info(
8468
8910
  `[Agent ${agentId}] Start queued (queue=${this.agentStartQueue.length}, active=${this.activeAgentStartCount}, max=${this.maxConcurrentAgentStarts}, interval=${this.agentStartIntervalMs}ms)`
8469
8911
  );
@@ -8480,7 +8922,7 @@ var AgentProcessManager = class _AgentProcessManager {
8480
8922
  const waitMs = shouldRateLimit ? Math.max(0, this.agentStartIntervalMs - elapsed) : 0;
8481
8923
  if (waitMs > 0) {
8482
8924
  this.recordDaemonTrace("daemon.agent.start.rate_limited", {
8483
- ...this.startQueueTraceAttrs(next.agentId, next.config, next.wakeMessage, next.unreadSummary, next.resumePrompt, next.launchId),
8925
+ ...this.startQueueTraceAttrs(next.agentId, next.config, next.wakeMessage, next.unreadSummary, next.resumePrompt, next.launchId, next.wakeMessageTransient),
8484
8926
  wait_ms: waitMs
8485
8927
  });
8486
8928
  this.agentStartPumpTimer = setTimeout(() => {
@@ -8493,7 +8935,7 @@ var AgentProcessManager = class _AgentProcessManager {
8493
8935
  if (!item) return;
8494
8936
  if (this.queuedAgentStarts.get(item.agentId) !== item) {
8495
8937
  this.recordDaemonTrace("daemon.agent.start.skipped", {
8496
- ...this.startQueueTraceAttrs(item.agentId, item.config, item.wakeMessage, item.unreadSummary, item.resumePrompt, item.launchId),
8938
+ ...this.startQueueTraceAttrs(item.agentId, item.config, item.wakeMessage, item.unreadSummary, item.resumePrompt, item.launchId, item.wakeMessageTransient),
8497
8939
  reason: "stale_queue_item"
8498
8940
  });
8499
8941
  this.pumpAgentStartQueue();
@@ -8502,7 +8944,7 @@ var AgentProcessManager = class _AgentProcessManager {
8502
8944
  this.queuedAgentStarts.delete(item.agentId);
8503
8945
  if (this.agents.has(item.agentId) || this.agentsStarting.has(item.agentId)) {
8504
8946
  this.recordDaemonTrace("daemon.agent.start.skipped", {
8505
- ...this.startQueueTraceAttrs(item.agentId, item.config, item.wakeMessage, item.unreadSummary, item.resumePrompt, item.launchId),
8947
+ ...this.startQueueTraceAttrs(item.agentId, item.config, item.wakeMessage, item.unreadSummary, item.resumePrompt, item.launchId, item.wakeMessageTransient),
8506
8948
  reason: "already_running_or_starting"
8507
8949
  });
8508
8950
  logger.info(`[Agent ${item.agentId}] Queued start skipped (already running or starting)`);
@@ -8516,14 +8958,15 @@ var AgentProcessManager = class _AgentProcessManager {
8516
8958
  logger.info(
8517
8959
  `[Agent ${item.agentId}] Dequeued start (remaining=${this.agentStartQueue.length}, active=${this.activeAgentStartCount})`
8518
8960
  );
8519
- this.recordDaemonTrace("daemon.agent.start.dequeued", this.startQueueTraceAttrs(item.agentId, item.config, item.wakeMessage, item.unreadSummary, item.resumePrompt, item.launchId));
8961
+ this.recordDaemonTrace("daemon.agent.start.dequeued", this.startQueueTraceAttrs(item.agentId, item.config, item.wakeMessage, item.unreadSummary, item.resumePrompt, item.launchId, item.wakeMessageTransient));
8520
8962
  this.startAgentNow(
8521
8963
  item.agentId,
8522
8964
  item.config,
8523
8965
  item.wakeMessage,
8524
8966
  item.unreadSummary,
8525
8967
  item.resumePrompt,
8526
- item.launchId
8968
+ item.launchId,
8969
+ item.wakeMessageTransient ?? false
8527
8970
  ).then(() => {
8528
8971
  this.releaseAgentStartSlot(item.agentId, "spawn attempted");
8529
8972
  item.resolve();
@@ -8558,7 +9001,7 @@ var AgentProcessManager = class _AgentProcessManager {
8558
9001
  this.agentStartPumpTimer = null;
8559
9002
  }
8560
9003
  this.recordDaemonTrace("daemon.agent.start.cancelled", {
8561
- ...this.startQueueTraceAttrs(agentId, item.config, item.wakeMessage, item.unreadSummary, item.resumePrompt, item.launchId),
9004
+ ...this.startQueueTraceAttrs(agentId, item.config, item.wakeMessage, item.unreadSummary, item.resumePrompt, item.launchId, item.wakeMessageTransient),
8562
9005
  reason
8563
9006
  }, "cancelled");
8564
9007
  logger.info(`[Agent ${agentId}] Queued start cancelled (${reason})`);
@@ -8569,7 +9012,7 @@ var AgentProcessManager = class _AgentProcessManager {
8569
9012
  for (const item of this.agentStartQueue) {
8570
9013
  if (this.queuedAgentStarts.get(item.agentId) === item) {
8571
9014
  this.recordDaemonTrace("daemon.agent.start.cancelled", {
8572
- ...this.startQueueTraceAttrs(item.agentId, item.config, item.wakeMessage, item.unreadSummary, item.resumePrompt, item.launchId),
9015
+ ...this.startQueueTraceAttrs(item.agentId, item.config, item.wakeMessage, item.unreadSummary, item.resumePrompt, item.launchId, item.wakeMessageTransient),
8573
9016
  reason
8574
9017
  }, "cancelled");
8575
9018
  logger.info(`[Agent ${item.agentId}] Queued start cancelled (${reason})`);
@@ -8584,10 +9027,10 @@ var AgentProcessManager = class _AgentProcessManager {
8584
9027
  this.agentStartPumpTimer = null;
8585
9028
  }
8586
9029
  }
8587
- async startAgentNow(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId) {
9030
+ async startAgentNow(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId, wakeMessageTransient = false) {
8588
9031
  if (this.agents.has(agentId)) {
8589
9032
  this.recordDaemonTrace("daemon.agent.spawn.skipped", {
8590
- ...this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId),
9033
+ ...this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId, wakeMessageTransient),
8591
9034
  reason: "already_running"
8592
9035
  });
8593
9036
  logger.info(`[Agent ${agentId}] Start ignored (already running)`);
@@ -8595,14 +9038,15 @@ var AgentProcessManager = class _AgentProcessManager {
8595
9038
  }
8596
9039
  if (this.agentsStarting.has(agentId)) {
8597
9040
  this.recordDaemonTrace("daemon.agent.spawn.skipped", {
8598
- ...this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId),
9041
+ ...this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId, wakeMessageTransient),
8599
9042
  reason: "already_starting"
8600
9043
  });
8601
9044
  logger.info(`[Agent ${agentId}] Start ignored (startup in progress)`);
8602
9045
  return;
8603
9046
  }
8604
9047
  this.agentsStarting.add(agentId);
8605
- this.recordDaemonTrace("daemon.agent.spawn.started", this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId));
9048
+ this.recordDaemonTrace("daemon.agent.spawn.started", this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId, wakeMessageTransient));
9049
+ let agentProcess = null;
8606
9050
  try {
8607
9051
  const driver = this.driverResolver(config.runtime || "claude");
8608
9052
  const legacyWakeRuntimeProfile = wakeMessage ? runtimeProfileNotificationFromMessage(wakeMessage) : null;
@@ -8665,15 +9109,23 @@ var AgentProcessManager = class _AgentProcessManager {
8665
9109
  prompt += getBusyDeliveryNote(driver);
8666
9110
  promptSource = "resume_prompt";
8667
9111
  } else if (wakeMessage) {
8668
- const runtimeProfileControlPrompt = formatRuntimeProfileControlPrompt([wakeMessage]);
8669
- if (runtimeProfileControlPrompt) {
9112
+ const transientWakeMessage = wakeMessageTransient === true;
9113
+ const runtimeProfileControlPrompt = transientWakeMessage ? null : formatRuntimeProfileControlPrompt([wakeMessage]);
9114
+ if (transientWakeMessage) {
9115
+ prompt = `System notice received:
9116
+
9117
+ ${formatIncomingMessage(wakeMessage, driver)}
9118
+
9119
+ Respond as appropriate. Complete all your work before stopping.
9120
+ ${RESPONSE_TARGET_HINT}`;
9121
+ } else if (runtimeProfileControlPrompt) {
8670
9122
  prompt = runtimeProfileControlPrompt;
8671
9123
  } else {
8672
9124
  wakeMessageDeliveredAsInboxUpdate = true;
8673
9125
  prompt = this.formatInboxUpdateRuntimeInput([wakeMessage, ...startingInboxMessages], driver);
8674
9126
  }
8675
- promptSource = runtimeProfileControlPrompt ? "runtime_profile_control_message" : "wake_inbox_update";
8676
- if (!runtimeProfileControlPrompt && unreadSummary && Object.keys(unreadSummary).length > 0) {
9127
+ promptSource = transientWakeMessage ? "transient_wake_message" : runtimeProfileControlPrompt ? "runtime_profile_control_message" : "wake_inbox_update";
9128
+ if (!transientWakeMessage && !runtimeProfileControlPrompt && unreadSummary && Object.keys(unreadSummary).length > 0) {
8677
9129
  const otherUnread = Object.entries(unreadSummary);
8678
9130
  if (otherUnread.length > 0) {
8679
9131
  prompt += `
@@ -8730,7 +9182,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
8730
9182
  this.sendAgentStatus(agentId, "active", launchId || null);
8731
9183
  this.broadcastActivity(agentId, "online", "Process idle");
8732
9184
  this.recordDaemonTrace("daemon.agent.spawn.deferred", {
8733
- ...this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId),
9185
+ ...this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId, wakeMessageTransient),
8734
9186
  pending_messages_count: pendingMessages.length,
8735
9187
  reason: "defer_until_concrete_message"
8736
9188
  });
@@ -8740,7 +9192,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
8740
9192
  }
8741
9193
  return;
8742
9194
  }
8743
- const { process: proc } = await driver.spawn({
9195
+ const runtimeContext = {
8744
9196
  agentId,
8745
9197
  config: effectiveConfig,
8746
9198
  standingPrompt,
@@ -8752,15 +9204,10 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
8752
9204
  launchId: launchId || null,
8753
9205
  agentCredentialProxyInboxCoordinator: this.createAgentProxyInboxCoordinator(agentId),
8754
9206
  cliTransportTraceDir: this.cliTransportTraceDir
8755
- });
8756
- this.recordDaemonTrace("daemon.agent.spawn.created", {
8757
- ...this.startQueueTraceAttrs(agentId, effectiveConfig, wakeMessage, unreadSummary, resumePrompt, launchId),
8758
- detached: false,
8759
- new_session: false,
8760
- process_pid_present: typeof proc.pid === "number"
8761
- });
8762
- const agentProcess = {
8763
- process: proc,
9207
+ };
9208
+ const runtime = driver.createSession?.(runtimeContext) ?? createChildProcessRuntimeSession(driver, runtimeContext);
9209
+ agentProcess = {
9210
+ runtime,
8764
9211
  driver,
8765
9212
  inbox: wakeMessageDeliveredAsInboxUpdate && wakeMessage ? [wakeMessage, ...startingInboxMessages] : startingInboxMessages,
8766
9213
  config: runtimeConfig,
@@ -8807,30 +9254,20 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
8807
9254
  }
8808
9255
  if (wakeMessageDeliveredAsInboxUpdate) {
8809
9256
  this.recordInboxUpdateProjection(agentId, agentProcess, agentProcess.inbox, "spawn_wake_inbox_update", "wake", prompt);
8810
- } else if (wakeMessage) {
9257
+ } else if (wakeMessage && !wakeMessageTransient) {
8811
9258
  this.consumeVisibleMessages(agentId, { messages: [wakeMessage], source: "spawn_wake_message" });
8812
9259
  this.ackInjectedRuntimeProfileMessages(agentId, [wakeMessage], agentProcess.launchId);
8813
9260
  }
8814
- let buffer = "";
8815
- proc.stdout?.on("data", (chunk) => {
8816
- const chunkText = chunk.toString();
9261
+ runtime.on("stdout", (chunkText) => {
8817
9262
  const current = this.agents.get(agentId);
8818
9263
  if (current) {
8819
9264
  current.recentStdout = pushRecentStdout(current.recentStdout, chunkText);
8820
9265
  }
8821
- buffer += chunkText;
8822
- const lines = buffer.split("\n");
8823
- buffer = lines.pop() || "";
8824
- for (const line of lines) {
8825
- if (!line.trim()) continue;
8826
- const events = driver.parseLine(line);
8827
- for (const event of events) {
8828
- this.handleParsedEvent(agentId, event, driver);
8829
- }
8830
- }
8831
9266
  });
8832
- proc.stderr?.on("data", (chunk) => {
8833
- const text = chunk.toString().trim();
9267
+ runtime.on("runtime_event", (event) => {
9268
+ this.handleParsedEvent(agentId, event, driver);
9269
+ });
9270
+ runtime.on("stderr", (text) => {
8834
9271
  if (!text) return;
8835
9272
  const current = this.agents.get(agentId);
8836
9273
  if (driver.id === "codex" && isCodexProviderReconnectLog(text)) {
@@ -8855,7 +9292,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
8855
9292
  }
8856
9293
  logger.error(`[Agent ${agentId} stderr]: ${text}`);
8857
9294
  });
8858
- proc.on("error", (err) => {
9295
+ runtime.on("error", (err) => {
8859
9296
  const current = this.agents.get(agentId);
8860
9297
  if (current) {
8861
9298
  current.spawnError = err.message;
@@ -8870,9 +9307,9 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
8870
9307
  }, "error");
8871
9308
  logger.error(`[Agent ${agentId}] Process error: ${err.message}`);
8872
9309
  });
8873
- proc.on("exit", (code, signal) => {
9310
+ runtime.on("exit", ({ code, signal }) => {
8874
9311
  const current = this.agents.get(agentId);
8875
- if (current && current.process === proc) {
9312
+ if (current && current.runtime === runtime) {
8876
9313
  current.exitCode = code;
8877
9314
  current.exitSignal = signal;
8878
9315
  }
@@ -8887,14 +9324,14 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
8887
9324
  runtime_trace_active: Boolean(current?.runtimeTraceSpan),
8888
9325
  inbox_count: current?.inbox.length ?? 0,
8889
9326
  pending_notification_count: current?.notifications.pendingCount ?? 0,
8890
- ...this.processExitTraceAttrs.get(proc)
9327
+ ...this.runtimeExitTraceAttrs.get(runtime)
8891
9328
  });
8892
9329
  logger.info(`[Agent ${agentId}] Process exited with code ${code}${signal ? ` (signal ${signal})` : ""}`);
8893
9330
  });
8894
- proc.on("close", (code, signal) => {
9331
+ runtime.on("close", ({ code, signal }) => {
8895
9332
  if (this.agents.has(agentId)) {
8896
9333
  const ap = this.agents.get(agentId);
8897
- if (ap.process !== proc) return;
9334
+ if (ap.runtime !== runtime) return;
8898
9335
  ap.notifications.clearTimer();
8899
9336
  if (ap.pendingTrajectory?.timer) {
8900
9337
  clearTimeout(ap.pendingTrajectory.timer);
@@ -9035,14 +9472,54 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
9035
9472
  }
9036
9473
  }
9037
9474
  });
9475
+ const startResult = await runtime.start({ text: prompt, sessionId: effectiveConfig.sessionId || null });
9476
+ if (!startResult.ok) {
9477
+ throw new Error(`Runtime session failed to start: ${startResult.reason}${startResult.error ? ` (${startResult.error})` : ""}`);
9478
+ }
9479
+ this.recordDaemonTrace("daemon.agent.spawn.created", {
9480
+ ...this.startQueueTraceAttrs(agentId, effectiveConfig, wakeMessage, unreadSummary, resumePrompt, launchId, wakeMessageTransient),
9481
+ detached: false,
9482
+ new_session: false,
9483
+ process_pid_present: typeof runtime.pid === "number"
9484
+ });
9038
9485
  this.sendAgentStatus(agentId, "active", launchId || null);
9039
9486
  this.broadcastActivity(agentId, "working", "Starting\u2026");
9040
9487
  this.startRuntimeStartupTimeout(agentId, agentProcess);
9041
9488
  } catch (err) {
9042
9489
  this.agentsStarting.delete(agentId);
9490
+ this.cleanupFailedRuntimeStart(agentId, agentProcess, err);
9043
9491
  throw err;
9044
9492
  }
9045
9493
  }
9494
+ cleanupFailedRuntimeStart(agentId, ap, err) {
9495
+ if (!ap) return;
9496
+ if (this.agents.get(agentId) !== ap) return;
9497
+ ap.notifications.clearTimer();
9498
+ if (ap.pendingTrajectory?.timer) {
9499
+ clearTimeout(ap.pendingTrajectory.timer);
9500
+ ap.pendingTrajectory.timer = null;
9501
+ }
9502
+ if (ap.activityHeartbeat) {
9503
+ clearInterval(ap.activityHeartbeat);
9504
+ ap.activityHeartbeat = null;
9505
+ }
9506
+ if (ap.compactionWatchdog) {
9507
+ clearTimeout(ap.compactionWatchdog);
9508
+ ap.compactionWatchdog = null;
9509
+ }
9510
+ this.clearRuntimeStartupTimeout(ap);
9511
+ this.clearStalledRecoverySigtermWatchdog(ap);
9512
+ this.endRuntimeTrace(ap, "error", {
9513
+ outcome: "runtime-start-failed",
9514
+ failure_detail: err instanceof Error ? err.message : String(err),
9515
+ ...runtimeTraceCounterAttrs(ap),
9516
+ ...this.finalizeRuntimeProfileTurnControl(agentId, ap, "runtime_error")
9517
+ });
9518
+ cleanupAgentCredentialProxy(agentId, ap.launchId);
9519
+ this.revokeManagedRunnerCredential(agentId, ap.config, ap.launchId);
9520
+ this.agents.delete(agentId);
9521
+ this.idleAgentConfigs.delete(agentId);
9522
+ }
9046
9523
  async buildSpawnConfig(agentId, config) {
9047
9524
  const baseConfig = config.serverUrl === this.serverUrl ? config : { ...config, serverUrl: this.serverUrl };
9048
9525
  const runnerConfig = await this.ensureManagedRunnerCredential(agentId, baseConfig);
@@ -9211,7 +9688,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
9211
9688
  ap.inbox.push(message);
9212
9689
  if (ap.driver.supportsStdinNotification && ap.sessionId) {
9213
9690
  ap.notifications.add();
9214
- if (ap.driver.busyDeliveryMode === "gated") {
9691
+ if (ap.runtime.descriptor.busyDelivery === "gated") {
9215
9692
  this.recordGatedSteeringEvent(agentId, ap, "buffer", {
9216
9693
  reason: "runtime_profile",
9217
9694
  kind,
@@ -9278,36 +9755,36 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
9278
9755
  cleanupAgentCredentialProxy(agentId, ap.launchId);
9279
9756
  this.revokeManagedRunnerCredential(agentId, ap.config, ap.launchId);
9280
9757
  this.agents.delete(agentId);
9281
- this.processExitTraceAttrs.set(ap.process, {
9758
+ this.runtimeExitTraceAttrs.set(ap.runtime, {
9282
9759
  stop_source: silent ? "daemon_internal" : "explicit_request",
9283
9760
  stop_wait_requested: wait,
9284
9761
  stop_silent: silent
9285
9762
  });
9286
- ap.process.kill("SIGTERM");
9763
+ await ap.runtime.stop({
9764
+ signal: "SIGTERM",
9765
+ forceAfterMs: wait ? 5e3 : void 0,
9766
+ reason: silent ? "daemon_internal" : "explicit_request"
9767
+ });
9287
9768
  if (!silent) {
9288
- this.sendRuntimeProfileReportFor(agentId, ap.config, ap.sessionId, ap.launchId);
9769
+ this.sendRuntimeProfileReportFor(agentId, ap.config, ap.sessionId, ap.launchId, "stop");
9289
9770
  this.sendAgentStatus(agentId, "inactive", ap.launchId);
9290
9771
  this.broadcastActivity(agentId, "offline", "Stopped");
9291
9772
  logger.info(`[Agent ${agentId}] Stopped by request`);
9292
9773
  }
9293
9774
  if (wait) {
9294
9775
  await new Promise((resolve) => {
9295
- const forceKillTimer = setTimeout(() => {
9776
+ const timeoutTimer = setTimeout(() => {
9296
9777
  if (!silent) {
9297
9778
  logger.warn(`[Agent ${agentId}] Stop timed out; force killing`);
9298
9779
  }
9299
- try {
9300
- ap.process.kill("SIGKILL");
9301
- } catch {
9302
- }
9303
9780
  resolve();
9304
9781
  }, 5e3);
9305
- ap.process.on("exit", () => {
9306
- clearTimeout(forceKillTimer);
9782
+ ap.runtime.on("exit", () => {
9783
+ clearTimeout(timeoutTimer);
9307
9784
  resolve();
9308
9785
  });
9309
- if (ap.process.exitCode !== null || ap.process.signalCode !== null) {
9310
- clearTimeout(forceKillTimer);
9786
+ if (ap.runtime.closed) {
9787
+ clearTimeout(timeoutTimer);
9311
9788
  resolve();
9312
9789
  }
9313
9790
  });
@@ -9317,9 +9794,24 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
9317
9794
  if (traceContext.deliveryId) {
9318
9795
  this.deliveryTraceContexts.set(message, traceContext);
9319
9796
  }
9797
+ if (traceContext.transient) {
9798
+ this.deliveryTraceContexts.set(message, traceContext);
9799
+ }
9800
+ const transientDelivery = this.isTransientDelivery(message);
9320
9801
  const ap = this.agents.get(agentId);
9321
9802
  if (!ap) {
9322
9803
  if (this.agentsStarting.has(agentId) || this.queuedAgentStarts.has(agentId)) {
9804
+ if (transientDelivery) {
9805
+ const queuedStart2 = this.queuedAgentStarts.get(agentId);
9806
+ this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
9807
+ outcome: "transient_dropped_during_start",
9808
+ accepted: true,
9809
+ process_present: false,
9810
+ startup_pending: true,
9811
+ launchId: queuedStart2?.launchId
9812
+ }));
9813
+ return true;
9814
+ }
9323
9815
  const queuedStart = this.queuedAgentStarts.get(agentId);
9324
9816
  const pending = this.startingInboxes.get(agentId) || [];
9325
9817
  pending.push(message);
@@ -9337,7 +9829,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
9337
9829
  const cached = this.idleAgentConfigs.get(agentId);
9338
9830
  if (cached) {
9339
9831
  const driver = this.driverResolver(cached.config.runtime || "claude");
9340
- if (this.shouldDeferWakeMessage(agentId, driver, message)) {
9832
+ if (!transientDelivery && this.shouldDeferWakeMessage(agentId, driver, message)) {
9341
9833
  this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
9342
9834
  outcome: "deferred_wake_message",
9343
9835
  accepted: true,
@@ -9360,7 +9852,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
9360
9852
  session_id_present: Boolean(cached.sessionId),
9361
9853
  launchId: cached.launchId || void 0
9362
9854
  }));
9363
- return this.startAgent(agentId, cached.config, message, void 0, void 0, cached.launchId || void 0).then(() => true, (err) => {
9855
+ return this.startAgent(agentId, cached.config, message, void 0, void 0, cached.launchId || void 0, transientDelivery).then(() => true, (err) => {
9364
9856
  logger.error(`[Agent ${agentId}] Failed to auto-restart`, err);
9365
9857
  if (this.reportRunnerCredentialMintFailure(agentId, err, cached.launchId, "idle_auto_restart")) {
9366
9858
  return false;
@@ -9370,6 +9862,15 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
9370
9862
  });
9371
9863
  }
9372
9864
  logger.warn(`[Agent ${agentId}] Delivery received but no running process or cached idle config exists`);
9865
+ if (transientDelivery) {
9866
+ this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
9867
+ outcome: "transient_dropped_no_process",
9868
+ accepted: true,
9869
+ process_present: false,
9870
+ cached_idle_config_present: false
9871
+ }));
9872
+ return true;
9873
+ }
9373
9874
  this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
9374
9875
  outcome: "rejected_no_process",
9375
9876
  accepted: false,
@@ -9380,7 +9881,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
9380
9881
  this.broadcastActivity(agentId, "offline", "Process unavailable; restart required");
9381
9882
  return false;
9382
9883
  }
9383
- if (this.shouldDeferWakeMessage(agentId, ap.driver, message)) {
9884
+ if (!transientDelivery && this.shouldDeferWakeMessage(agentId, ap.driver, message)) {
9384
9885
  this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
9385
9886
  outcome: "deferred_wake_message",
9386
9887
  accepted: true,
@@ -9395,6 +9896,19 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
9395
9896
  }
9396
9897
  const stickyTerminalFailure = classifyStickyTerminalFailure(ap);
9397
9898
  if (stickyTerminalFailure) {
9899
+ if (transientDelivery) {
9900
+ this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
9901
+ outcome: "transient_dropped_terminal_runtime_error",
9902
+ accepted: true,
9903
+ process_present: true,
9904
+ runtime: ap.config.runtime,
9905
+ session_id_present: Boolean(ap.sessionId),
9906
+ launchId: ap.launchId || void 0,
9907
+ is_idle: ap.isIdle,
9908
+ inbox_count: ap.inbox.length
9909
+ }));
9910
+ return true;
9911
+ }
9398
9912
  ap.inbox.push(message);
9399
9913
  this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
9400
9914
  outcome: "queued_terminal_runtime_error",
@@ -9411,6 +9925,30 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
9411
9925
  return true;
9412
9926
  }
9413
9927
  if (ap.isIdle && ap.driver.supportsStdinNotification && ap.sessionId) {
9928
+ if (transientDelivery) {
9929
+ this.commitApmIdleState(agentId, ap, false);
9930
+ this.startRuntimeTrace(agentId, ap, "stdin-idle-delivery", [message]);
9931
+ this.broadcastActivity(agentId, "working", "Message received");
9932
+ const stdinAccepted2 = this.deliverMessagesViaStdin(
9933
+ agentId,
9934
+ ap,
9935
+ [message],
9936
+ "idle",
9937
+ { transient: true }
9938
+ );
9939
+ this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
9940
+ outcome: "stdin_idle_transient_delivery",
9941
+ accepted: true,
9942
+ process_present: true,
9943
+ runtime: ap.config.runtime,
9944
+ session_id_present: true,
9945
+ launchId: ap.launchId || void 0,
9946
+ stdin_delivery_accepted: stdinAccepted2,
9947
+ delivered_messages_count: 1,
9948
+ inbox_count: ap.inbox.length
9949
+ }));
9950
+ return true;
9951
+ }
9414
9952
  ap.inbox.push(message);
9415
9953
  const nextMessages = [...ap.inbox];
9416
9954
  this.commitApmIdleState(agentId, ap, false);
@@ -9435,6 +9973,19 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
9435
9973
  }));
9436
9974
  return true;
9437
9975
  }
9976
+ if (transientDelivery) {
9977
+ this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
9978
+ outcome: "transient_dropped_busy",
9979
+ accepted: true,
9980
+ process_present: true,
9981
+ runtime: ap.config.runtime,
9982
+ session_id_present: Boolean(ap.sessionId),
9983
+ launchId: ap.launchId || void 0,
9984
+ is_idle: ap.isIdle,
9985
+ inbox_count: ap.inbox.length
9986
+ }));
9987
+ return true;
9988
+ }
9438
9989
  ap.inbox.push(message);
9439
9990
  if (this.recoverStaleProcessForQueuedMessageIfNeeded(agentId, ap)) {
9440
9991
  this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
@@ -9475,7 +10026,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
9475
10026
  if (ap.gatedSteering.compacting) {
9476
10027
  ap.notifications.add();
9477
10028
  ap.notifications.clearTimer();
9478
- if (ap.driver.busyDeliveryMode === "gated") {
10029
+ if (ap.runtime.descriptor.busyDelivery === "gated") {
9479
10030
  this.recordGatedSteeringEvent(agentId, ap, "buffer", {
9480
10031
  reason: "compaction_boundary",
9481
10032
  pendingMessages: ap.inbox.length
@@ -9500,7 +10051,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
9500
10051
  }));
9501
10052
  return true;
9502
10053
  }
9503
- if (ap.driver.busyDeliveryMode === "gated") {
10054
+ if (ap.runtime.descriptor.busyDelivery === "gated") {
9504
10055
  ap.notifications.add();
9505
10056
  if (!ap.notifications.hasTimer) {
9506
10057
  this.scheduleStdinNotification(agentId, ap, STDIN_NOTIFICATION_INITIAL_DELAY_MS);
@@ -9683,7 +10234,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
9683
10234
  traceparent: formatTraceparent(span.context)
9684
10235
  };
9685
10236
  const ap = this.agents.get(agentId);
9686
- if (ap && !(ap.sessionId && ap.driver.supportsStdinNotification && ap.isIdle) && !(ap.sessionId && ap.driver.busyDeliveryMode === "direct")) {
10237
+ if (ap && !(ap.sessionId && ap.driver.supportsStdinNotification && ap.isIdle) && !(ap.sessionId && ap.runtime.descriptor.busyDelivery === "direct")) {
9687
10238
  this.enqueueRuntimeProfileNotification(agentId, ap, message, kind, key);
9688
10239
  span.end("ok", {
9689
10240
  attrs: {
@@ -9713,7 +10264,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
9713
10264
  });
9714
10265
  return written;
9715
10266
  }
9716
- if (ap?.sessionId && ap.driver.busyDeliveryMode === "direct") {
10267
+ if (ap?.sessionId && ap.runtime.descriptor.busyDelivery === "direct") {
9717
10268
  const written = this.deliverMessagesViaStdin(agentId, ap, [message], "busy");
9718
10269
  span.end(written ? "ok" : "error", {
9719
10270
  attrs: {
@@ -9818,7 +10369,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
9818
10369
  });
9819
10370
  span.end("ok", { attrs: { outcome: "agent_config_ack_sent" } });
9820
10371
  }
9821
- sendRuntimeProfileWireReport(report) {
10372
+ sendRuntimeProfileWireReport(report, source) {
9822
10373
  const span = this.tracer.startSpan("daemon.runtime_profile.report.sent", {
9823
10374
  surface: "daemon",
9824
10375
  kind: "producer",
@@ -9826,6 +10377,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
9826
10377
  agentId: report.agentId,
9827
10378
  launchId: report.launchId || void 0,
9828
10379
  runtime: report.facts.runtime,
10380
+ report_source: source,
9829
10381
  model_present: Boolean(report.facts.model),
9830
10382
  session_ref_present: Boolean(report.facts.sessionRef),
9831
10383
  workspace_ref_present: Boolean(report.facts.workspaceRef || report.facts.workspacePathRef)
@@ -9836,17 +10388,18 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
9836
10388
  agentId: report.agentId,
9837
10389
  facts: report.facts,
9838
10390
  launchId: report.launchId || void 0,
9839
- traceparent: formatTraceparent(span.context)
10391
+ traceparent: formatTraceparent(span.context),
10392
+ source
9840
10393
  });
9841
10394
  span.end("ok");
9842
10395
  }
9843
- sendRuntimeProfileReportFor(agentId, config, sessionId, launchId) {
9844
- this.sendRuntimeProfileWireReport(this.buildRuntimeProfileReport(agentId, config, sessionId, launchId));
10396
+ sendRuntimeProfileReportFor(agentId, config, sessionId, launchId, source) {
10397
+ this.sendRuntimeProfileWireReport(this.buildRuntimeProfileReport(agentId, config, sessionId, launchId), source);
9845
10398
  }
9846
- sendRuntimeProfileReport(agentId) {
10399
+ sendRuntimeProfileReport(agentId, source) {
9847
10400
  const report = this.getAgentRuntimeProfileReport(agentId);
9848
10401
  if (!report) return;
9849
- this.sendRuntimeProfileWireReport(report);
10402
+ this.sendRuntimeProfileWireReport(report, source);
9850
10403
  }
9851
10404
  // Machine-level workspace scanning
9852
10405
  async scanAllWorkspaces() {
@@ -10136,23 +10689,23 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
10136
10689
  clearTimeout(ap.stalledRecoverySigtermTimer);
10137
10690
  ap.stalledRecoverySigtermTimer = null;
10138
10691
  }
10139
- mergeProcessExitTraceAttrs(proc, attrs) {
10140
- this.processExitTraceAttrs.set(proc, {
10141
- ...this.processExitTraceAttrs.get(proc) ?? {},
10692
+ mergeRuntimeExitTraceAttrs(runtime, attrs) {
10693
+ this.runtimeExitTraceAttrs.set(runtime, {
10694
+ ...this.runtimeExitTraceAttrs.get(runtime) ?? {},
10142
10695
  ...attrs
10143
10696
  });
10144
10697
  }
10145
10698
  startStalledRecoverySigtermWatchdog(agentId, ap, runtimeLabel, queuedMessagesAtSignal, staleForMs) {
10146
10699
  this.clearStalledRecoverySigtermWatchdog(ap);
10147
10700
  const timeoutMs = stalledRecoverySigtermTimeoutMs();
10148
- const processAtSignal = ap.process;
10701
+ const runtimeAtSignal = ap.runtime;
10149
10702
  ap.stalledRecoverySigtermTimer = setTimeout(() => {
10150
10703
  ap.stalledRecoverySigtermTimer = null;
10151
10704
  const current = this.agents.get(agentId);
10152
- if (!current || current !== ap || current.process !== processAtSignal || current.expectedTerminationReason !== "stalled_recovery") {
10705
+ if (!current || current !== ap || current.runtime !== runtimeAtSignal || current.expectedTerminationReason !== "stalled_recovery") {
10153
10706
  return;
10154
10707
  }
10155
- this.mergeProcessExitTraceAttrs(processAtSignal, {
10708
+ this.mergeRuntimeExitTraceAttrs(runtimeAtSignal, {
10156
10709
  stalled_recovery_sigterm_timeout: true,
10157
10710
  stalled_recovery_sigterm_timeout_ms: timeoutMs
10158
10711
  });
@@ -10166,7 +10719,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
10166
10719
  queued_messages_at_signal: queuedMessagesAtSignal,
10167
10720
  stale_age_ms_at_signal: staleForMs,
10168
10721
  timeout_ms: timeoutMs,
10169
- process_pid_present: typeof processAtSignal.pid === "number",
10722
+ process_pid_present: typeof runtimeAtSignal.pid === "number",
10170
10723
  session_id_present: Boolean(current.sessionId),
10171
10724
  supports_stdin_notification: current.driver.supportsStdinNotification,
10172
10725
  busy_delivery_mode: current.driver.busyDeliveryMode
@@ -10175,7 +10728,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
10175
10728
  `[Agent ${agentId}] Stalled ${runtimeLabel} runtime did not exit after SIGTERM within ${timeoutMs}ms; force killing`
10176
10729
  );
10177
10730
  try {
10178
- processAtSignal.kill("SIGKILL");
10731
+ void runtimeAtSignal.stop({ signal: "SIGKILL", reason: "stalled_recovery_sigterm_timeout" });
10179
10732
  } catch (err) {
10180
10733
  const reason = err instanceof Error ? err.message : String(err);
10181
10734
  this.recordDaemonTrace("daemon.agent.stalled_recovery.sigkill_failed", {
@@ -10361,7 +10914,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
10361
10914
  ap.runtimeProgress.noteRuntimeEvent(eventKind);
10362
10915
  }
10363
10916
  recordGatedSteeringEvent(agentId, ap, event, attrs = {}) {
10364
- if (ap.driver.busyDeliveryMode !== "gated") return;
10917
+ if (ap.runtime.descriptor.busyDelivery !== "gated") return;
10365
10918
  const reduction = reduceApmGatedRecentEvent(ap.gatedSteering, { event });
10366
10919
  this.commitGatedSteeringDecisionState(agentId, ap, reduction.nextState);
10367
10920
  this.recordRuntimeTraceEvent(agentId, ap, `runtime.gated_steering.${event}`, {
@@ -10396,7 +10949,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
10396
10949
  }
10397
10950
  notifyGatedSteeringBoundary(agentId, ap, reason) {
10398
10951
  const readiness = reduceApmGatedFlushReadiness(ap.gatedSteering, {
10399
- isGated: ap.driver.busyDeliveryMode === "gated",
10952
+ isGated: ap.runtime.descriptor.busyDelivery === "gated",
10400
10953
  hasSession: Boolean(ap.sessionId),
10401
10954
  inboxLength: ap.inbox.length,
10402
10955
  reason
@@ -10446,7 +10999,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
10446
10999
  });
10447
11000
  return true;
10448
11001
  }
10449
- if (ap.driver.busyDeliveryMode === "gated") {
11002
+ if (ap.runtime.descriptor.busyDelivery === "gated") {
10450
11003
  const flushReduction = reduceApmGatedFlush(ap.gatedSteering, { reason: effect.reason });
10451
11004
  this.commitGatedSteeringDecisionState(agentId, ap, flushReduction.nextState);
10452
11005
  this.recordGatedSteeringEvent(agentId, ap, "flush", {
@@ -10548,12 +11101,12 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
10548
11101
  this.sendAgentStatus(agentId, "inactive", ap.launchId);
10549
11102
  this.idleAgentConfigs.delete(agentId);
10550
11103
  try {
10551
- this.processExitTraceAttrs.set(ap.process, {
11104
+ this.runtimeExitTraceAttrs.set(ap.runtime, {
10552
11105
  stop_source: "startup_timeout",
10553
11106
  expectedTerminationReason: "startup_timeout",
10554
11107
  timeout_ms: timeoutMs
10555
11108
  });
10556
- ap.process.kill("SIGTERM");
11109
+ void ap.runtime.stop({ signal: "SIGTERM", reason: "startup_timeout" });
10557
11110
  } catch (err) {
10558
11111
  const reason = err instanceof Error ? err.message : String(err);
10559
11112
  logger.warn(`[Agent ${agentId}] Failed to terminate startup-timed-out ${ap.driver.id} process: ${reason}`);
@@ -10639,13 +11192,13 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
10639
11192
  );
10640
11193
  this.broadcastActivity(agentId, "working", `Restarting stalled ${runtimeLabel} runtime for queued message`);
10641
11194
  try {
10642
- this.processExitTraceAttrs.set(ap.process, {
11195
+ this.runtimeExitTraceAttrs.set(ap.runtime, {
10643
11196
  stop_source: "stalled_recovery",
10644
11197
  expectedTerminationReason: "stalled_recovery",
10645
11198
  queued_messages_count: ap.inbox.length
10646
11199
  });
10647
11200
  this.startStalledRecoverySigtermWatchdog(agentId, ap, runtimeLabel, ap.inbox.length, staleForMs);
10648
- ap.process.kill("SIGTERM");
11201
+ void ap.runtime.stop({ signal: "SIGTERM", reason: "stalled_recovery" });
10649
11202
  } catch (err) {
10650
11203
  this.clearStalledRecoverySigtermWatchdog(ap);
10651
11204
  const reason = err instanceof Error ? err.message : String(err);
@@ -10695,7 +11248,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
10695
11248
  case "session_init":
10696
11249
  if (ap) ap.sessionId = event.sessionId;
10697
11250
  this.sendToServer({ type: "agent:session", agentId, sessionId: event.sessionId, launchId: ap?.launchId || void 0 });
10698
- this.sendRuntimeProfileReport(agentId);
11251
+ this.sendRuntimeProfileReport(agentId, "session_init");
10699
11252
  break;
10700
11253
  case "thinking": {
10701
11254
  this.completeCompactionIfActive(agentId, "Context compaction finished (inferred from resumed output)");
@@ -10821,11 +11374,11 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
10821
11374
  if (ap.driver.terminateProcessOnTurnEnd) {
10822
11375
  logger.info(`[Agent ${agentId}] Turn completed; terminating ${ap.driver.id} process`);
10823
11376
  try {
10824
- this.processExitTraceAttrs.set(ap.process, {
11377
+ this.runtimeExitTraceAttrs.set(ap.runtime, {
10825
11378
  stop_source: "turn_end",
10826
11379
  expectedTerminationReason: "turn_end"
10827
11380
  });
10828
- ap.process.kill("SIGTERM");
11381
+ void ap.runtime.stop({ signal: "SIGTERM", reason: "turn_end" });
10829
11382
  } catch (err) {
10830
11383
  const reason = err instanceof Error ? err.message : String(err);
10831
11384
  logger.warn(`[Agent ${agentId}] Failed to terminate ${ap.driver.id} after turn_end: ${reason}`);
@@ -10834,7 +11387,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
10834
11387
  }
10835
11388
  if (event.sessionId) {
10836
11389
  this.sendToServer({ type: "agent:session", agentId, sessionId: event.sessionId, launchId: ap?.launchId || void 0 });
10837
- this.sendRuntimeProfileReport(agentId);
11390
+ this.sendRuntimeProfileReport(agentId, "turn_end");
10838
11391
  }
10839
11392
  break;
10840
11393
  case "error": {
@@ -10847,7 +11400,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
10847
11400
  if (runtimeErrorDiagnostics.spanAttrs.runtime_error_action_required === true) {
10848
11401
  visibleErrorMessage = formatRuntimeLoginRequiredMessage(ap.driver.id);
10849
11402
  }
10850
- const shouldDisableToolBoundaryFlush = ap.driver.busyDeliveryMode === "gated" && this.isThinkingBlockMutationError(event.message);
11403
+ const shouldDisableToolBoundaryFlush = ap.runtime.descriptor.busyDelivery === "gated" && this.isThinkingBlockMutationError(event.message);
10851
11404
  const terminalFailure = classifyTerminalFailure(ap);
10852
11405
  const reduction = reduceApmGatedError(ap.gatedSteering, {
10853
11406
  disableToolBoundaryFlush: shouldDisableToolBoundaryFlush,
@@ -10878,11 +11431,11 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
10878
11431
  if (terminalFailure.actionRequired) {
10879
11432
  logger.warn(`[Agent ${agentId}] ${ap.driver.id} auth requires user action; terminating runtime process`);
10880
11433
  try {
10881
- this.processExitTraceAttrs.set(ap.process, {
11434
+ this.runtimeExitTraceAttrs.set(ap.runtime, {
10882
11435
  stop_source: "runtime_auth_error",
10883
11436
  runtime_error_class: "AuthError"
10884
11437
  });
10885
- ap.process.kill("SIGTERM");
11438
+ void ap.runtime.stop({ signal: "SIGTERM", reason: "runtime_auth_error" });
10886
11439
  } catch (err) {
10887
11440
  const reason = err instanceof Error ? err.message : String(err);
10888
11441
  logger.warn(`[Agent ${agentId}] Failed to terminate ${ap.driver.id} after auth error: ${reason}`);
@@ -10907,8 +11460,10 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
10907
11460
  recordRuntimeTelemetry(agentId, ap, event) {
10908
11461
  const sessionId = ap.driver.currentSessionId ?? event.sessionId;
10909
11462
  const payloadAttrs = sanitizeRuntimeTelemetryPayloadAttrs(event.attrs);
11463
+ const versionAttrs = this.runtimeTelemetryVersionAttrs();
10910
11464
  const telemetryAttrs = {
10911
11465
  ...payloadAttrs,
11466
+ ...versionAttrs,
10912
11467
  ...event.source ? { source: event.source } : {},
10913
11468
  ...event.usageKind ? { usageKind: event.usageKind } : {},
10914
11469
  ...sessionId ? { sessionId } : {},
@@ -10926,6 +11481,24 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
10926
11481
  ap.runtimeTraceSpan?.addEvent(`runtime.telemetry.${event.name}`, telemetryAttrs);
10927
11482
  this.recordDaemonTrace(`daemon.runtime.telemetry.${event.name}`, attrs);
10928
11483
  }
11484
+ runtimeTelemetryVersionAttrs() {
11485
+ return {
11486
+ ...this.daemonVersion ? {
11487
+ daemonVersion: this.daemonVersion,
11488
+ daemon_version: this.daemonVersion,
11489
+ daemon_version_present: true
11490
+ } : {
11491
+ daemon_version_present: false
11492
+ },
11493
+ ...this.computerVersion ? {
11494
+ computerVersion: this.computerVersion,
11495
+ computer_version: this.computerVersion,
11496
+ computer_version_present: true
11497
+ } : {
11498
+ computer_version_present: false
11499
+ }
11500
+ };
11501
+ }
10929
11502
  sendAgentStatus(agentId, status, launchId) {
10930
11503
  this.sendToServer({ type: "agent:status", agentId, status, launchId: launchId || void 0 });
10931
11504
  }
@@ -11009,9 +11582,11 @@ ${formatAgentInboxDelta(inboxRows, { totalPendingMessages: inboxCount })}]`;
11009
11582
  ...projectionAttrs
11010
11583
  });
11011
11584
  logger.info(`[Agent ${agentId}] Sending stdin inbox update: ${inboxRows.length} changed target(s), ${inboxCount} pending message(s)`);
11012
- const encoded = ap.driver.encodeStdinMessage(notification, ap.sessionId, { mode: "busy" });
11013
- if (encoded) {
11014
- ap.process.stdin?.write(encoded + "\n");
11585
+ const sendResult = requireImmediateRuntimeSendResult(
11586
+ ap.runtime.send({ mode: "busy", text: notification, sessionId: ap.sessionId }),
11587
+ "busy inbox notification"
11588
+ );
11589
+ if (sendResult.ok) {
11015
11590
  this.recordDaemonTrace("daemon.agent.inbox_update.pushed", {
11016
11591
  agentId,
11017
11592
  runtime: ap.config.runtime,
@@ -11036,14 +11611,17 @@ ${formatAgentInboxDelta(inboxRows, { totalPendingMessages: inboxCount })}]`;
11036
11611
  return true;
11037
11612
  } else {
11038
11613
  ap.notifications.add(count);
11039
- const retryScheduled = ap.driver.busyDeliveryMode === "direct" ? this.scheduleStdinNotification(agentId, ap, this.stdinNotificationRetryMs) : false;
11614
+ const retryScheduled = ap.runtime.descriptor.busyDelivery === "direct" ? this.scheduleStdinNotification(agentId, ap, this.stdinNotificationRetryMs) : false;
11615
+ const outcome = runtimeSendFailureOutcome(sendResult);
11040
11616
  this.recordDaemonTrace("daemon.agent.stdin_notification", {
11041
11617
  agentId,
11042
11618
  runtime: ap.config.runtime,
11043
11619
  model: ap.config.model,
11044
11620
  launchId: ap.launchId || void 0,
11045
- outcome: "encode_failed",
11621
+ outcome,
11046
11622
  mode: "busy",
11623
+ failure_reason: sendResult.reason,
11624
+ failure_error: sendResult.error,
11047
11625
  pending_notification_count: count,
11048
11626
  retry_scheduled: retryScheduled,
11049
11627
  notification_timer_present: ap.notifications.hasTimer,
@@ -11094,8 +11672,11 @@ ${formatAgentInboxDelta(inboxRows, { totalPendingMessages: inboxCount })}]`;
11094
11672
  nativeStandingPrompt: Boolean(ap.driver.supportsNativeStandingPrompt)
11095
11673
  });
11096
11674
  this.recordRuntimeTraceEvent(agentId, ap, "runtime.input.prepared", inputTraceAttrs);
11097
- const encoded = ap.driver.encodeStdinMessage(prompt, ap.sessionId, { mode });
11098
- if (!encoded) {
11675
+ const sendResult = requireImmediateRuntimeSendResult(
11676
+ ap.runtime.send({ mode, text: prompt, sessionId: ap.sessionId }),
11677
+ `${mode} inbox update`
11678
+ );
11679
+ if (!sendResult.ok) {
11099
11680
  if (mode === "idle") {
11100
11681
  this.commitApmIdleState(agentId, ap, true);
11101
11682
  }
@@ -11117,7 +11698,9 @@ ${formatAgentInboxDelta(inboxRows, { totalPendingMessages: inboxCount })}]`;
11117
11698
  ...this.messagesTraceAttrs(messages),
11118
11699
  ...inputTraceAttrs,
11119
11700
  ...projectionAttrs,
11120
- outcome: "encode_failed",
11701
+ outcome: runtimeSendFailureOutcome(sendResult),
11702
+ failure_reason: sendResult.reason,
11703
+ failure_error: sendResult.error,
11121
11704
  requeued_messages_count: 0,
11122
11705
  cursors_advanced: "none"
11123
11706
  }, "error");
@@ -11130,7 +11713,6 @@ ${formatAgentInboxDelta(inboxRows, { totalPendingMessages: inboxCount })}]`;
11130
11713
  if (this.containsOrdinaryInboxMessage(messages)) {
11131
11714
  ap.lastRuntimeError = null;
11132
11715
  }
11133
- ap.process.stdin?.write(encoded + "\n");
11134
11716
  this.recordDaemonTrace("daemon.agent.stdin_delivery", {
11135
11717
  agentId,
11136
11718
  launchId: ap.launchId || void 0,
@@ -11153,7 +11735,7 @@ ${formatAgentInboxDelta(inboxRows, { totalPendingMessages: inboxCount })}]`;
11153
11735
  return true;
11154
11736
  }
11155
11737
  /** Deliver a message to an agent via stdin, formatting it the same way as the MCP bridge */
11156
- deliverMessagesViaStdin(agentId, ap, messages, mode) {
11738
+ deliverMessagesViaStdin(agentId, ap, messages, mode, options = {}) {
11157
11739
  if (messages.length === 0) return true;
11158
11740
  const runtimeProfileMigrationMessages = messages.filter((message) => runtimeProfileNotificationFromMessage(message)?.kind === "migration");
11159
11741
  if (runtimeProfileMigrationMessages.length > 0) {
@@ -11191,8 +11773,10 @@ ${formatAgentInboxDelta(inboxRows, { totalPendingMessages: inboxCount })}]`;
11191
11773
  pending_notification_count: ap.notifications.pendingCount,
11192
11774
  busy_delivery_mode: ap.driver.busyDeliveryMode,
11193
11775
  supports_stdin_notification: ap.driver.supportsStdinNotification,
11776
+ transient_delivery: options.transient === true,
11194
11777
  ...this.messagesTraceAttrs(messages)
11195
11778
  };
11779
+ const traceSource = options.transient ? `stdin_${mode}_transient_delivery` : `stdin_${mode}_delivery`;
11196
11780
  const prompt = formatRuntimeProfileControlPrompt(messages) ?? (messages.length === 1 ? `New message received:
11197
11781
 
11198
11782
  ${formatIncomingMessage(messages[0], ap.driver)}
@@ -11205,16 +11789,21 @@ ${messages.map((message) => formatIncomingMessage(message, ap.driver)).join("\n"
11205
11789
  Respond as appropriate. Complete all your work before stopping.
11206
11790
  ${RESPONSE_TARGET_HINT}`);
11207
11791
  const inputTraceAttrs = buildRuntimeInputTraceAttrs({
11208
- source: `stdin_${mode}_delivery`,
11792
+ source: traceSource,
11209
11793
  prompt,
11210
11794
  messages,
11211
11795
  sessionIdPresent: Boolean(ap.sessionId),
11212
11796
  nativeStandingPrompt: Boolean(ap.driver.supportsNativeStandingPrompt)
11213
11797
  });
11214
11798
  this.recordRuntimeTraceEvent(agentId, ap, "runtime.input.prepared", inputTraceAttrs);
11215
- const encoded = ap.driver.encodeStdinMessage(prompt, ap.sessionId, { mode });
11216
- if (!encoded) {
11217
- ap.inbox.unshift(...messages);
11799
+ const sendResult = requireImmediateRuntimeSendResult(
11800
+ ap.runtime.send({ mode, text: prompt, sessionId: ap.sessionId }),
11801
+ `${mode} message delivery`
11802
+ );
11803
+ if (!sendResult.ok) {
11804
+ if (!options.transient) {
11805
+ ap.inbox.unshift(...messages);
11806
+ }
11218
11807
  if (mode === "idle") {
11219
11808
  this.commitApmIdleState(agentId, ap, true);
11220
11809
  }
@@ -11224,12 +11813,16 @@ ${RESPONSE_TARGET_HINT}`);
11224
11813
  this.recordDaemonTrace("daemon.agent.stdin_delivery", {
11225
11814
  ...traceAttrs,
11226
11815
  ...inputTraceAttrs,
11227
- outcome: "encode_failed",
11228
- requeued_messages_count: messages.length
11816
+ outcome: runtimeSendFailureOutcome(sendResult),
11817
+ failure_reason: sendResult.reason,
11818
+ failure_error: sendResult.error,
11819
+ requeued_messages_count: options.transient ? 0 : messages.length
11229
11820
  }, "error");
11230
11821
  return false;
11231
11822
  }
11232
- this.consumeVisibleMessages(agentId, { messages, source: `stdin_${mode}_delivery` });
11823
+ if (!options.transient) {
11824
+ this.consumeVisibleMessages(agentId, { messages, source: traceSource });
11825
+ }
11233
11826
  const senders = [...new Set(messages.map((message) => `@${message.sender_name}`))].join(", ");
11234
11827
  logger.info(
11235
11828
  `[Agent ${agentId}] Delivering ${mode} ${messages.length === 1 ? "message" : `${messages.length} messages`} via stdin from ${senders}`
@@ -11237,7 +11830,6 @@ ${RESPONSE_TARGET_HINT}`);
11237
11830
  if (this.containsOrdinaryInboxMessage(messages)) {
11238
11831
  ap.lastRuntimeError = null;
11239
11832
  }
11240
- ap.process.stdin?.write(encoded + "\n");
11241
11833
  this.ackInjectedRuntimeProfileMessages(agentId, messages, ap.launchId);
11242
11834
  this.recordDaemonTrace("daemon.agent.stdin_delivery", {
11243
11835
  ...traceAttrs,
@@ -12205,7 +12797,7 @@ var DEFAULT_TRACE_UPLOAD_URL = "https://slock-trace-upload.botiverse.dev";
12205
12797
  var RUNNER_CREDENTIAL_SCOPES = ["send", "read", "mentions", "tasks", "reactions", "server", "channels", "knowledge"];
12206
12798
  var RUNNER_CREDENTIAL_MINT_MAX_ATTEMPTS2 = 3;
12207
12799
  var RUNNER_CREDENTIAL_MINT_RETRY_DELAY_MS2 = 250;
12208
- var DAEMON_CLI_USAGE = "Usage: slock-daemon --server-url <url> --api-key <key>";
12800
+ var DAEMON_CLI_USAGE = `Usage: slock-daemon --server-url <url> (--api-key <key> or ${DAEMON_API_KEY_ENV}=<key>)`;
12209
12801
  var RunnerCredentialMintError2 = class extends Error {
12210
12802
  code;
12211
12803
  retryable;
@@ -12241,9 +12833,9 @@ function runnerCredentialErrorDetail2(error) {
12241
12833
  async function waitForRunnerCredentialRetry2() {
12242
12834
  await new Promise((resolve) => setTimeout(resolve, RUNNER_CREDENTIAL_MINT_RETRY_DELAY_MS2));
12243
12835
  }
12244
- function parseDaemonCliArgs(args) {
12836
+ function parseDaemonCliArgs(args, env = {}) {
12245
12837
  let serverUrl = "";
12246
- let apiKey = "";
12838
+ let apiKey = env[DAEMON_API_KEY_ENV] ?? "";
12247
12839
  for (let i = 0; i < args.length; i++) {
12248
12840
  if (args[i] === "--server-url" && args[i + 1]) serverUrl = args[++i];
12249
12841
  if (args[i] === "--api-key" && args[i + 1]) apiKey = args[++i];
@@ -12444,7 +13036,9 @@ var DaemonCore = class {
12444
13036
  serverUrl: options.serverUrl,
12445
13037
  defaultAgentEnvVarsProvider: options.defaultAgentEnvVarsProvider,
12446
13038
  slockCliPath: this.slockCliPath,
12447
- tracer: this.tracer
13039
+ tracer: this.tracer,
13040
+ daemonVersion: this.daemonVersion,
13041
+ computerVersion: this.computerVersion
12448
13042
  };
12449
13043
  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);
12450
13044
  const connectionFactory = options.connectionFactory ?? ((connOptions) => new DaemonConnection(connOptions));
@@ -12695,7 +13289,8 @@ var DaemonCore = class {
12695
13289
  msg.wakeMessage,
12696
13290
  msg.unreadSummary,
12697
13291
  msg.resumePrompt,
12698
- msg.launchId
13292
+ msg.launchId,
13293
+ msg.wakeMessageTransient ?? false
12699
13294
  );
12700
13295
  }
12701
13296
  handleMessage(msg) {
@@ -12746,7 +13341,10 @@ var DaemonCore = class {
12746
13341
  logger.info(`[Agent ${msg.agentId}] Delivery received (seq=${msg.seq}, from=@${msg.message.sender_name}, target=${formatChannelTarget(msg)})`);
12747
13342
  try {
12748
13343
  span.addEvent("daemon.receive", { seq: msg.seq, deliveryId: msg.deliveryId });
12749
- const acceptedOrPromise = this.agentManager.deliverMessage(msg.agentId, msg.message, { deliveryId: msg.deliveryId });
13344
+ const acceptedOrPromise = this.agentManager.deliverMessage(msg.agentId, msg.message, {
13345
+ deliveryId: msg.deliveryId,
13346
+ transient: msg.transient ?? false
13347
+ });
12750
13348
  Promise.resolve(acceptedOrPromise).then((accepted) => {
12751
13349
  span.addEvent("daemon.deliver_to_agent_manager", { accepted });
12752
13350
  if (!accepted) {
@@ -12976,6 +13574,7 @@ var DaemonCore = class {
12976
13574
  agentId: report.agentId,
12977
13575
  launchId: report.launchId || void 0,
12978
13576
  runtime: report.facts.runtime,
13577
+ report_source: "connect",
12979
13578
  model_present: Boolean(report.facts.model),
12980
13579
  session_ref_present: Boolean(report.facts.sessionRef),
12981
13580
  workspace_ref_present: Boolean(report.facts.workspaceRef || report.facts.workspacePathRef)
@@ -12986,7 +13585,8 @@ var DaemonCore = class {
12986
13585
  agentId: report.agentId,
12987
13586
  facts: report.facts,
12988
13587
  launchId: report.launchId || void 0,
12989
- traceparent: formatTraceparent(span.context)
13588
+ traceparent: formatTraceparent(span.context),
13589
+ source: "connect"
12990
13590
  });
12991
13591
  span.end("ok");
12992
13592
  }
@@ -13010,6 +13610,8 @@ var DaemonCore = class {
13010
13610
  };
13011
13611
 
13012
13612
  export {
13613
+ DAEMON_API_KEY_ENV,
13614
+ scrubDaemonAuthEnv,
13013
13615
  resolveWorkspaceDirectoryPath,
13014
13616
  scanWorkspaceDirectories,
13015
13617
  deleteWorkspaceDirectory,