@slock-ai/daemon 0.51.1 → 0.52.1-play.20260520171228

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.
@@ -7,11 +7,11 @@ import {
7
7
  } from "./chunk-KNMCE6WB.js";
8
8
 
9
9
  // src/core.ts
10
- import path16 from "path";
10
+ import path17 from "path";
11
11
  import os8 from "os";
12
12
  import { createRequire } from "module";
13
13
  import { accessSync } from "fs";
14
- import { fileURLToPath } from "url";
14
+ import { fileURLToPath as fileURLToPath2 } from "url";
15
15
 
16
16
  // ../shared/src/tracing/index.ts
17
17
  var DEFAULT_TRACE_FLAGS = "00";
@@ -723,6 +723,7 @@ var SERVER_CAPABILITY_MATRIX = {
723
723
  var RUNTIMES = [
724
724
  { id: "claude", displayName: "Claude Code", binary: "claude", supported: true },
725
725
  { id: "codex", displayName: "Codex CLI", binary: "codex", supported: true },
726
+ { id: "pi", displayName: "Pi", binary: "pi", supported: true },
726
727
  { id: "kimi", displayName: "Kimi CLI", binary: "kimi", supported: true },
727
728
  { id: "copilot", displayName: "Copilot CLI", binary: "copilot", supported: true },
728
729
  { id: "cursor", displayName: "Cursor CLI", binary: "cursor-agent", supported: true },
@@ -764,10 +765,10 @@ var DISPLAY_PLAN_CONFIG = {
764
765
  };
765
766
 
766
767
  // src/agentProcessManager.ts
767
- import { mkdirSync as mkdirSync4, readdirSync as readdirSync2, statSync as statSync2, writeFileSync as writeFileSync7 } from "fs";
768
+ import { mkdirSync as mkdirSync5, readdirSync as readdirSync2, statSync as statSync2, writeFileSync as writeFileSync8 } from "fs";
768
769
  import { mkdir, writeFile, access, readdir as readdir2, stat as stat2, readFile, rm as rm2 } from "fs/promises";
769
770
  import { createHash as createHash2 } from "crypto";
770
- import path12 from "path";
771
+ import path13 from "path";
771
772
  import os6 from "os";
772
773
 
773
774
  // src/drivers/claude.ts
@@ -823,6 +824,7 @@ function buildPrompt(config, variant, opts) {
823
824
  "- Always communicate through `slock` CLI commands. This is your only output channel.",
824
825
  ...opts.extraCriticalRules,
825
826
  "- Use only the provided `slock` CLI commands for messaging.",
827
+ "- Do not combine multiple `slock` CLI commands in one shell command. Run one `slock` command per tool call, read its output, then decide the next command.",
826
828
  "- Always claim a task via `slock task claim` before starting work on it. If the claim fails, move on to a different task."
827
829
  ] : [
828
830
  `- Always communicate through ${sendCmd}. This is your only output channel.`,
@@ -871,13 +873,15 @@ Use the \`slock\` CLI for chat / task / attachment operations. The daemon inject
871
873
  17. **\`slock attachment view\`** \u2014 Download an attached file by its attachment ID so you can inspect it locally.
872
874
  18. **\`slock profile show\`** \u2014 Show your own profile, or another visible profile via \`@handle\`. Mirrors the canonical Slock profile view.
873
875
  19. **\`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.
874
- 20. **\`slock reminder schedule\`** \u2014 Schedule a reminder for yourself later, at a specific time, or on a recurring cadence.
875
- 21. **\`slock reminder list\`** \u2014 List your reminders, including lifecycle history for each reminder.
876
- 22. **\`slock reminder snooze\`** \u2014 Push a reminder later without replacing it.
877
- 23. **\`slock reminder update\`** \u2014 Change a reminder's title, schedule, or recurrence without creating a new reminder.
878
- 24. **\`slock reminder cancel\`** \u2014 Cancel one of your reminders by ID.
879
- 25. **\`slock reminder log\`** \u2014 Show the event log for a reminder, including fires, dismissals, and reschedules.
880
- 26. **\`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\`).
876
+ 20. **\`slock integration list\`** \u2014 List registered third-party services and this agent's active Slock Agent Logins.
877
+ 21. **\`slock integration login\`** \u2014 Provision or reuse this agent's login for a registered third-party service.
878
+ 22. **\`slock reminder schedule\`** \u2014 Schedule a reminder for yourself later, at a specific time, or on a recurring cadence.
879
+ 23. **\`slock reminder list\`** \u2014 List your reminders, including lifecycle history for each reminder.
880
+ 24. **\`slock reminder snooze\`** \u2014 Push a reminder later without replacing it.
881
+ 25. **\`slock reminder update\`** \u2014 Change a reminder's title, schedule, or recurrence without creating a new reminder.
882
+ 26. **\`slock reminder cancel\`** \u2014 Cancel one of your reminders by ID.
883
+ 27. **\`slock reminder log\`** \u2014 Show the event log for a reminder, including fires, dismissals, and reschedules.
884
+ 28. **\`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\`).
881
885
 
882
886
  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:
883
887
  - failure \u2192 stderr \`{"ok":false,"code":"...","message":"..."}\` with non-zero exit
@@ -978,6 +982,11 @@ Each channel has a **name** and optionally a **description** that define its pur
978
982
  - **Reply in context** \u2014 always respond in the channel/thread the message came from.
979
983
  - **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.
980
984
  - If unsure where something belongs, call ${serverInfoCmd} to review channel descriptions.`;
985
+ const thirdPartyIntegrationsSection = isCli ? `### Third-party integrations
986
+
987
+ If a 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, first run \`slock integration list\` and match the app to a registered service before browsing the app. Use \`slock integration login --service <service>\` to provision or reuse your agent login for that service. 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 third-party 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, 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 registered service / Slock Agent Login path.` : `### Third-party integrations
988
+
989
+ If a 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, first inspect the registered-service interface and match the app to a registered 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 third-party 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 registered service / Slock Agent Login path.`;
981
990
  const readingHistorySection = isCli ? `### Reading history
982
991
 
983
992
  \`slock message read --channel "#channel-name"\` or \`slock message read --channel dm:@peer-name\` or \`slock message read --channel "#channel:shortid"\`
@@ -1149,6 +1158,8 @@ ${discoverySection}
1149
1158
 
1150
1159
  ${channelAwarenessSection}
1151
1160
 
1161
+ ${thirdPartyIntegrationsSection}
1162
+
1152
1163
  ${readingHistorySection}
1153
1164
 
1154
1165
  ${historicalReferenceSection}
@@ -1180,6 +1191,17 @@ Keep the user informed. They cannot see your internal reasoning, so:
1180
1191
  - For multi-step work, send short progress updates (e.g. "Working on step 2/3\u2026").
1181
1192
  - When done, summarize the result.
1182
1193
  - Keep updates concise \u2014 one or two sentences. Don't flood the chat.
1194
+ - For long answers where users need the conclusion first but details still matter, put the conclusion and next action outside any collapse, then use sanitized HTML details blocks for optional depth:
1195
+
1196
+ \`\`\`html
1197
+ <details>
1198
+ <summary>Evidence, logs, or edge cases</summary>
1199
+
1200
+ Detailed notes go here.
1201
+ </details>
1202
+ \`\`\`
1203
+
1204
+ Do not hide the main recommendation, blocker, or required action inside \`<details>\`; only fold supporting evidence, logs, alternatives, or extended rationale.
1183
1205
 
1184
1206
  ### Conversation etiquette
1185
1207
 
@@ -1275,13 +1297,12 @@ You may develop a specialized role over time through your interactions. Embrace
1275
1297
 
1276
1298
  ## Message Notifications
1277
1299
 
1278
- While you are working, new messages may be delivered directly into your current thread.
1300
+ While you are working, the daemon may write a batched inbox-count notification into your current turn.
1279
1301
 
1280
1302
  How to handle these:
1281
- - Treat direct follow-up messages as new user input for the same live session.
1282
- - Adapt if the new message changes priority or direction.
1283
- - You do NOT need to poll just because direct follow-up delivery is available.
1284
- - Use ${checkCmd} only when you need to inspect other pending channels or recover broader context.`;
1303
+ - Treat the notification as a signal that new Slock messages are waiting; it does not include the message content.
1304
+ - Call ${checkCmd} at the next safe breakpoint to materialize the pending messages before taking side-effect actions that depend on current context.
1305
+ - If the new message is higher priority, pivot after reading it. If not, continue your current work.`;
1285
1306
  } else {
1286
1307
  const notifyExample = isCli ? `\`[System notification: You have N new message(s) waiting. Call slock message check to read them when you're ready.]\`` : `\`[System notification: You have N new message(s) waiting. Call ${t("check_messages")} to read them when you're ready.]\``;
1287
1308
  prompt += `
@@ -1349,6 +1370,19 @@ function listLegacySlockStatePaths(slockHome = resolveSlockHome(), homeDir = os.
1349
1370
  return candidates.filter((candidate) => existsSync(candidate.path));
1350
1371
  }
1351
1372
 
1373
+ // src/authEnv.ts
1374
+ var DAEMON_API_KEY_ENV = "SLOCK_MACHINE_API_KEY";
1375
+ var SLOCK_AGENT_TOKEN_ENV = "SLOCK_AGENT_TOKEN";
1376
+ function scrubDaemonAuthEnv(env) {
1377
+ delete env[DAEMON_API_KEY_ENV];
1378
+ return env;
1379
+ }
1380
+ function scrubDaemonChildEnv(env) {
1381
+ delete env[DAEMON_API_KEY_ENV];
1382
+ delete env[SLOCK_AGENT_TOKEN_ENV];
1383
+ return env;
1384
+ }
1385
+
1352
1386
  // src/agentCredentialProxy.ts
1353
1387
  import { randomBytes } from "crypto";
1354
1388
  import http from "http";
@@ -1356,6 +1390,7 @@ import { URL as URL2 } from "url";
1356
1390
  var registrations = /* @__PURE__ */ new Map();
1357
1391
  var servers = /* @__PURE__ */ new Map();
1358
1392
  var DECODED_RESPONSE_HEADERS = /* @__PURE__ */ new Set(["content-encoding", "content-length", "transfer-encoding"]);
1393
+ var LOCAL_HELD_CONTEXT_LIMIT = 3;
1359
1394
  function allocatePort() {
1360
1395
  return 43e3 + randomBytes(2).readUInt16BE(0) % 1e4;
1361
1396
  }
@@ -1388,8 +1423,10 @@ async function handleProxyRequest(req, res) {
1388
1423
  res.end(JSON.stringify({ error: "invalid local agent proxy token", code: "invalid_agent_proxy_token" }));
1389
1424
  return;
1390
1425
  }
1426
+ const method = req.method ?? "GET";
1427
+ let target;
1391
1428
  try {
1392
- const target = new URL2(req.url ?? "/", registration.serverUrl);
1429
+ target = new URL2(req.url ?? "/", registration.serverUrl);
1393
1430
  const headers = new Headers();
1394
1431
  for (const [name, value] of Object.entries(req.headers)) {
1395
1432
  if (value === void 0) continue;
@@ -1405,12 +1442,20 @@ async function handleProxyRequest(req, res) {
1405
1442
  headers.set("X-Agent-Id", registration.agentId);
1406
1443
  headers.set("X-Slock-Client", "cli");
1407
1444
  headers.set("X-Slock-Agent-Active-Capabilities", registration.activeCapabilities);
1408
- const method = req.method ?? "GET";
1409
1445
  let body = method === "GET" || method === "HEAD" ? void 0 : req;
1410
1446
  let sendTarget;
1411
- if (method === "POST" && target.pathname === "/internal/agent-api/send") {
1447
+ const sideEffectAction = agentApiSideEffectAction(target.pathname);
1448
+ if (method === "GET" && target.pathname === "/internal/agent-api/events") {
1449
+ const localEvents = localAgentApiEventsResponse(registration, target);
1450
+ if (localEvents) {
1451
+ res.writeHead(localEvents.status, { "content-type": "application/json" });
1452
+ res.end(JSON.stringify(localEvents.body));
1453
+ return;
1454
+ }
1455
+ }
1456
+ if (method === "POST" && sideEffectAction) {
1412
1457
  const rawBody = await readRequestBody(req);
1413
- const prepared = await prepareAgentApiSendForward(registration, headers, rawBody);
1458
+ const prepared = await prepareAgentApiSideEffectForward(registration, headers, rawBody, sideEffectAction);
1414
1459
  if (prepared.localResponse) {
1415
1460
  const responseText = JSON.stringify(prepared.localResponse);
1416
1461
  res.writeHead(200, { "content-type": "application/json" });
@@ -1418,7 +1463,7 @@ async function handleProxyRequest(req, res) {
1418
1463
  return;
1419
1464
  }
1420
1465
  body = prepared.bodyText;
1421
- sendTarget = prepared.target;
1466
+ if (sideEffectAction === "send") sendTarget = prepared.target;
1422
1467
  headers.set("content-type", "application/json");
1423
1468
  headers.set("content-length", String(Buffer.byteLength(prepared.bodyText)));
1424
1469
  }
@@ -1452,14 +1497,33 @@ async function handleProxyRequest(req, res) {
1452
1497
  res.end();
1453
1498
  }
1454
1499
  } catch (err) {
1500
+ const failure = proxyFailureForError(method, target, err);
1501
+ logger.warn(
1502
+ `[Agent Credential Proxy] request failed (agent=${registration.agentId}, launch=${registration.launchId ?? "none"}, method=${failure.method}, path=${failure.pathname}, query_keys=${failure.queryKeys.join(",") || "none"}): ${failure.errorName}: ${failure.errorMessage}`
1503
+ );
1504
+ registration.inboxCoordinator?.recordProxyFailure?.(failure);
1455
1505
  res.writeHead(502, { "content-type": "application/json" });
1456
1506
  res.end(JSON.stringify({
1457
1507
  error: "failed to proxy local agent request",
1458
1508
  code: "agent_proxy_failed",
1459
- detail: err instanceof Error ? err.message : String(err)
1509
+ detail: failure.errorMessage
1460
1510
  }));
1461
1511
  }
1462
1512
  }
1513
+ function proxyFailureForError(method, target, err) {
1514
+ const queryKeys = target ? [.../* @__PURE__ */ new Set([...target.searchParams.keys()])].sort() : [];
1515
+ return {
1516
+ method,
1517
+ pathname: target?.pathname ?? "unknown",
1518
+ queryKeys,
1519
+ errorName: err instanceof Error ? err.name : typeof err,
1520
+ errorMessage: truncateProxyErrorMessage(err instanceof Error ? err.message : String(err))
1521
+ };
1522
+ }
1523
+ function truncateProxyErrorMessage(message) {
1524
+ const normalized = message.replace(/\s+/g, " ").trim();
1525
+ return normalized.length > 500 ? `${normalized.slice(0, 497)}...` : normalized;
1526
+ }
1463
1527
  async function readRequestBody(req) {
1464
1528
  let body = "";
1465
1529
  req.setEncoding("utf8");
@@ -1471,14 +1535,6 @@ async function readRequestBody(req) {
1471
1535
  function messageSeq(message) {
1472
1536
  return Number(message.seq ?? 0);
1473
1537
  }
1474
- function boundaryBefore(messages) {
1475
- let minSeq = Number.POSITIVE_INFINITY;
1476
- for (const message of messages) {
1477
- const seq = Math.floor(messageSeq(message));
1478
- if (Number.isFinite(seq) && seq > 0) minSeq = Math.min(minSeq, seq);
1479
- }
1480
- return Number.isFinite(minSeq) ? Math.max(0, minSeq - 1) : void 0;
1481
- }
1482
1538
  function maxMessageSeq(messages) {
1483
1539
  let maxSeq = 0;
1484
1540
  for (const message of messages) {
@@ -1487,6 +1543,117 @@ function maxMessageSeq(messages) {
1487
1543
  }
1488
1544
  return maxSeq > 0 ? maxSeq : void 0;
1489
1545
  }
1546
+ function sortBySeq(messages) {
1547
+ return [...messages].sort((a, b) => messageSeq(a) - messageSeq(b));
1548
+ }
1549
+ function latestVisibleMessages(messages, limit) {
1550
+ const sorted = sortBySeq(messages);
1551
+ return sorted.slice(Math.max(0, sorted.length - limit));
1552
+ }
1553
+ function parseAgentApiEventsQuery(target) {
1554
+ const limit = Math.min(Math.max(Number(target.searchParams.get("limit")) || 50, 1), 200);
1555
+ const sinceRaw = target.searchParams.get("since")?.trim() ?? "";
1556
+ if (!sinceRaw) return { limit, sinceSeq: null, sinceCursorKind: null };
1557
+ if (sinceRaw === "latest") return { limit, sinceSeq: null, sinceCursorKind: "latest" };
1558
+ const parsed = Number(sinceRaw);
1559
+ if (!Number.isFinite(parsed) || parsed < 0) {
1560
+ return {
1561
+ limit,
1562
+ sinceSeq: null,
1563
+ sinceCursorKind: null,
1564
+ error: {
1565
+ error: "since must be a non-negative integer (messageSeq) or 'latest'",
1566
+ code: "since_invalid"
1567
+ }
1568
+ };
1569
+ }
1570
+ return { limit, sinceSeq: Math.floor(parsed), sinceCursorKind: "seq" };
1571
+ }
1572
+ function localAgentApiEventsResponse(registration, target) {
1573
+ const coordinator = registration.inboxCoordinator;
1574
+ if (!coordinator) return void 0;
1575
+ const pending = coordinator.getAllPendingMessages?.() ?? [];
1576
+ const parsedQuery = parseAgentApiEventsQuery(target);
1577
+ if (parsedQuery.error && pending.length > 0) {
1578
+ return { status: 400, body: parsedQuery.error };
1579
+ }
1580
+ if (pending.length === 0) return void 0;
1581
+ const normalized = sortBySeq(normalizeVisibleMessages(pending));
1582
+ const filtered = parsedQuery.sinceSeq !== null ? normalized.filter((message) => {
1583
+ const seq = messageSeq(message);
1584
+ return Number.isFinite(seq) && seq > parsedQuery.sinceSeq;
1585
+ }) : normalized;
1586
+ const events = filtered.slice(0, parsedQuery.limit);
1587
+ const hasMore = filtered.length > events.length;
1588
+ const newestEvent = events[events.length - 1];
1589
+ const lastSeenMsgId = newestEvent?.message_id ?? newestEvent?.id ?? null;
1590
+ const lastSeenSeq = newestEvent?.seq ?? parsedQuery.sinceSeq;
1591
+ if (events.length > 0) {
1592
+ coordinator.consumeVisibleMessages({ messages: events, source: "agent_api_events" });
1593
+ }
1594
+ coordinator.recordDrainOutcome?.({
1595
+ source: "daemon_pending",
1596
+ sinceCursorKind: parsedQuery.sinceCursorKind,
1597
+ notifiedCount: pending.length,
1598
+ drainedCount: events.length,
1599
+ hasMore
1600
+ });
1601
+ return {
1602
+ status: 200,
1603
+ body: {
1604
+ events,
1605
+ last_seen_msgId: lastSeenMsgId,
1606
+ last_seen_seq: lastSeenSeq,
1607
+ reply_target: null,
1608
+ pending_notice_ids: [],
1609
+ wake_reason: null,
1610
+ has_more: hasMore
1611
+ }
1612
+ };
1613
+ }
1614
+ function heldAvailableActions(action) {
1615
+ return action === "send" ? ["check_messages", "send_draft", "send_anyway"] : ["check_messages", "retry_action"];
1616
+ }
1617
+ function localHeldResponse(input) {
1618
+ if (input.messages.length === 0) return void 0;
1619
+ const normalized = sortBySeq(normalizeVisibleMessages(input.messages, input.target));
1620
+ const heldMessages = latestVisibleMessages(normalized, LOCAL_HELD_CONTEXT_LIMIT);
1621
+ const omittedMessageCount = Math.max(0, normalized.length - heldMessages.length);
1622
+ const seenUpToSeq = maxMessageSeq(normalized);
1623
+ if (seenUpToSeq === void 0) return void 0;
1624
+ input.coordinator.consumeVisibleMessages({
1625
+ target: input.target,
1626
+ messages: heldMessages,
1627
+ boundarySeq: seenUpToSeq,
1628
+ source: input.source
1629
+ });
1630
+ const response = {
1631
+ state: "held",
1632
+ outcome: "held",
1633
+ subtype: "freshness",
1634
+ reason: "newer_messages_available",
1635
+ available_actions: heldAvailableActions(input.action),
1636
+ heldMessages,
1637
+ newMessageCount: normalized.length,
1638
+ shownMessageCount: heldMessages.length,
1639
+ omittedMessageCount
1640
+ };
1641
+ response.seenUpToSeq = seenUpToSeq;
1642
+ return response;
1643
+ }
1644
+ function recordFreshnessDecision(coordinator, decision) {
1645
+ coordinator?.recordFreshnessDecision?.(decision);
1646
+ }
1647
+ function agentApiSideEffectAction(pathname) {
1648
+ if (pathname === "/internal/agent-api/send") return "send";
1649
+ if (pathname === "/internal/agent-api/tasks/claim") return "task_claim";
1650
+ if (pathname === "/internal/agent-api/tasks/update-status") return "task_update";
1651
+ return void 0;
1652
+ }
1653
+ function sideEffectTarget(action, body) {
1654
+ const field = action === "send" ? body.target : body.channel;
1655
+ return typeof field === "string" && field.length > 0 ? field : void 0;
1656
+ }
1490
1657
  function parseTargetFields(target) {
1491
1658
  if (target.startsWith("dm:@")) {
1492
1659
  const rest = target.slice("dm:@".length);
@@ -1543,42 +1710,98 @@ async function loadRecentTargetMessages(registration, headers, target) {
1543
1710
  const parsed = await res.json().catch(() => null);
1544
1711
  return Array.isArray(parsed?.messages) ? normalizeVisibleMessages(parsed.messages, target) : [];
1545
1712
  }
1546
- async function prepareAgentApiSendForward(registration, headers, rawBody) {
1713
+ async function prepareAgentApiSideEffectForward(registration, headers, rawBody, action) {
1547
1714
  let body;
1548
1715
  try {
1549
1716
  body = rawBody ? JSON.parse(rawBody) : {};
1550
1717
  } catch {
1551
1718
  return { bodyText: rawBody };
1552
1719
  }
1553
- const target = typeof body.target === "string" ? body.target : void 0;
1720
+ const target = sideEffectTarget(action, body);
1554
1721
  const coordinator = registration.inboxCoordinator;
1555
- if (!target || !coordinator || body.continueAnyway === true) {
1722
+ if (!target || !coordinator) {
1723
+ return { bodyText: JSON.stringify(body), target };
1724
+ }
1725
+ if (action === "send" && body.continueAnyway === true) {
1726
+ recordFreshnessDecision(coordinator, {
1727
+ action,
1728
+ decision: "bypass",
1729
+ target,
1730
+ inboxTrustState: "trusted",
1731
+ reason: "continue_anyway"
1732
+ });
1556
1733
  return { bodyText: JSON.stringify(body), target };
1557
1734
  }
1558
1735
  const pending = coordinator.getPendingMessages(target);
1559
1736
  if (pending.length > 0) {
1560
- const staleBoundary = boundaryBefore(pending);
1561
- if (staleBoundary !== void 0) {
1562
- body.seenUpToSeq = staleBoundary;
1737
+ const localResponse = localHeldResponse({
1738
+ action,
1739
+ target,
1740
+ messages: pending,
1741
+ coordinator,
1742
+ source: "side_effect_preflight_context"
1743
+ });
1744
+ if (localResponse) {
1745
+ recordFreshnessDecision(coordinator, {
1746
+ action,
1747
+ decision: "local_hold",
1748
+ target,
1749
+ inboxTrustState: "trusted",
1750
+ reason: "exact_target_pending",
1751
+ pendingCount: pending.length,
1752
+ pendingMaxSeq: maxMessageSeq(pending),
1753
+ modelSeenSeq: coordinator.getBoundary(target),
1754
+ heldMessageCount: typeof localResponse.shownMessageCount === "number" ? localResponse.shownMessageCount : void 0,
1755
+ omittedMessageCount: typeof localResponse.omittedMessageCount === "number" ? localResponse.omittedMessageCount : void 0
1756
+ });
1563
1757
  }
1564
- return { bodyText: JSON.stringify(body), target };
1758
+ return {
1759
+ bodyText: JSON.stringify(body),
1760
+ target,
1761
+ localResponse
1762
+ };
1565
1763
  }
1566
1764
  const existingBoundary = typeof body.seenUpToSeq === "number" && Number.isFinite(body.seenUpToSeq) ? Math.max(0, Math.floor(body.seenUpToSeq)) : void 0;
1567
1765
  const loadedBoundary = coordinator.getBoundary(target);
1568
1766
  const boundary = Math.max(existingBoundary ?? 0, loadedBoundary ?? 0);
1569
1767
  if (boundary > 0) {
1570
- body.seenUpToSeq = boundary;
1768
+ if (action === "send") body.seenUpToSeq = boundary;
1769
+ recordFreshnessDecision(coordinator, {
1770
+ action,
1771
+ decision: "forward",
1772
+ target,
1773
+ inboxTrustState: "trusted",
1774
+ reason: "model_seen_boundary",
1775
+ pendingCount: 0,
1776
+ modelSeenSeq: boundary
1777
+ });
1571
1778
  return { bodyText: JSON.stringify(body), target };
1572
1779
  }
1573
1780
  const recent = await loadRecentTargetMessages(registration, headers, target);
1574
1781
  if (recent.length > 0) {
1575
1782
  const seenUpToSeq = maxMessageSeq(recent);
1576
- coordinator.consumeVisibleMessages({ target, messages: recent, boundarySeq: seenUpToSeq, source: "send_preflight_context" });
1783
+ coordinator.consumeVisibleMessages({ target, messages: recent, boundarySeq: seenUpToSeq, source: "side_effect_preflight_context" });
1784
+ recordFreshnessDecision(coordinator, {
1785
+ action,
1786
+ decision: "syncing_hold",
1787
+ target,
1788
+ inboxTrustState: "untrusted",
1789
+ reason: "target_first_touch_recent_context",
1790
+ pendingCount: 0,
1791
+ pendingMaxSeq: seenUpToSeq,
1792
+ modelSeenSeq: 0,
1793
+ heldMessageCount: recent.length,
1794
+ omittedMessageCount: 0
1795
+ });
1577
1796
  return {
1578
1797
  bodyText: JSON.stringify(body),
1579
1798
  target,
1580
1799
  localResponse: {
1581
1800
  state: "held",
1801
+ outcome: "held",
1802
+ subtype: "freshness",
1803
+ reason: "newer_messages_available",
1804
+ available_actions: heldAvailableActions(action),
1582
1805
  seenUpToSeq,
1583
1806
  heldMessages: recent,
1584
1807
  newMessageCount: recent.length,
@@ -1587,6 +1810,15 @@ async function prepareAgentApiSendForward(registration, headers, rawBody) {
1587
1810
  }
1588
1811
  };
1589
1812
  }
1813
+ recordFreshnessDecision(coordinator, {
1814
+ action,
1815
+ decision: "forward",
1816
+ target,
1817
+ inboxTrustState: "trusted",
1818
+ reason: "no_exact_target_pending_or_recent_context",
1819
+ pendingCount: 0,
1820
+ modelSeenSeq: 0
1821
+ });
1590
1822
  return { bodyText: JSON.stringify(body), target };
1591
1823
  }
1592
1824
  function shouldBufferJsonResponse(upstream, pathname, registration) {
@@ -1613,8 +1845,28 @@ function consumeVisibleResponse(registration, targetUrl, sendTarget, responseTex
1613
1845
  });
1614
1846
  return;
1615
1847
  }
1848
+ if (targetUrl.pathname === "/internal/agent-api/send" && parsed.state === "sent") {
1849
+ const messageSeq2 = typeof parsed.messageSeq === "number" && Number.isFinite(parsed.messageSeq) ? Math.floor(parsed.messageSeq) : void 0;
1850
+ if (sendTarget && messageSeq2 && messageSeq2 > 0) {
1851
+ coordinator.consumeVisibleMessages({
1852
+ target: sendTarget,
1853
+ messages: normalizeVisibleMessages([{ seq: messageSeq2, id: parsed.messageId }], sendTarget),
1854
+ boundarySeq: messageSeq2,
1855
+ source: "agent_api_send_commit"
1856
+ });
1857
+ }
1858
+ return;
1859
+ }
1616
1860
  if (targetUrl.pathname === "/internal/agent-api/events" && Array.isArray(parsed.events)) {
1617
- coordinator.consumeVisibleMessages({ messages: normalizeVisibleMessages(parsed.events), source: "agent_api_events" });
1861
+ const messages = normalizeVisibleMessages(parsed.events);
1862
+ coordinator.consumeVisibleMessages({ messages, source: "agent_api_events" });
1863
+ coordinator.recordDrainOutcome?.({
1864
+ source: "server_events",
1865
+ sinceCursorKind: parseAgentApiEventsQuery(targetUrl).sinceCursorKind,
1866
+ notifiedCount: 0,
1867
+ drainedCount: messages.length,
1868
+ hasMore: Boolean(parsed.has_more)
1869
+ });
1618
1870
  return;
1619
1871
  }
1620
1872
  if (targetUrl.pathname === "/internal/agent-api/history" && Array.isArray(parsed.messages)) {
@@ -1733,7 +1985,7 @@ ${cmdCredentialLine}"${process.execPath}" "${ctx.slockCliPath}" %*\r
1733
1985
  ...agentCredentialProxy ? {} : { SLOCK_AGENT_TOKEN_FILE: tokenFile },
1734
1986
  PATH: `${slockDir}${path2.delimiter}${process.env.PATH ?? ""}`
1735
1987
  };
1736
- delete spawnEnv.SLOCK_AGENT_TOKEN;
1988
+ scrubDaemonChildEnv(spawnEnv);
1737
1989
  delete spawnEnv.SLOCK_AGENT_CREDENTIAL_KEY;
1738
1990
  delete spawnEnv.SLOCK_AGENT_CREDENTIAL_KEY_FILE;
1739
1991
  delete spawnEnv.SLOCK_AGENT_PROXY_URL;
@@ -1780,7 +2032,7 @@ function resolveCommandOnWindows(command, env, execFileSyncFn) {
1780
2032
  }
1781
2033
  function resolveCommandOnPath(command, deps = {}) {
1782
2034
  const platform = deps.platform ?? process.platform;
1783
- const env = deps.env ?? process.env;
2035
+ const env = scrubDaemonChildEnv({ ...deps.env ?? process.env });
1784
2036
  const execFileSyncFn = deps.execFileSyncFn ?? execFileSync;
1785
2037
  if (platform === "win32") {
1786
2038
  return resolveCommandOnWindows(command, env, execFileSyncFn);
@@ -1805,7 +2057,7 @@ function firstExistingPath(candidates, deps = {}) {
1805
2057
  return null;
1806
2058
  }
1807
2059
  function readCommandVersion(command, args = [], deps = {}) {
1808
- const env = deps.env ?? process.env;
2060
+ const env = scrubDaemonChildEnv({ ...deps.env ?? process.env });
1809
2061
  const execFileSyncFn = deps.execFileSyncFn ?? execFileSync;
1810
2062
  try {
1811
2063
  const output = normalizeExecOutput(execFileSyncFn(command, [...args, "--version"], {
@@ -2159,8 +2411,7 @@ var ClaudeDriver = class {
2159
2411
  "- Runtime Profile migration completion is the only exception to CLI-only operation: when a migration notice tells you to acknowledge with `runtime_profile_migration_done`, call the `mcp__chat__runtime_profile_migration_done` tool with the exact `migration_key`; do not use `slock` CLI or reply in chat as the acknowledgment."
2160
2412
  ],
2161
2413
  postStartupNotes: [
2162
- "**Claude runtime note:** Slock preserves Claude Code same-turn steering through a gated stream-json delivery path. Busy messages are buffered and delivered at Claude-observed safe boundaries; if no earlier safe boundary is available, they are delivered after the current turn ends.",
2163
- "For long tool runs, you can also use `slock message check` at natural breakpoints to pull pending messages explicitly."
2414
+ "**Claude runtime note:** While you are busy, Slock batches inbox-count notifications instead of injecting message content. Use `slock message check` at natural breakpoints to pull the pending messages before side-effect actions that depend on current context."
2164
2415
  ],
2165
2416
  includeStdinNotificationSection: true,
2166
2417
  messageNotificationStyle: "direct"
@@ -2654,7 +2905,7 @@ var CodexDriver = class {
2654
2905
  toolPrefix: "",
2655
2906
  extraCriticalRules: [],
2656
2907
  postStartupNotes: [
2657
- "**IMPORTANT**: Your process stays alive across turns. New messages may be delivered directly into the current thread while you are working."
2908
+ "**IMPORTANT**: Your process stays alive across turns. While you are working, Slock may write batched inbox-count notifications into the current turn; call `slock message check` at natural breakpoints to read the pending messages."
2658
2909
  ],
2659
2910
  includeStdinNotificationSection: true,
2660
2911
  messageNotificationStyle: "direct"
@@ -3102,7 +3353,7 @@ function detectCursorModels(runCommand = runCursorModelsCommand) {
3102
3353
  }
3103
3354
  function runCursorModelsCommand() {
3104
3355
  return spawnSync("cursor-agent", ["models"], {
3105
- env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" },
3356
+ env: scrubDaemonChildEnv({ ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" }),
3106
3357
  encoding: "utf8",
3107
3358
  timeout: 5e3
3108
3359
  });
@@ -3149,7 +3400,7 @@ function resolveGeminiSpawn(commandArgs, deps = {}) {
3149
3400
  }
3150
3401
  const execFileSyncFn = deps.execFileSyncFn ?? execFileSync3;
3151
3402
  const existsSyncFn = deps.existsSyncFn ?? existsSync6;
3152
- const env = deps.env ?? process.env;
3403
+ const env = scrubDaemonChildEnv({ ...deps.env ?? process.env });
3153
3404
  const winPath = path8.win32;
3154
3405
  let geminiEntry = null;
3155
3406
  try {
@@ -3323,13 +3574,16 @@ var GeminiDriver = class {
3323
3574
  // src/drivers/kimi.ts
3324
3575
  import { randomUUID } from "crypto";
3325
3576
  import { spawn as spawn6 } from "child_process";
3326
- import { existsSync as existsSync7, readFileSync as readFileSync3, writeFileSync as writeFileSync6 } from "fs";
3577
+ import { chmodSync, existsSync as existsSync7, readFileSync as readFileSync3, writeFileSync as writeFileSync6 } from "fs";
3327
3578
  import os4 from "os";
3328
3579
  import path9 from "path";
3329
3580
  var KIMI_WIRE_PROTOCOL_VERSION = "1.3";
3330
3581
  var KIMI_SYSTEM_PROMPT_FILE = ".slock-kimi-system.md";
3331
3582
  var KIMI_AGENT_FILE = ".slock-kimi-agent.yaml";
3332
3583
  var KIMI_MCP_FILE = ".slock-kimi-mcp.json";
3584
+ var KIMI_GENERATED_CONFIG_FILE = ".slock-kimi-config.toml";
3585
+ var SLOCK_KIMI_CONFIG_CONTENT_ENV = "SLOCK_KIMI_CONFIG_CONTENT";
3586
+ var SLOCK_KIMI_CONFIG_FILE_ENV = "SLOCK_KIMI_CONFIG_FILE";
3333
3587
  function parseToolArguments(raw) {
3334
3588
  if (typeof raw !== "string") return raw;
3335
3589
  try {
@@ -3338,6 +3592,73 @@ function parseToolArguments(raw) {
3338
3592
  return raw;
3339
3593
  }
3340
3594
  }
3595
+ function readKimiConfigSource(home = os4.homedir(), env = process.env) {
3596
+ const inlineConfig = env[SLOCK_KIMI_CONFIG_CONTENT_ENV];
3597
+ if (inlineConfig && inlineConfig.trim()) {
3598
+ return {
3599
+ raw: inlineConfig,
3600
+ explicitPath: null,
3601
+ sourcePath: SLOCK_KIMI_CONFIG_CONTENT_ENV
3602
+ };
3603
+ }
3604
+ const explicitPath = env[SLOCK_KIMI_CONFIG_FILE_ENV];
3605
+ const configPath = explicitPath && explicitPath.trim() ? explicitPath : path9.join(home, ".kimi", "config.toml");
3606
+ try {
3607
+ return {
3608
+ raw: readFileSync3(configPath, "utf8"),
3609
+ explicitPath: explicitPath && explicitPath.trim() ? explicitPath : null,
3610
+ sourcePath: configPath
3611
+ };
3612
+ } catch {
3613
+ return {
3614
+ raw: null,
3615
+ explicitPath: explicitPath && explicitPath.trim() ? explicitPath : null,
3616
+ sourcePath: configPath
3617
+ };
3618
+ }
3619
+ }
3620
+ function buildKimiSpawnEnv(env = process.env) {
3621
+ const spawnEnv = { ...env, FORCE_COLOR: "0", NO_COLOR: "1" };
3622
+ delete spawnEnv[SLOCK_KIMI_CONFIG_CONTENT_ENV];
3623
+ delete spawnEnv[SLOCK_KIMI_CONFIG_FILE_ENV];
3624
+ return scrubDaemonChildEnv(spawnEnv);
3625
+ }
3626
+ function buildKimiEffectiveEnv(ctx, overrideEnv) {
3627
+ return {
3628
+ ...process.env,
3629
+ ...ctx.config.envVars || {},
3630
+ ...overrideEnv || {}
3631
+ };
3632
+ }
3633
+ function buildKimiLaunchOptions(ctx, opts = {}) {
3634
+ const env = buildKimiEffectiveEnv(ctx, opts.env);
3635
+ const source = readKimiConfigSource(opts.home ?? os4.homedir(), env);
3636
+ const args = [];
3637
+ let configFilePath = null;
3638
+ let configContent = null;
3639
+ if (source.explicitPath) {
3640
+ configFilePath = source.explicitPath;
3641
+ } else if (source.raw !== null && source.sourcePath === SLOCK_KIMI_CONFIG_CONTENT_ENV) {
3642
+ configFilePath = path9.join(ctx.workingDirectory, KIMI_GENERATED_CONFIG_FILE);
3643
+ configContent = source.raw;
3644
+ if (opts.writeGeneratedConfig !== false) {
3645
+ writeFileSync6(configFilePath, source.raw, { encoding: "utf8", mode: 384 });
3646
+ chmodSync(configFilePath, 384);
3647
+ }
3648
+ }
3649
+ if (configFilePath) {
3650
+ args.push("--config-file", configFilePath);
3651
+ }
3652
+ if (ctx.config.model && ctx.config.model !== "default") {
3653
+ args.push("--model", ctx.config.model);
3654
+ }
3655
+ return {
3656
+ args,
3657
+ env: buildKimiSpawnEnv(env),
3658
+ configFilePath,
3659
+ configContent
3660
+ };
3661
+ }
3341
3662
  function resolveKimiSpawn(commandArgs, deps = {}) {
3342
3663
  return {
3343
3664
  command: resolveCommandOnPath("kimi", deps) ?? "kimi",
@@ -3361,7 +3682,25 @@ var KimiDriver = class {
3361
3682
  };
3362
3683
  model = {
3363
3684
  detectedModelsVerifiedAs: "launchable",
3364
- toLaunchSpec: (modelId) => ({ args: ["--model", modelId] })
3685
+ toLaunchSpec: (modelId, ctx, opts) => {
3686
+ if (!ctx) return { args: ["--model", modelId] };
3687
+ const launchCtx = {
3688
+ ...ctx,
3689
+ config: {
3690
+ ...ctx.config,
3691
+ model: modelId
3692
+ }
3693
+ };
3694
+ const launch = buildKimiLaunchOptions(launchCtx, {
3695
+ home: opts?.home,
3696
+ writeGeneratedConfig: false
3697
+ });
3698
+ return {
3699
+ args: launch.args,
3700
+ env: launch.env,
3701
+ configFiles: launch.configFilePath ? [launch.configFilePath] : void 0
3702
+ };
3703
+ }
3365
3704
  };
3366
3705
  supportsStdinNotification = true;
3367
3706
  mcpToolPrefix = "";
@@ -3415,6 +3754,7 @@ var KimiDriver = class {
3415
3754
  }
3416
3755
  }
3417
3756
  }), "utf8");
3757
+ const launch = buildKimiLaunchOptions(ctx);
3418
3758
  const args = [
3419
3759
  "--wire",
3420
3760
  "--yolo",
@@ -3423,14 +3763,15 @@ var KimiDriver = class {
3423
3763
  "--mcp-config-file",
3424
3764
  mcpConfigPath,
3425
3765
  "--session",
3426
- this.sessionId
3766
+ this.sessionId,
3767
+ ...launch.args
3427
3768
  ];
3428
3769
  if (ctx.config.model && ctx.config.model !== "default") {
3429
3770
  args.push("--model", ctx.config.model);
3430
3771
  }
3431
3772
  const spawnEnv = prepareCliTransport(ctx, { NO_COLOR: "1" }).spawnEnv;
3432
- const launch = resolveKimiSpawn(args);
3433
- const proc = spawn6(launch.command, launch.args, {
3773
+ const spawnTarget = resolveKimiSpawn(args);
3774
+ const proc = spawn6(spawnTarget.command, spawnTarget.args, {
3434
3775
  cwd: ctx.workingDirectory,
3435
3776
  stdio: ["pipe", "pipe", "pipe"],
3436
3777
  env: spawnEnv,
@@ -3438,7 +3779,7 @@ var KimiDriver = class {
3438
3779
  // and has an 8191-character command-line limit. Kimi's official
3439
3780
  // installer/uv entrypoint is an executable, so launch it directly and
3440
3781
  // keep prompts on stdin / files instead of routing through cmd.exe.
3441
- shell: launch.shell
3782
+ shell: spawnTarget.shell
3442
3783
  });
3443
3784
  proc.stdin?.write(JSON.stringify({
3444
3785
  jsonrpc: "2.0",
@@ -3554,14 +3895,9 @@ var KimiDriver = class {
3554
3895
  return detectKimiModels();
3555
3896
  }
3556
3897
  };
3557
- function detectKimiModels(home = os4.homedir()) {
3558
- const configPath = path9.join(home, ".kimi", "config.toml");
3559
- let raw;
3560
- try {
3561
- raw = readFileSync3(configPath, "utf8");
3562
- } catch {
3563
- return null;
3564
- }
3898
+ function detectKimiModels(home = os4.homedir(), opts = {}) {
3899
+ const raw = readKimiConfigSource(home, opts.env).raw;
3900
+ if (raw === null) return null;
3565
3901
  const models = [];
3566
3902
  const sectionRe = /^\s*\[models(?:\.([^\]]+)|"\.[^"]+"|\."[^"]+")\s*\]\s*$/gm;
3567
3903
  const lineRe = /^\s*\[models\.(.+?)\s*\]\s*$/gm;
@@ -3582,7 +3918,7 @@ function detectKimiModels(home = os4.homedir()) {
3582
3918
 
3583
3919
  // src/drivers/opencode.ts
3584
3920
  import { spawn as spawn7, spawnSync as spawnSync2 } from "child_process";
3585
- import { readFileSync as readFileSync4 } from "fs";
3921
+ import { existsSync as existsSync8, readFileSync as readFileSync4 } from "fs";
3586
3922
  import os5 from "os";
3587
3923
  import path10 from "path";
3588
3924
  var CHAT_MCP_SERVER_NAME = "chat";
@@ -3820,11 +4156,14 @@ function detectOpenCodeModels(home = os5.homedir(), runCommand = runOpenCodeMode
3820
4156
  if (commandResult.error || commandResult.status !== 0) return null;
3821
4157
  return parseOpenCodeModelsOutput(commandResult.stdout);
3822
4158
  }
3823
- function runOpenCodeModelsCommand(home) {
3824
- const result = spawnSync2("opencode", ["models"], {
3825
- env: { ...process.env, HOME: home, FORCE_COLOR: "0", NO_COLOR: "1" },
4159
+ function runOpenCodeModelsCommand(home, deps = {}) {
4160
+ const platform = deps.platform ?? process.platform;
4161
+ const spawnSyncFn = deps.spawnSyncFn ?? spawnSync2;
4162
+ const result = spawnSyncFn("opencode", ["models"], {
4163
+ env: scrubDaemonChildEnv({ ...process.env, HOME: home, FORCE_COLOR: "0", NO_COLOR: "1" }),
3826
4164
  encoding: "utf8",
3827
- timeout: 5e3
4165
+ timeout: 5e3,
4166
+ shell: platform === "win32"
3828
4167
  });
3829
4168
  return {
3830
4169
  status: result.status,
@@ -3832,6 +4171,102 @@ function runOpenCodeModelsCommand(home) {
3832
4171
  error: result.error
3833
4172
  };
3834
4173
  }
4174
+ function isWindowsCommandShim(commandPath) {
4175
+ const ext = path10.win32.extname(commandPath).toLowerCase();
4176
+ return ext === ".cmd" || ext === ".bat";
4177
+ }
4178
+ function opencodePackageEntryCandidates(packageRoot) {
4179
+ const winPath = path10.win32;
4180
+ return [
4181
+ winPath.join(packageRoot, "bin", "opencode.exe"),
4182
+ winPath.join(packageRoot, "bin", "opencode.js"),
4183
+ winPath.join(packageRoot, "bin", "opencode.mjs"),
4184
+ winPath.join(packageRoot, "dist", "index.js")
4185
+ ];
4186
+ }
4187
+ function openCodeSpecForEntry(entry, commandArgs) {
4188
+ if (path10.win32.extname(entry).toLowerCase() === ".exe") {
4189
+ return { command: entry, args: commandArgs, shell: false };
4190
+ }
4191
+ return { command: process.execPath, args: [entry, ...commandArgs], shell: false };
4192
+ }
4193
+ function resolveWindowsOpenCodePackageEntry(commandPath, deps = {}) {
4194
+ const existsSyncFn = deps.existsSyncFn ?? existsSync8;
4195
+ const execFileSyncFn = deps.execFileSyncFn;
4196
+ const env = deps.env ?? process.env;
4197
+ const winPath = path10.win32;
4198
+ const candidates = [];
4199
+ if (execFileSyncFn) {
4200
+ try {
4201
+ const globalRoot = String(execFileSyncFn("npm", ["root", "-g"], {
4202
+ encoding: "utf8",
4203
+ stdio: ["ignore", "pipe", "ignore"],
4204
+ env
4205
+ })).trim();
4206
+ if (globalRoot) {
4207
+ candidates.push(...opencodePackageEntryCandidates(winPath.join(globalRoot, "opencode-ai")));
4208
+ }
4209
+ } catch {
4210
+ }
4211
+ }
4212
+ if (commandPath) {
4213
+ const commandDir = winPath.dirname(commandPath);
4214
+ candidates.push(...opencodePackageEntryCandidates(winPath.join(commandDir, "node_modules", "opencode-ai")));
4215
+ candidates.push(...extractWindowsShimTargets(commandPath, deps));
4216
+ }
4217
+ for (const candidate of candidates) {
4218
+ if (existsSyncFn(candidate)) return candidate;
4219
+ }
4220
+ return null;
4221
+ }
4222
+ function extractWindowsShimTargets(commandPath, deps = {}) {
4223
+ if (!isWindowsCommandShim(commandPath)) return [];
4224
+ const readFileSyncFn = deps.readFileSyncFn ?? readFileSync4;
4225
+ const commandDir = path10.win32.dirname(commandPath);
4226
+ let raw;
4227
+ try {
4228
+ raw = String(readFileSyncFn(commandPath, "utf8"));
4229
+ } catch {
4230
+ return [];
4231
+ }
4232
+ const candidates = [];
4233
+ const dp0Pattern = /%~dp0\\?([^"\r\n]*?opencode\.(?:exe|js|mjs|cjs))/gi;
4234
+ for (const match of raw.matchAll(dp0Pattern)) {
4235
+ const relative = match[1]?.replace(/^\\+/, "");
4236
+ if (relative) candidates.push(path10.win32.normalize(path10.win32.join(commandDir, relative)));
4237
+ }
4238
+ return candidates;
4239
+ }
4240
+ function resolveOpenCodeSpawn(commandArgs, deps = {}) {
4241
+ const platform = deps.platform ?? process.platform;
4242
+ if (platform !== "win32") {
4243
+ return {
4244
+ command: resolveCommandOnPath("opencode", deps) ?? "opencode",
4245
+ args: commandArgs,
4246
+ shell: false
4247
+ };
4248
+ }
4249
+ const command = resolveCommandOnPath("opencode", deps);
4250
+ if (command && path10.win32.extname(command).toLowerCase() === ".exe") {
4251
+ return { command, args: commandArgs, shell: false };
4252
+ }
4253
+ const packageEntry = resolveWindowsOpenCodePackageEntry(command, deps);
4254
+ if (packageEntry) return openCodeSpecForEntry(packageEntry, commandArgs);
4255
+ if (command && !isWindowsCommandShim(command)) {
4256
+ return { command, args: commandArgs, shell: false };
4257
+ }
4258
+ throw new Error(
4259
+ "Cannot resolve OpenCode CLI entry point on Windows without cmd.exe. Install the native OpenCode executable or install opencode-ai globally so Slock can launch node_modules/opencode-ai/bin/opencode.exe directly."
4260
+ );
4261
+ }
4262
+ function readOpenCodeVersion(deps = {}) {
4263
+ try {
4264
+ const launch = resolveOpenCodeSpawn([], deps);
4265
+ return readCommandVersion(launch.command, launch.args, deps);
4266
+ } catch {
4267
+ return null;
4268
+ }
4269
+ }
3835
4270
  function isSystemFirstMessageTask(message) {
3836
4271
  return message.sender_id === "system" && message.channel_type === "channel" && message.channel_name === "all" && message.content.trimStart().startsWith(FIRST_MESSAGE_TASK_PREFIX);
3837
4272
  }
@@ -3874,7 +4309,7 @@ var OpenCodeDriver = class {
3874
4309
  model: modelId
3875
4310
  }
3876
4311
  };
3877
- const version = readCommandVersion("opencode");
4312
+ const version = readOpenCodeVersion();
3878
4313
  const launch = buildOpenCodeLaunchOptions(launchCtx, opts?.home, version);
3879
4314
  return {
3880
4315
  args: launch.args,
@@ -3895,8 +4330,13 @@ var OpenCodeDriver = class {
3895
4330
  sessionId = null;
3896
4331
  sessionAnnounced = false;
3897
4332
  probe() {
3898
- if (!resolveCommandOnPath("opencode")) return { available: false };
3899
- const version = readCommandVersion("opencode") || void 0;
4333
+ let version;
4334
+ try {
4335
+ const launch = resolveOpenCodeSpawn([]);
4336
+ version = readCommandVersion(launch.command, launch.args);
4337
+ } catch {
4338
+ return { available: false };
4339
+ }
3900
4340
  const unsupportedMessage = unsupportedOpenCodeVersionMessage(version);
3901
4341
  if (unsupportedMessage) {
3902
4342
  return {
@@ -3904,7 +4344,7 @@ var OpenCodeDriver = class {
3904
4344
  version: `${version} (requires >= ${MIN_SUPPORTED_OPENCODE_VERSION})`
3905
4345
  };
3906
4346
  }
3907
- return { available: true, version };
4347
+ return { available: true, version: version ?? void 0 };
3908
4348
  }
3909
4349
  async detectModels() {
3910
4350
  return detectOpenCodeModels();
@@ -3912,17 +4352,18 @@ var OpenCodeDriver = class {
3912
4352
  spawn(ctx) {
3913
4353
  this.sessionId = ctx.config.sessionId || null;
3914
4354
  this.sessionAnnounced = false;
3915
- const version = readCommandVersion("opencode");
4355
+ const version = readOpenCodeVersion();
3916
4356
  const unsupportedMessage = unsupportedOpenCodeVersionMessage(version);
3917
4357
  if (unsupportedMessage) {
3918
4358
  throw new Error(unsupportedMessage);
3919
4359
  }
3920
4360
  const launch = buildOpenCodeLaunchOptions(ctx, os5.homedir(), version);
3921
- const proc = spawn7("opencode", launch.args, {
4361
+ const spawnSpec = resolveOpenCodeSpawn(launch.args);
4362
+ const proc = spawn7(spawnSpec.command, spawnSpec.args, {
3922
4363
  cwd: ctx.workingDirectory,
3923
4364
  stdio: ["pipe", "pipe", "pipe"],
3924
4365
  env: launch.env,
3925
- shell: process.platform === "win32"
4366
+ shell: spawnSpec.shell
3926
4367
  });
3927
4368
  proc.stdin?.end();
3928
4369
  return { process: proc };
@@ -3980,6 +4421,297 @@ var OpenCodeDriver = class {
3980
4421
  }
3981
4422
  };
3982
4423
 
4424
+ // src/drivers/pi.ts
4425
+ import { spawn as spawn8 } from "child_process";
4426
+ import { existsSync as existsSync9, mkdirSync as mkdirSync4, writeFileSync as writeFileSync7 } from "fs";
4427
+ import path11 from "path";
4428
+ import { fileURLToPath } from "url";
4429
+ import { getAgentDir, VERSION as PI_SDK_VERSION } from "@earendil-works/pi-coding-agent";
4430
+ var CHAT_MCP_TOOL_PREFIX2 = "chat_";
4431
+ var NO_MESSAGE_PROMPT2 = "No new messages are pending. Stop now.";
4432
+ var FIRST_MESSAGE_TASK_PREFIX2 = "First message task (system-triggered):";
4433
+ var MIN_SUPPORTED_PI_VERSION = "0.74.0";
4434
+ function parseSemver2(version) {
4435
+ const match = version.match(/(\d+)\.(\d+)\.(\d+)/);
4436
+ if (!match) return null;
4437
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
4438
+ }
4439
+ function isSupportedPiVersion(version) {
4440
+ if (!version) return true;
4441
+ const actual = parseSemver2(version);
4442
+ const minimum = parseSemver2(MIN_SUPPORTED_PI_VERSION);
4443
+ if (!actual || !minimum) return true;
4444
+ for (let i = 0; i < 3; i += 1) {
4445
+ if (actual[i] > minimum[i]) return true;
4446
+ if (actual[i] < minimum[i]) return false;
4447
+ }
4448
+ return true;
4449
+ }
4450
+ function unsupportedPiVersionMessage(version) {
4451
+ if (!version || isSupportedPiVersion(version)) return null;
4452
+ return `Pi SDK ${version} is unsupported; requires @earendil-works/pi-coding-agent >= ${MIN_SUPPORTED_PI_VERSION}. Upgrade the daemon Pi dependency before starting this runtime.`;
4453
+ }
4454
+ function probePi(version = PI_SDK_VERSION) {
4455
+ const unsupportedMessage = unsupportedPiVersionMessage(version);
4456
+ if (unsupportedMessage) {
4457
+ return {
4458
+ available: false,
4459
+ version: `${version} (requires @earendil-works/pi-coding-agent >= ${MIN_SUPPORTED_PI_VERSION})`
4460
+ };
4461
+ }
4462
+ return { available: true, version };
4463
+ }
4464
+ function resolvePiSdkRunnerPath(moduleUrl = import.meta.url) {
4465
+ const moduleDir = path11.dirname(fileURLToPath(moduleUrl));
4466
+ const sourceSibling = path11.join(moduleDir, "piSdkRunner.ts");
4467
+ if (existsSync9(sourceSibling)) return sourceSibling;
4468
+ const bundledEntry = path11.join(moduleDir, "drivers", "piSdkRunner.js");
4469
+ if (existsSync9(bundledEntry)) return bundledEntry;
4470
+ return path11.join(moduleDir, "piSdkRunner.js");
4471
+ }
4472
+ function buildPiSdkNodeArgs(runnerPath = resolvePiSdkRunnerPath()) {
4473
+ if (runnerPath.endsWith(".ts")) {
4474
+ return [...process.execArgv, runnerPath];
4475
+ }
4476
+ return [runnerPath];
4477
+ }
4478
+ function buildPiLaunchOptions(ctx, opts = {}) {
4479
+ const command = opts.command ?? process.execPath;
4480
+ const piDir = path11.join(ctx.workingDirectory, ".slock", "pi");
4481
+ const sessionDir = path11.join(piDir, "sessions");
4482
+ mkdirSync4(sessionDir, { recursive: true });
4483
+ const slock = prepareCliTransport(ctx, { NO_COLOR: "1" });
4484
+ const runnerPath = opts.runnerPath ?? resolvePiSdkRunnerPath();
4485
+ const agentDir = opts.agentDir ?? getAgentDir();
4486
+ const runnerConfigPath = path11.join(
4487
+ piDir,
4488
+ `sdk-run-${(ctx.launchId || "launch").replace(/[^a-zA-Z0-9_.-]/g, "_")}.json`
4489
+ );
4490
+ const turnPrompt = ctx.prompt === ctx.standingPrompt ? NO_MESSAGE_PROMPT2 : ctx.prompt;
4491
+ const runnerConfig = {
4492
+ cwd: ctx.workingDirectory,
4493
+ agentDir,
4494
+ sessionDir,
4495
+ sessionId: ctx.config.sessionId || null,
4496
+ standingPrompt: ctx.standingPrompt,
4497
+ prompt: turnPrompt,
4498
+ model: ctx.config.model && ctx.config.model !== "default" ? ctx.config.model : null
4499
+ };
4500
+ writeFileSync7(runnerConfigPath, `${JSON.stringify(runnerConfig)}
4501
+ `, { encoding: "utf8", mode: 384 });
4502
+ const args = [
4503
+ ...buildPiSdkNodeArgs(runnerPath),
4504
+ "--config",
4505
+ runnerConfigPath
4506
+ ];
4507
+ return {
4508
+ command,
4509
+ args,
4510
+ env: slock.spawnEnv,
4511
+ sessionDir,
4512
+ agentDir,
4513
+ runnerConfigPath,
4514
+ sdkVersion: PI_SDK_VERSION
4515
+ };
4516
+ }
4517
+ function isSystemFirstMessageTask2(message) {
4518
+ return message.sender_id === "system" && message.channel_type === "channel" && message.channel_name === "all" && message.content.trimStart().startsWith(FIRST_MESSAGE_TASK_PREFIX2);
4519
+ }
4520
+ function buildPiSystemPrompt(config) {
4521
+ return buildCliTransportSystemPrompt(config, {
4522
+ toolPrefix: CHAT_MCP_TOOL_PREFIX2,
4523
+ extraCriticalRules: [
4524
+ "- Runtime Profile migration controls are not available in the Pi runtime yet. If asked to acknowledge a runtime migration, explain the blocker instead of inventing a command."
4525
+ ],
4526
+ postStartupNotes: [
4527
+ "**Pi runtime note:** Slock launches you as a per-turn process. Complete the current wake using `slock` CLI commands, then stop; the daemon will restart you when new messages arrive."
4528
+ ],
4529
+ includeStdinNotificationSection: false,
4530
+ messageNotificationStyle: "poll"
4531
+ });
4532
+ }
4533
+ function contentText(content) {
4534
+ if (!content) return "";
4535
+ const chunks = [];
4536
+ for (const item of content) {
4537
+ if (item.type === "text" && typeof item.text === "string") {
4538
+ chunks.push(item.text);
4539
+ }
4540
+ }
4541
+ return chunks.join("");
4542
+ }
4543
+ function apiKeyErrorMessage(line) {
4544
+ const trimmed = line.trim();
4545
+ if (!trimmed) return null;
4546
+ if (/no api key found/i.test(trimmed)) return trimmed;
4547
+ if (/api key.+required/i.test(trimmed)) return trimmed;
4548
+ if (/no models available/i.test(trimmed)) return trimmed;
4549
+ return null;
4550
+ }
4551
+ var PiDriver = class {
4552
+ id = "pi";
4553
+ lifecycle = {
4554
+ kind: "per_turn",
4555
+ start: "defer_until_concrete_message",
4556
+ exit: "terminate_on_turn_end",
4557
+ inFlightWake: "coalesce_into_pending"
4558
+ };
4559
+ communication = {
4560
+ chat: "slock_cli",
4561
+ runtimeControl: "none"
4562
+ };
4563
+ session = {
4564
+ recovery: "resume_or_fresh"
4565
+ };
4566
+ model = {
4567
+ detectedModelsVerifiedAs: "launchable",
4568
+ toLaunchSpec: (modelId, ctx) => {
4569
+ if (!ctx) return modelId && modelId !== "default" ? { args: ["--model", modelId] } : { args: [] };
4570
+ const launchCtx = {
4571
+ ...ctx,
4572
+ config: {
4573
+ ...ctx.config,
4574
+ model: modelId
4575
+ }
4576
+ };
4577
+ const launch = buildPiLaunchOptions(launchCtx);
4578
+ return {
4579
+ args: launch.args,
4580
+ env: launch.env,
4581
+ configFiles: [launch.runnerConfigPath],
4582
+ params: {
4583
+ agentDir: launch.agentDir,
4584
+ sessionDir: launch.sessionDir,
4585
+ sdkVersion: launch.sdkVersion,
4586
+ resources: "extensions/skills/prompt-templates/themes/context-files disabled by Slock policy"
4587
+ }
4588
+ };
4589
+ }
4590
+ };
4591
+ supportsStdinNotification = false;
4592
+ mcpToolPrefix = CHAT_MCP_TOOL_PREFIX2;
4593
+ busyDeliveryMode = "none";
4594
+ terminateProcessOnTurnEnd = true;
4595
+ deferSpawnUntilMessage = true;
4596
+ usesSlockCliForCommunication = true;
4597
+ sessionId = null;
4598
+ sessionAnnounced = false;
4599
+ apiKeyErrorAnnounced = false;
4600
+ turnEnded = false;
4601
+ assistantTextByMessageId = /* @__PURE__ */ new Map();
4602
+ shouldDeferWakeMessage(message) {
4603
+ return isSystemFirstMessageTask2(message);
4604
+ }
4605
+ probe() {
4606
+ return probePi();
4607
+ }
4608
+ async detectModels() {
4609
+ return null;
4610
+ }
4611
+ spawn(ctx) {
4612
+ this.sessionId = ctx.config.sessionId || null;
4613
+ this.sessionAnnounced = false;
4614
+ this.apiKeyErrorAnnounced = false;
4615
+ this.turnEnded = false;
4616
+ this.assistantTextByMessageId.clear();
4617
+ const unsupportedMessage = unsupportedPiVersionMessage(PI_SDK_VERSION);
4618
+ if (unsupportedMessage) throw new Error(unsupportedMessage);
4619
+ const launch = buildPiLaunchOptions(ctx);
4620
+ const proc = spawn8(launch.command, launch.args, {
4621
+ cwd: ctx.workingDirectory,
4622
+ stdio: ["pipe", "pipe", "pipe"],
4623
+ env: launch.env,
4624
+ shell: false
4625
+ });
4626
+ proc.stdin?.end();
4627
+ return { process: proc };
4628
+ }
4629
+ parseLine(line) {
4630
+ let event;
4631
+ try {
4632
+ event = JSON.parse(line);
4633
+ } catch {
4634
+ if (this.apiKeyErrorAnnounced) return [];
4635
+ const message = apiKeyErrorMessage(line);
4636
+ if (!message) return [];
4637
+ this.apiKeyErrorAnnounced = true;
4638
+ this.turnEnded = true;
4639
+ return [
4640
+ { kind: "error", message },
4641
+ { kind: "turn_end", sessionId: this.sessionId || void 0 }
4642
+ ];
4643
+ }
4644
+ const events = [];
4645
+ if (event.type === "session" && event.id) {
4646
+ this.sessionId = event.id;
4647
+ }
4648
+ if (!this.sessionAnnounced && this.sessionId) {
4649
+ events.push({ kind: "session_init", sessionId: this.sessionId });
4650
+ this.sessionAnnounced = true;
4651
+ }
4652
+ switch (event.type) {
4653
+ case "agent_start":
4654
+ case "turn_start":
4655
+ events.push({ kind: "thinking", text: "" });
4656
+ break;
4657
+ case "message_update":
4658
+ case "message_end":
4659
+ if (event.message?.role === "assistant") {
4660
+ const key = event.message.id || "current";
4661
+ const currentText = contentText(event.message.content);
4662
+ const previousText = this.assistantTextByMessageId.get(key) ?? "";
4663
+ if (currentText.length > previousText.length && currentText.startsWith(previousText)) {
4664
+ events.push({ kind: "text", text: currentText.slice(previousText.length) });
4665
+ } else if (currentText && currentText !== previousText) {
4666
+ events.push({ kind: "text", text: currentText });
4667
+ }
4668
+ this.assistantTextByMessageId.set(key, currentText);
4669
+ if (event.message.stopReason === "error" || event.message.stopReason === "aborted") {
4670
+ events.push({ kind: "error", message: event.message.errorMessage || `Request ${event.message.stopReason}` });
4671
+ }
4672
+ }
4673
+ break;
4674
+ case "tool_execution_start":
4675
+ events.push({
4676
+ kind: "tool_call",
4677
+ name: event.toolName || "unknown_tool",
4678
+ input: event.args
4679
+ });
4680
+ break;
4681
+ case "tool_execution_end":
4682
+ events.push({
4683
+ kind: "tool_output",
4684
+ name: event.toolName || "unknown_tool"
4685
+ });
4686
+ if (event.isError) {
4687
+ events.push({ kind: "error", message: `Pi tool ${event.toolName || "unknown_tool"} failed` });
4688
+ }
4689
+ break;
4690
+ case "compaction_start":
4691
+ events.push({ kind: "compaction_started" });
4692
+ break;
4693
+ case "compaction_end":
4694
+ events.push({ kind: "compaction_finished" });
4695
+ if (event.errorMessage) events.push({ kind: "error", message: event.errorMessage });
4696
+ break;
4697
+ case "turn_end":
4698
+ case "agent_end":
4699
+ if (!this.turnEnded) {
4700
+ events.push({ kind: "turn_end", sessionId: this.sessionId || void 0 });
4701
+ this.turnEnded = true;
4702
+ }
4703
+ break;
4704
+ }
4705
+ return events;
4706
+ }
4707
+ encodeStdinMessage(_text, _sessionId, _opts) {
4708
+ return null;
4709
+ }
4710
+ buildSystemPrompt(config, _agentId) {
4711
+ return buildPiSystemPrompt(config);
4712
+ }
4713
+ };
4714
+
3983
4715
  // src/drivers/index.ts
3984
4716
  var driverFactories = {
3985
4717
  claude: () => new ClaudeDriver(),
@@ -3988,7 +4720,8 @@ var driverFactories = {
3988
4720
  cursor: () => new CursorDriver(),
3989
4721
  gemini: () => new GeminiDriver(),
3990
4722
  kimi: () => new KimiDriver(),
3991
- opencode: () => new OpenCodeDriver()
4723
+ opencode: () => new OpenCodeDriver(),
4724
+ pi: () => new PiDriver()
3992
4725
  };
3993
4726
  function getDriver(runtimeId) {
3994
4727
  const createDriver = driverFactories[runtimeId];
@@ -4001,7 +4734,7 @@ function getDriver(runtimeId) {
4001
4734
 
4002
4735
  // src/workspaces.ts
4003
4736
  import { readdir, rm, stat } from "fs/promises";
4004
- import path11 from "path";
4737
+ import path12 from "path";
4005
4738
  function isValidWorkspaceDirectoryName(directoryName) {
4006
4739
  return !directoryName.includes("/") && !directoryName.includes("\\") && !directoryName.includes("..");
4007
4740
  }
@@ -4009,7 +4742,7 @@ function resolveWorkspaceDirectoryPath(dataDir, directoryName) {
4009
4742
  if (!isValidWorkspaceDirectoryName(directoryName)) {
4010
4743
  return null;
4011
4744
  }
4012
- return path11.join(dataDir, directoryName);
4745
+ return path12.join(dataDir, directoryName);
4013
4746
  }
4014
4747
  function emptyWorkspaceDirectorySummary(latestMtime = /* @__PURE__ */ new Date(0)) {
4015
4748
  return {
@@ -4058,7 +4791,7 @@ async function summarizeWorkspaceDirectory(dirPath) {
4058
4791
  return summary;
4059
4792
  }
4060
4793
  const childSummaries = await Promise.all(
4061
- entries.map((entry) => summarizeWorkspaceEntry(path11.join(dirPath, entry.name), entry))
4794
+ entries.map((entry) => summarizeWorkspaceEntry(path12.join(dirPath, entry.name), entry))
4062
4795
  );
4063
4796
  for (const childSummary of childSummaries) {
4064
4797
  summary = mergeWorkspaceDirectorySummaries(summary, childSummary);
@@ -4077,7 +4810,7 @@ async function scanWorkspaceDirectories(dataDir) {
4077
4810
  if (!entry.isDirectory()) {
4078
4811
  return null;
4079
4812
  }
4080
- const dirPath = path11.join(dataDir, entry.name);
4813
+ const dirPath = path12.join(dataDir, entry.name);
4081
4814
  try {
4082
4815
  const summary = await summarizeWorkspaceDirectory(dirPath);
4083
4816
  return {
@@ -4160,6 +4893,7 @@ function classifyRuntimeError(message, httpStatus) {
4160
4893
  return "ProviderApiError";
4161
4894
  }
4162
4895
  if (/\btimeout|timed out\b/i.test(message)) return "TimeoutError";
4896
+ if (/stream closed before response\.completed|error decoding response body/i.test(message)) return "ProviderStreamError";
4163
4897
  if (/\brate.?limit|too many requests\b/i.test(message)) return "RateLimitError";
4164
4898
  if (/\bnot found\b/i.test(message)) return "NotFoundError";
4165
4899
  return "RuntimeError";
@@ -4334,12 +5068,12 @@ function findSessionJsonl(root, predicate) {
4334
5068
  for (const entry of entries) {
4335
5069
  if (++visited > maxEntries) return null;
4336
5070
  if (!entry.isFile() || !predicate(entry.name)) continue;
4337
- return path12.join(dir, entry.name);
5071
+ return path13.join(dir, entry.name);
4338
5072
  }
4339
5073
  for (const entry of entries) {
4340
5074
  if (++visited > maxEntries) return null;
4341
5075
  if (!entry.isDirectory()) continue;
4342
- const found = visit(path12.join(dir, entry.name), depth - 1);
5076
+ const found = visit(path13.join(dir, entry.name), depth - 1);
4343
5077
  if (found) return found;
4344
5078
  }
4345
5079
  return null;
@@ -4352,10 +5086,10 @@ function safeSessionFilename(value) {
4352
5086
  }
4353
5087
  function writeRuntimeSessionHandoff(runtime, sessionId, fallbackDir) {
4354
5088
  try {
4355
- const dir = path12.join(fallbackDir, ".slock", "runtime-sessions");
4356
- mkdirSync4(dir, { recursive: true });
4357
- const filePath = path12.join(dir, `${runtime}-${safeSessionFilename(sessionId)}.jsonl`);
4358
- writeFileSync7(filePath, JSON.stringify({
5089
+ const dir = path13.join(fallbackDir, ".slock", "runtime-sessions");
5090
+ mkdirSync5(dir, { recursive: true });
5091
+ const filePath = path13.join(dir, `${runtime}-${safeSessionFilename(sessionId)}.jsonl`);
5092
+ writeFileSync8(filePath, JSON.stringify({
4359
5093
  type: "runtime_session_handoff",
4360
5094
  runtime,
4361
5095
  sessionId,
@@ -4374,7 +5108,7 @@ function writeRuntimeSessionHandoff(runtime, sessionId, fallbackDir) {
4374
5108
  }
4375
5109
  }
4376
5110
  function resolveRuntimeSessionRef(runtime, sessionId, homeDir = os6.homedir(), fallbackDir) {
4377
- const directPath = path12.isAbsolute(sessionId) ? sessionId : null;
5111
+ const directPath = path13.isAbsolute(sessionId) ? sessionId : null;
4378
5112
  if (directPath) {
4379
5113
  try {
4380
5114
  if (statSync2(directPath).isFile()) {
@@ -4383,7 +5117,7 @@ function resolveRuntimeSessionRef(runtime, sessionId, homeDir = os6.homedir(), f
4383
5117
  } catch {
4384
5118
  }
4385
5119
  }
4386
- const resolvedPath = runtime === "claude" ? findSessionJsonl(path12.join(homeDir, ".claude", "projects"), (filename) => filename === `${sessionId}.jsonl`) : runtime === "codex" ? findSessionJsonl(path12.join(homeDir, ".codex", "sessions"), (filename) => filename.endsWith(".jsonl") && filename.includes(sessionId)) : null;
5120
+ const resolvedPath = runtime === "claude" ? findSessionJsonl(path13.join(homeDir, ".claude", "projects"), (filename) => filename === `${sessionId}.jsonl`) : runtime === "codex" ? findSessionJsonl(path13.join(homeDir, ".codex", "sessions"), (filename) => filename.endsWith(".jsonl") && filename.includes(sessionId)) : null;
4387
5121
  if (!resolvedPath && fallbackDir) {
4388
5122
  const fallback = writeRuntimeSessionHandoff(runtime, sessionId, fallbackDir);
4389
5123
  if (fallback) return fallback;
@@ -5034,12 +5768,21 @@ function classifyTerminalFailure(ap) {
5034
5768
  ].filter((value) => !!value);
5035
5769
  for (const text of candidates) {
5036
5770
  const lower = text.toLowerCase();
5037
- if (lower.includes("usage limit") || lower.includes("quota exceeded") || lower.includes("quota limit") || lower.includes("budget limit exceeded") || lower.includes("usage not included in your plan") || lower.includes("modelnotfounderror") || lower.includes("requested entity was not found") || lower.includes("model deprecated") || lower.includes("model not found")) {
5771
+ if (lower.includes("usage limit") || lower.includes("quota exceeded") || lower.includes("quota limit") || lower.includes("budget limit exceeded") || lower.includes("usage not included in your plan") || lower.includes("modelnotfounderror") || lower.includes("requested entity was not found") || lower.includes("model deprecated") || lower.includes("model not found") || isProviderStreamFailureText(text)) {
5038
5772
  return text;
5039
5773
  }
5040
5774
  }
5041
5775
  return null;
5042
5776
  }
5777
+ function isProviderStreamFailureText(text) {
5778
+ return /stream closed before response\.completed|error decoding response body/i.test(text);
5779
+ }
5780
+ function isCodexProviderReconnectLog(text) {
5781
+ return /Reconnecting\.\.\.\s*\d+\s*\/\s*\d+/i.test(text);
5782
+ }
5783
+ function isCodexBenignTransportLog(text) {
5784
+ return /Falling back from WebSockets/i.test(text);
5785
+ }
5043
5786
  function hasDirectStdinRecoveryEvidence(ap) {
5044
5787
  const candidates = [
5045
5788
  ap.lastRuntimeError,
@@ -5234,10 +5977,10 @@ function getMessageDeliveryText(driver) {
5234
5977
  function getBusyDeliveryNote(driver) {
5235
5978
  if (!driver.supportsStdinNotification) return "";
5236
5979
  if (driver.busyDeliveryMode === "direct") {
5237
- return "\n\nNote: While you are busy, new messages may be delivered directly into your active turn. Handle them when appropriate and keep working.";
5980
+ return "\n\nNote: While you are busy, the daemon may write batched inbox-count notifications into your active turn. Call check_messages to read the pending messages before context-sensitive side effects.";
5238
5981
  }
5239
5982
  if (driver.busyDeliveryMode === "gated") {
5240
- return "\n\nNote: While you are busy, new messages may be delivered at runtime-observed safe boundaries in your active turn. If no safe boundary is available, they will be delivered after the current turn ends.";
5983
+ return "\n\nNote: While you are busy, the daemon may write batched inbox-count notifications into your active turn at runtime-observed safe boundaries. Call check_messages to read the pending messages before context-sensitive side effects.";
5241
5984
  }
5242
5985
  return "\n\nNote: While you are busy, you may receive [System notification: ...] messages. Finish your current step, then call check_messages to check for messages.";
5243
5986
  }
@@ -5313,13 +6056,16 @@ var AgentProcessManager = class _AgentProcessManager {
5313
6056
  getVisibleBoundary(agentId, target) {
5314
6057
  return this.agentVisibleBoundaries.get(agentId)?.get(target);
5315
6058
  }
5316
- pendingVisibleMessages(agentId, target) {
5317
- const collect = (messages) => (messages ?? []).filter((message) => formatMessageTarget(message) === target && typeof message.seq === "number" && message.seq > 0);
6059
+ allPendingVisibleMessages(agentId) {
6060
+ const collect = (messages) => (messages ?? []).filter((message) => typeof message.seq === "number" && message.seq > 0);
5318
6061
  return [
5319
6062
  ...collect(this.agents.get(agentId)?.inbox),
5320
6063
  ...collect(this.startingInboxes.get(agentId))
5321
6064
  ];
5322
6065
  }
6066
+ pendingVisibleMessages(agentId, target) {
6067
+ return this.allPendingVisibleMessages(agentId).filter((message) => formatMessageTarget(message) === target);
6068
+ }
5323
6069
  /**
5324
6070
  * Single-inbox consume point for messages that have been rendered into the
5325
6071
  * agent-visible input surface. Daemon pending inbox is only a runner cache:
@@ -5390,8 +6136,71 @@ var AgentProcessManager = class _AgentProcessManager {
5390
6136
  return {
5391
6137
  getBoundary: (target) => this.getVisibleBoundary(agentId, target),
5392
6138
  getPendingMessages: (target) => this.pendingVisibleMessages(agentId, target),
5393
- consumeVisibleMessages: (input) => this.consumeVisibleMessages(agentId, input)
6139
+ getAllPendingMessages: () => this.allPendingVisibleMessages(agentId),
6140
+ consumeVisibleMessages: (input) => this.consumeVisibleMessages(agentId, input),
6141
+ recordDrainOutcome: (input) => {
6142
+ this.recordDaemonTrace("daemon.agent.drain.outcome", {
6143
+ agentId,
6144
+ source: input.source,
6145
+ since_cursor_kind: input.sinceCursorKind ?? void 0,
6146
+ notified_count: input.notifiedCount,
6147
+ drained_count: input.drainedCount,
6148
+ has_more: input.hasMore
6149
+ });
6150
+ },
6151
+ recordProxyFailure: (input) => this.recordAgentProxyFailure(agentId, input),
6152
+ recordFreshnessDecision: (input) => {
6153
+ this.recordDaemonTrace("daemon.agent.inbox.freshness_decision", {
6154
+ agentId,
6155
+ action: input.action,
6156
+ decision: input.decision,
6157
+ target: input.target,
6158
+ inbox_trust_state: input.inboxTrustState,
6159
+ reason: input.reason,
6160
+ pending_count: input.pendingCount,
6161
+ pending_max_seq: input.pendingMaxSeq,
6162
+ model_seen_seq: input.modelSeenSeq,
6163
+ held_message_count: input.heldMessageCount,
6164
+ omitted_message_count: input.omittedMessageCount
6165
+ });
6166
+ this.recordFreshnessDecisionActivity(agentId, input);
6167
+ }
6168
+ };
6169
+ }
6170
+ recordAgentProxyFailure(agentId, input) {
6171
+ this.recordDaemonTrace("daemon.agent.proxy.failure", {
6172
+ agentId,
6173
+ method: input.method,
6174
+ path: input.pathname,
6175
+ query_keys: input.queryKeys,
6176
+ error_name: input.errorName,
6177
+ error_message: input.errorMessage
6178
+ }, "error");
6179
+ }
6180
+ recordFreshnessDecisionActivity(agentId, input) {
6181
+ if (input.decision !== "local_hold" && input.decision !== "syncing_hold") return;
6182
+ const ap = this.agents.get(agentId);
6183
+ const messageCount = input.pendingCount ?? input.heldMessageCount ?? 0;
6184
+ const title = input.action === "send" ? "Send held by freshness check" : input.action === "task_claim" ? "Task claim held by freshness check" : "Task update held by freshness check";
6185
+ const entry = {
6186
+ kind: "slock_action",
6187
+ title,
6188
+ text: [
6189
+ input.target ? `target: ${input.target}` : null,
6190
+ `new messages: ${messageCount} newer message${messageCount === 1 ? "" : "s"}`,
6191
+ `decision: ${input.decision === "syncing_hold" ? "syncing hold" : "local hold"}; review the newer context before retrying`
6192
+ ].filter((line) => Boolean(line)).join("\n")
5394
6193
  };
6194
+ if (ap) ap.activityClientSeq += 1;
6195
+ this.sendToServer({
6196
+ type: "agent:activity",
6197
+ agentId,
6198
+ activity: ap?.lastActivity || "online",
6199
+ detail: ap?.lastActivityDetail || "",
6200
+ entries: [entry],
6201
+ launchId: ap?.launchId || void 0,
6202
+ clientSeq: ap?.activityClientSeq
6203
+ });
5395
6204
  }
5396
6205
  recordDaemonTrace(name, attrs, status = "ok", parentTraceparent) {
5397
6206
  const span = this.tracer.startSpan(name, {
@@ -5617,26 +6426,26 @@ var AgentProcessManager = class _AgentProcessManager {
5617
6426
  this.recordDaemonTrace("daemon.agent.spawn.started", this.startQueueTraceAttrs(agentId, config, wakeMessage, unreadSummary, resumePrompt, launchId));
5618
6427
  try {
5619
6428
  const driver = this.driverResolver(config.runtime || "claude");
5620
- const agentDataDir = path12.join(this.dataDir, agentId);
6429
+ const agentDataDir = path13.join(this.dataDir, agentId);
5621
6430
  await mkdir(agentDataDir, { recursive: true });
5622
6431
  const runtimeConfig = withLocalRuntimeContext(config, agentId, agentDataDir);
5623
- const memoryMdPath = path12.join(agentDataDir, "MEMORY.md");
6432
+ const memoryMdPath = path13.join(agentDataDir, "MEMORY.md");
5624
6433
  try {
5625
6434
  await access(memoryMdPath);
5626
6435
  } catch {
5627
6436
  const initialMemoryMd = buildInitialMemoryMd(runtimeConfig);
5628
6437
  await writeFile(memoryMdPath, initialMemoryMd);
5629
6438
  }
5630
- const notesDir = path12.join(agentDataDir, "notes");
6439
+ const notesDir = path13.join(agentDataDir, "notes");
5631
6440
  await mkdir(notesDir, { recursive: true });
5632
6441
  if (getOnboardingSeedMode(config) === FIRST_CINDY_SEED_MODE) {
5633
6442
  const seedFiles = buildOnboardingSeedFiles();
5634
6443
  for (const { relativePath, content } of seedFiles) {
5635
- const fullPath = path12.join(agentDataDir, relativePath);
6444
+ const fullPath = path13.join(agentDataDir, relativePath);
5636
6445
  try {
5637
6446
  await access(fullPath);
5638
6447
  } catch {
5639
- await mkdir(path12.dirname(fullPath), { recursive: true });
6448
+ await mkdir(path13.dirname(fullPath), { recursive: true });
5640
6449
  await writeFile(fullPath, content);
5641
6450
  }
5642
6451
  }
@@ -5825,8 +6634,24 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5825
6634
  proc.stderr?.on("data", (chunk) => {
5826
6635
  const text = chunk.toString().trim();
5827
6636
  if (!text) return;
5828
- if (/Reconnecting\.\.\.|Falling back from WebSockets/i.test(text)) return;
5829
6637
  const current = this.agents.get(agentId);
6638
+ if (driver.id === "codex" && isCodexProviderReconnectLog(text)) {
6639
+ if (current) {
6640
+ current.recentStderr = pushRecentStderr(current.recentStderr, text);
6641
+ }
6642
+ this.recordDaemonTrace("daemon.agent.provider_reconnect", {
6643
+ agentId,
6644
+ launchId: current?.launchId || void 0,
6645
+ runtime: config.runtime,
6646
+ model: config.model
6647
+ });
6648
+ this.broadcastActivity(agentId, "working", "Codex reconnecting to provider\u2026", [
6649
+ { kind: "text", text }
6650
+ ]);
6651
+ logger.info(`[Agent ${agentId} stderr]: ${text}`);
6652
+ return;
6653
+ }
6654
+ if (driver.id === "codex" && isCodexBenignTransportLog(text)) return;
5830
6655
  if (current) {
5831
6656
  current.recentStderr = pushRecentStderr(current.recentStderr, text);
5832
6657
  }
@@ -5970,10 +6795,20 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
5970
6795
  }
5971
6796
  this.broadcastActivity(agentId, "online", "Process idle");
5972
6797
  } else {
5973
- this.idleAgentConfigs.delete(agentId);
5974
6798
  const reason = formatCrashReason(finalCode, finalSignal, ap);
5975
- logger.error(`[Agent ${agentId}] Process crashed (${reason}) \u2014 marking inactive`);
5976
- this.sendAgentStatus(agentId, "inactive", ap.launchId);
6799
+ if (terminalFailureDetail && isProviderStreamFailureText(terminalFailureDetail)) {
6800
+ this.idleAgentConfigs.set(agentId, {
6801
+ config: { ...ap.config, sessionId: ap.sessionId },
6802
+ sessionId: ap.sessionId,
6803
+ launchId: ap.launchId
6804
+ });
6805
+ logger.warn(`[Agent ${agentId}] Recoverable provider stream failure (${reason}) \u2014 keeping agent wakeable`);
6806
+ this.sendAgentStatus(agentId, "active", ap.launchId);
6807
+ } else {
6808
+ this.idleAgentConfigs.delete(agentId);
6809
+ logger.error(`[Agent ${agentId}] Process crashed (${reason}) \u2014 marking inactive`);
6810
+ this.sendAgentStatus(agentId, "inactive", ap.launchId);
6811
+ }
5977
6812
  if (terminalFailureDetail) {
5978
6813
  this.broadcastActivity(
5979
6814
  agentId,
@@ -6446,6 +7281,11 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
6446
7281
  }
6447
7282
  if (ap.driver.busyDeliveryMode === "gated") {
6448
7283
  ap.pendingNotificationCount++;
7284
+ if (!ap.notificationTimer) {
7285
+ ap.notificationTimer = setTimeout(() => {
7286
+ this.sendStdinNotification(agentId);
7287
+ }, 3e3);
7288
+ }
6449
7289
  this.recordGatedSteeringEvent(agentId, ap, "buffer", {
6450
7290
  reason: "busy_message",
6451
7291
  pendingMessages: ap.inbox.length
@@ -6458,7 +7298,8 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
6458
7298
  session_id_present: true,
6459
7299
  launchId: ap.launchId || void 0,
6460
7300
  inbox_count: ap.inbox.length,
6461
- pending_notification_count: ap.pendingNotificationCount
7301
+ pending_notification_count: ap.pendingNotificationCount,
7302
+ notification_timer_present: Boolean(ap.notificationTimer)
6462
7303
  }));
6463
7304
  return true;
6464
7305
  }
@@ -6481,7 +7322,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
6481
7322
  return true;
6482
7323
  }
6483
7324
  async resetWorkspace(agentId) {
6484
- const agentDataDir = path12.join(this.dataDir, agentId);
7325
+ const agentDataDir = path13.join(this.dataDir, agentId);
6485
7326
  try {
6486
7327
  await rm2(agentDataDir, { recursive: true, force: true });
6487
7328
  logger.info(`[Agent ${agentId}] Workspace reset complete (${agentDataDir})`);
@@ -6518,7 +7359,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
6518
7359
  return result;
6519
7360
  }
6520
7361
  buildRuntimeProfileReport(agentId, config, sessionId, launchId) {
6521
- const workspacePath = path12.join(this.dataDir, agentId);
7362
+ const workspacePath = path13.join(this.dataDir, agentId);
6522
7363
  return {
6523
7364
  agentId,
6524
7365
  launchId,
@@ -6775,7 +7616,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
6775
7616
  }
6776
7617
  // Workspace file browsing
6777
7618
  async getFileTree(agentId, dirPath) {
6778
- const agentDir = path12.join(this.dataDir, agentId);
7619
+ const agentDir = path13.join(this.dataDir, agentId);
6779
7620
  try {
6780
7621
  await stat2(agentDir);
6781
7622
  } catch {
@@ -6783,8 +7624,8 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
6783
7624
  }
6784
7625
  let targetDir = agentDir;
6785
7626
  if (dirPath) {
6786
- const resolved = path12.resolve(agentDir, dirPath);
6787
- if (!resolved.startsWith(agentDir + path12.sep) && resolved !== agentDir) {
7627
+ const resolved = path13.resolve(agentDir, dirPath);
7628
+ if (!resolved.startsWith(agentDir + path13.sep) && resolved !== agentDir) {
6788
7629
  return [];
6789
7630
  }
6790
7631
  targetDir = resolved;
@@ -6792,14 +7633,14 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
6792
7633
  return this.listDirectoryChildren(targetDir, agentDir);
6793
7634
  }
6794
7635
  async readFile(agentId, filePath) {
6795
- const agentDir = path12.join(this.dataDir, agentId);
6796
- const resolved = path12.resolve(agentDir, filePath);
6797
- if (!resolved.startsWith(agentDir + path12.sep) && resolved !== agentDir) {
7636
+ const agentDir = path13.join(this.dataDir, agentId);
7637
+ const resolved = path13.resolve(agentDir, filePath);
7638
+ if (!resolved.startsWith(agentDir + path13.sep) && resolved !== agentDir) {
6798
7639
  throw new Error("Access denied");
6799
7640
  }
6800
7641
  const info = await stat2(resolved);
6801
7642
  if (info.isDirectory()) throw new Error("Cannot read a directory");
6802
- const ext = path12.extname(resolved).toLowerCase();
7643
+ const ext = path13.extname(resolved).toLowerCase();
6803
7644
  if (WORKSPACE_TEXT_EXTENSIONS.has(ext) || ext === "") {
6804
7645
  if (info.size > WORKSPACE_TEXT_FILE_MAX_BYTES) throw new Error("File too large");
6805
7646
  const content = await readFile(resolved, "utf-8");
@@ -6834,13 +7675,13 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
6834
7675
  const agent = this.agents.get(agentId);
6835
7676
  const runtime = runtimeHint || agent?.config.runtime || "claude";
6836
7677
  const home = os6.homedir();
6837
- const workspaceDir = path12.join(this.dataDir, agentId);
7678
+ const workspaceDir = path13.join(this.dataDir, agentId);
6838
7679
  const paths = _AgentProcessManager.SKILL_PATHS[runtime] || _AgentProcessManager.SKILL_PATHS.claude;
6839
7680
  const globalResults = await Promise.all(
6840
- paths.global.map((p) => this.scanSkillsDir(path12.join(home, p)))
7681
+ paths.global.map((p) => this.scanSkillsDir(path13.join(home, p)))
6841
7682
  );
6842
7683
  const workspaceResults = await Promise.all(
6843
- paths.workspace.map((p) => this.scanSkillsDir(path12.join(workspaceDir, p)))
7684
+ paths.workspace.map((p) => this.scanSkillsDir(path13.join(workspaceDir, p)))
6844
7685
  );
6845
7686
  const dedup = (skills) => {
6846
7687
  const seen = /* @__PURE__ */ new Set();
@@ -6869,7 +7710,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
6869
7710
  const skills = [];
6870
7711
  for (const entry of entries) {
6871
7712
  if (entry.isDirectory() || entry.isSymbolicLink()) {
6872
- const skillMd = path12.join(dir, entry.name, "SKILL.md");
7713
+ const skillMd = path13.join(dir, entry.name, "SKILL.md");
6873
7714
  try {
6874
7715
  const content = await readFile(skillMd, "utf-8");
6875
7716
  const skill = this.parseSkillMd(entry.name, content);
@@ -6880,7 +7721,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
6880
7721
  } else if (entry.name.endsWith(".md")) {
6881
7722
  const cmdName = entry.name.replace(/\.md$/, "");
6882
7723
  try {
6883
- const content = await readFile(path12.join(dir, entry.name), "utf-8");
7724
+ const content = await readFile(path13.join(dir, entry.name), "utf-8");
6884
7725
  const skill = this.parseSkillMd(cmdName, content);
6885
7726
  skill.sourcePath = dir;
6886
7727
  skills.push(skill);
@@ -7283,11 +8124,17 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
7283
8124
  if (reason !== "turn_end") {
7284
8125
  if (ap.gatedSteering.toolBoundaryFlushDisabled) return false;
7285
8126
  if (ap.gatedSteering.compacting || ap.gatedSteering.outstandingToolUses > 0) return false;
8127
+ this.recordGatedSteeringEvent(agentId, ap, "notify", { reason, pendingMessages: ap.inbox.length });
8128
+ return this.sendStdinNotification(agentId);
7286
8129
  }
7287
8130
  const pendingMessages = ap.inbox.length;
7288
8131
  const pendingNotificationCount = ap.pendingNotificationCount;
7289
8132
  const nextMessages = ap.inbox.splice(0, ap.inbox.length);
7290
8133
  ap.pendingNotificationCount = 0;
8134
+ if (ap.notificationTimer) {
8135
+ clearTimeout(ap.notificationTimer);
8136
+ ap.notificationTimer = null;
8137
+ }
7291
8138
  ap.gatedSteering.lastFlushReason = reason;
7292
8139
  if (reason !== "turn_end") {
7293
8140
  ap.gatedSteering.inFlightBatch = {
@@ -7315,28 +8162,6 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
7315
8162
  }
7316
8163
  flushCompactionBoundaryMessages(agentId, ap) {
7317
8164
  if (!ap.sessionId || !ap.driver.supportsStdinNotification || ap.inbox.length === 0) return false;
7318
- if (ap.driver.busyDeliveryMode === "gated") {
7319
- return this.tryFlushGatedSteering(agentId, ap, "compaction_finished");
7320
- }
7321
- if (ap.driver.busyDeliveryMode === "direct") {
7322
- const pendingMessages = ap.inbox.length;
7323
- const pendingNotificationCount = ap.pendingNotificationCount;
7324
- const nextMessages = ap.inbox.splice(0, ap.inbox.length);
7325
- ap.pendingNotificationCount = 0;
7326
- if (ap.notificationTimer) {
7327
- clearTimeout(ap.notificationTimer);
7328
- ap.notificationTimer = null;
7329
- }
7330
- this.recordRuntimeTraceEvent(agentId, ap, "runtime.compaction_boundary.flush", {
7331
- mode: "direct",
7332
- messageCount: nextMessages.length
7333
- });
7334
- if (this.deliverMessagesViaStdin(agentId, ap, nextMessages, "busy")) {
7335
- return true;
7336
- }
7337
- ap.pendingNotificationCount += pendingNotificationCount || pendingMessages;
7338
- return false;
7339
- }
7340
8165
  if (ap.pendingNotificationCount > 0) {
7341
8166
  if (ap.notificationTimer) {
7342
8167
  clearTimeout(ap.notificationTimer);
@@ -7555,6 +8380,10 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
7555
8380
  if (ap.inbox.length > 0 && ap.driver.supportsStdinNotification && ap.sessionId) {
7556
8381
  const nextMessages = ap.inbox.splice(0, ap.inbox.length);
7557
8382
  ap.pendingNotificationCount = 0;
8383
+ if (ap.notificationTimer) {
8384
+ clearTimeout(ap.notificationTimer);
8385
+ ap.notificationTimer = null;
8386
+ }
7558
8387
  if (ap.driver.busyDeliveryMode === "gated") {
7559
8388
  ap.gatedSteering.lastFlushReason = "turn_end";
7560
8389
  this.recordGatedSteeringEvent(agentId, ap, "flush", {
@@ -7698,13 +8527,16 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
7698
8527
  /** Send a batched notification to the agent via stdin about pending messages */
7699
8528
  sendStdinNotification(agentId) {
7700
8529
  const ap = this.agents.get(agentId);
7701
- if (!ap) return;
8530
+ if (!ap) return false;
7702
8531
  const count = ap.pendingNotificationCount;
7703
8532
  ap.pendingNotificationCount = 0;
7704
- ap.notificationTimer = null;
7705
- if (count === 0) return;
7706
- if (ap.isIdle) return;
7707
- if (!ap.sessionId) return;
8533
+ if (ap.notificationTimer) {
8534
+ clearTimeout(ap.notificationTimer);
8535
+ ap.notificationTimer = null;
8536
+ }
8537
+ if (count === 0) return false;
8538
+ if (ap.isIdle) return false;
8539
+ if (!ap.sessionId) return false;
7708
8540
  if (ap.gatedSteering.compacting) {
7709
8541
  this.recordRuntimeTraceEvent(agentId, ap, "runtime.compaction_boundary.delivery_suppressed", {
7710
8542
  pendingNotificationCount: count,
@@ -7715,30 +8547,12 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
7715
8547
  logger.info(
7716
8548
  `[Agent ${agentId}] Suppressing stdin delivery until context compaction finishes; pending=${ap.inbox.length}`
7717
8549
  );
7718
- return;
7719
- }
7720
- if (ap.driver.busyDeliveryMode === "gated") {
7721
- this.recordGatedSteeringEvent(agentId, ap, "suppress", {
7722
- reason: "timer_notification_not_safe_boundary",
7723
- pendingNotificationCount: count
7724
- });
7725
- ap.pendingNotificationCount += count;
7726
- logger.info(
7727
- `[Agent ${agentId}] Suppressing raw busy stdin notification until Claude gated steering boundary; pending=${ap.inbox.length}`
7728
- );
7729
- return;
7730
- }
7731
- if (ap.driver.busyDeliveryMode === "direct" && ap.inbox.length > 0) {
7732
- const queuedMessages = ap.inbox.splice(0, ap.inbox.length);
7733
- console.log(`[Agent ${agentId}] Delivering queued message via stdin while busy`);
7734
- if (this.deliverMessagesViaStdin(agentId, ap, queuedMessages, "busy")) {
7735
- return;
7736
- }
7737
- ap.pendingNotificationCount += count;
7738
- return;
8550
+ return false;
7739
8551
  }
7740
- const notification = `[System notification: You have ${count} new message${count > 1 ? "s" : ""} waiting. Call check_messages to read ${count > 1 ? "them" : "it"} when you're ready.]`;
7741
- logger.info(`[Agent ${agentId}] Sending stdin notification: ${count} message(s)`);
8552
+ const inboxCount = ap.inbox.length;
8553
+ if (inboxCount === 0) return false;
8554
+ const notification = `[System notification: You have ${inboxCount} pending inbox message${inboxCount > 1 ? "s" : ""}. Call check_messages to read ${inboxCount > 1 ? "them" : "it"} when you're ready.]`;
8555
+ logger.info(`[Agent ${agentId}] Sending stdin notification: ${inboxCount} pending inbox message(s)`);
7742
8556
  const encoded = ap.driver.encodeStdinMessage(notification, ap.sessionId, { mode: "busy" });
7743
8557
  if (encoded) {
7744
8558
  ap.process.stdin?.write(encoded + "\n");
@@ -7750,9 +8564,12 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
7750
8564
  outcome: "written",
7751
8565
  mode: "busy",
7752
8566
  pending_notification_count: count,
8567
+ inbox_count: inboxCount,
7753
8568
  session_id_present: true
7754
8569
  });
8570
+ return true;
7755
8571
  } else {
8572
+ ap.pendingNotificationCount += count;
7756
8573
  this.recordDaemonTrace("daemon.agent.stdin_notification", {
7757
8574
  agentId,
7758
8575
  runtime: ap.config.runtime,
@@ -7761,8 +8578,10 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
7761
8578
  outcome: "encode_failed",
7762
8579
  mode: "busy",
7763
8580
  pending_notification_count: count,
8581
+ inbox_count: inboxCount,
7764
8582
  session_id_present: true
7765
8583
  }, "error");
8584
+ return false;
7766
8585
  }
7767
8586
  }
7768
8587
  /** Deliver a message to an agent via stdin, formatting it the same way as the MCP bridge */
@@ -7868,8 +8687,8 @@ ${RESPONSE_TARGET_HINT}`);
7868
8687
  const nodes = [];
7869
8688
  for (const entry of entries) {
7870
8689
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
7871
- const fullPath = path12.join(dir, entry.name);
7872
- const relativePath = path12.relative(rootDir, fullPath);
8690
+ const fullPath = path13.join(dir, entry.name);
8691
+ const relativePath = path13.relative(rootDir, fullPath);
7873
8692
  let info;
7874
8693
  try {
7875
8694
  info = await stat2(fullPath);
@@ -7998,10 +8817,12 @@ var DaemonConnection = class {
7998
8817
  messageKind = msg.type;
7999
8818
  this.markInbound(messageKind);
8000
8819
  this.resetWatchdog();
8001
- this.trace("daemon.connection.inbound_received", {
8002
- message_type: messageKind,
8003
- last_inbound_age_ms_bucket: "0"
8004
- });
8820
+ if (messageKind !== "ping") {
8821
+ this.trace("daemon.connection.inbound_received", {
8822
+ message_type: messageKind,
8823
+ last_inbound_age_ms_bucket: "0"
8824
+ });
8825
+ }
8005
8826
  this.options.onMessage(msg);
8006
8827
  } catch (err) {
8007
8828
  this.markInbound("invalid_json");
@@ -8172,9 +8993,9 @@ var ReminderCache = class {
8172
8993
 
8173
8994
  // src/machineLock.ts
8174
8995
  import { createHash as createHash3, randomUUID as randomUUID2 } from "crypto";
8175
- import { mkdirSync as mkdirSync5, readFileSync as readFileSync5, rmSync as rmSync3, statSync as statSync3, writeFileSync as writeFileSync8 } from "fs";
8996
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync5, rmSync as rmSync3, statSync as statSync3, writeFileSync as writeFileSync9 } from "fs";
8176
8997
  import os7 from "os";
8177
- import path13 from "path";
8998
+ import path14 from "path";
8178
8999
  var INCOMPLETE_LOCK_STALE_MS = 3e4;
8179
9000
  var DaemonMachineLockConflictError = class extends Error {
8180
9001
  code = "DAEMON_MACHINE_LOCK_HELD";
@@ -8196,7 +9017,7 @@ function resolveDefaultMachineStateRoot() {
8196
9017
  return resolveSlockHomePath("machines");
8197
9018
  }
8198
9019
  function ownerPath(lockDir) {
8199
- return path13.join(lockDir, "owner.json");
9020
+ return path14.join(lockDir, "owner.json");
8200
9021
  }
8201
9022
  function readOwner(lockDir) {
8202
9023
  try {
@@ -8226,13 +9047,13 @@ function acquireDaemonMachineLock(options) {
8226
9047
  const rootDir = options.rootDir ?? resolveDefaultMachineStateRoot();
8227
9048
  const fingerprint = apiKeyFingerprint(options.apiKey);
8228
9049
  const lockId = getDaemonMachineLockId(options.apiKey);
8229
- const machineDir = path13.join(rootDir, lockId);
8230
- const lockDir = path13.join(machineDir, "daemon.lock");
9050
+ const machineDir = path14.join(rootDir, lockId);
9051
+ const lockDir = path14.join(machineDir, "daemon.lock");
8231
9052
  const token = randomUUID2();
8232
- mkdirSync5(machineDir, { recursive: true });
9053
+ mkdirSync6(machineDir, { recursive: true });
8233
9054
  for (let attempt = 0; attempt < 2; attempt += 1) {
8234
9055
  try {
8235
- mkdirSync5(lockDir);
9056
+ mkdirSync6(lockDir);
8236
9057
  const owner = {
8237
9058
  pid: process.pid,
8238
9059
  token,
@@ -8242,7 +9063,7 @@ function acquireDaemonMachineLock(options) {
8242
9063
  apiKeyFingerprint: fingerprint.slice(0, 16)
8243
9064
  };
8244
9065
  try {
8245
- writeFileSync8(ownerPath(lockDir), `${JSON.stringify(owner, null, 2)}
9066
+ writeFileSync9(ownerPath(lockDir), `${JSON.stringify(owner, null, 2)}
8246
9067
  `, { mode: 384 });
8247
9068
  } catch (err) {
8248
9069
  rmSync3(lockDir, { recursive: true, force: true });
@@ -8279,8 +9100,8 @@ function acquireDaemonMachineLock(options) {
8279
9100
  }
8280
9101
 
8281
9102
  // src/localTraceSink.ts
8282
- import { appendFileSync, mkdirSync as mkdirSync6, readdirSync as readdirSync3, rmSync as rmSync4, statSync as statSync4, writeFileSync as writeFileSync9 } from "fs";
8283
- import path14 from "path";
9103
+ import { appendFileSync, mkdirSync as mkdirSync7, readdirSync as readdirSync3, rmSync as rmSync4, statSync as statSync4, writeFileSync as writeFileSync10 } from "fs";
9104
+ import path15 from "path";
8284
9105
  var DEFAULT_MAX_FILE_BYTES = 5 * 1024 * 1024;
8285
9106
  var DEFAULT_MAX_FILE_AGE_MS = 5 * 60 * 1e3;
8286
9107
  var DEFAULT_MAX_FILES = 8;
@@ -8316,7 +9137,7 @@ var LocalRotatingTraceSink = class {
8316
9137
  currentSize = 0;
8317
9138
  sequence = 0;
8318
9139
  constructor(options) {
8319
- this.traceDir = path14.join(options.machineDir, "traces");
9140
+ this.traceDir = path15.join(options.machineDir, "traces");
8320
9141
  this.maxFileBytes = Math.max(1024, Math.floor(options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES));
8321
9142
  const baseAgeMs = Math.max(1e3, Math.floor(options.maxFileAgeMs ?? DEFAULT_MAX_FILE_AGE_MS));
8322
9143
  const ageJitterMs = Math.max(0, Math.floor(options.maxFileAgeJitterMs ?? 0));
@@ -8342,15 +9163,15 @@ var LocalRotatingTraceSink = class {
8342
9163
  return this.currentFile;
8343
9164
  }
8344
9165
  ensureFile(nextBytes) {
8345
- mkdirSync6(this.traceDir, { recursive: true, mode: 448 });
9166
+ mkdirSync7(this.traceDir, { recursive: true, mode: 448 });
8346
9167
  const nowMs = this.nowMsProvider();
8347
9168
  const shouldRotateForAge = this.currentFileOpenedAtMs !== null && nowMs - this.currentFileOpenedAtMs >= this.maxFileAgeMs;
8348
9169
  if (!this.currentFile || this.currentSize + nextBytes > this.maxFileBytes || shouldRotateForAge) {
8349
- this.currentFile = path14.join(
9170
+ this.currentFile = path15.join(
8350
9171
  this.traceDir,
8351
9172
  `daemon-trace-${safeTimestamp(nowMs)}-${process.pid}-${String(this.sequence++).padStart(4, "0")}.jsonl`
8352
9173
  );
8353
- writeFileSync9(this.currentFile, "", { flag: "a", mode: 384 });
9174
+ writeFileSync10(this.currentFile, "", { flag: "a", mode: 384 });
8354
9175
  this.currentSize = statSync4(this.currentFile).size;
8355
9176
  this.currentFileOpenedAtMs = nowMs;
8356
9177
  this.pruneOldFiles();
@@ -8361,7 +9182,7 @@ var LocalRotatingTraceSink = class {
8361
9182
  const excess = files.length - this.maxFiles;
8362
9183
  if (excess <= 0) return;
8363
9184
  for (const file of files.slice(0, excess)) {
8364
- rmSync4(path14.join(this.traceDir, file), { force: true });
9185
+ rmSync4(path15.join(this.traceDir, file), { force: true });
8365
9186
  }
8366
9187
  }
8367
9188
  };
@@ -8448,11 +9269,11 @@ function isDiagnosticErrorAttr(key) {
8448
9269
  import { createHash as createHash5, randomUUID as randomUUID3 } from "crypto";
8449
9270
  import { gzipSync } from "zlib";
8450
9271
  import { mkdir as mkdir2, readFile as readFile2, readdir as readdir3, stat as stat3, writeFile as writeFile2 } from "fs/promises";
8451
- import path15 from "path";
9272
+ import path16 from "path";
8452
9273
 
8453
9274
  // src/directUploadCapability.ts
8454
- function joinUrl(base, path17) {
8455
- return `${base.replace(/\/+$/, "")}${path17}`;
9275
+ function joinUrl(base, path18) {
9276
+ return `${base.replace(/\/+$/, "")}${path18}`;
8456
9277
  }
8457
9278
  function jsonHeaders(apiKey) {
8458
9279
  return {
@@ -8671,7 +9492,7 @@ var DaemonTraceBundleUploader = class {
8671
9492
  }, nextMs);
8672
9493
  }
8673
9494
  async findUploadCandidates() {
8674
- const traceDir = path15.join(this.options.machineDir, "traces");
9495
+ const traceDir = path16.join(this.options.machineDir, "traces");
8675
9496
  let names;
8676
9497
  try {
8677
9498
  names = await readdir3(traceDir);
@@ -8683,8 +9504,8 @@ var DaemonTraceBundleUploader = class {
8683
9504
  const currentFile = this.options.currentFileProvider?.();
8684
9505
  const candidates = [];
8685
9506
  for (const name of names.filter((entry) => entry.startsWith("daemon-trace-") && entry.endsWith(".jsonl")).sort()) {
8686
- const file = path15.join(traceDir, name);
8687
- if (currentFile && path15.resolve(file) === path15.resolve(currentFile)) continue;
9507
+ const file = path16.join(traceDir, name);
9508
+ if (currentFile && path16.resolve(file) === path16.resolve(currentFile)) continue;
8688
9509
  if (await this.isUploaded(file)) continue;
8689
9510
  try {
8690
9511
  const info = await stat3(file);
@@ -8758,8 +9579,8 @@ var DaemonTraceBundleUploader = class {
8758
9579
  }
8759
9580
  }
8760
9581
  uploadStatePath(file) {
8761
- const stateDir = path15.join(this.options.machineDir, "trace-uploads");
8762
- return path15.join(stateDir, `${path15.basename(file)}.uploaded.json`);
9582
+ const stateDir = path16.join(this.options.machineDir, "trace-uploads");
9583
+ return path16.join(stateDir, `${path16.basename(file)}.uploaded.json`);
8763
9584
  }
8764
9585
  async isUploaded(file) {
8765
9586
  try {
@@ -8771,9 +9592,9 @@ var DaemonTraceBundleUploader = class {
8771
9592
  }
8772
9593
  async markUploaded(file, metadata) {
8773
9594
  const stateFile = this.uploadStatePath(file);
8774
- await mkdir2(path15.dirname(stateFile), { recursive: true, mode: 448 });
9595
+ await mkdir2(path16.dirname(stateFile), { recursive: true, mode: 448 });
8775
9596
  await writeFile2(stateFile, `${JSON.stringify({
8776
- file: path15.basename(file),
9597
+ file: path16.basename(file),
8777
9598
  uploadedAt: (/* @__PURE__ */ new Date()).toISOString(),
8778
9599
  ...metadata
8779
9600
  }, null, 2)}
@@ -8795,7 +9616,7 @@ var DEFAULT_TRACE_UPLOAD_URL = "https://slock-trace-upload.botiverse.dev";
8795
9616
  var RUNNER_CREDENTIAL_SCOPES = ["send", "read", "mentions", "tasks", "reactions", "server", "channels"];
8796
9617
  var RUNNER_CREDENTIAL_MINT_MAX_ATTEMPTS2 = 3;
8797
9618
  var RUNNER_CREDENTIAL_MINT_RETRY_DELAY_MS2 = 250;
8798
- var DAEMON_CLI_USAGE = "Usage: slock-daemon --server-url <url> --api-key <key>";
9619
+ var DAEMON_CLI_USAGE = `Usage: slock-daemon --server-url <url> (--api-key <key> or ${DAEMON_API_KEY_ENV}=<key>)`;
8799
9620
  var RunnerCredentialMintError2 = class extends Error {
8800
9621
  code;
8801
9622
  retryable;
@@ -8831,9 +9652,9 @@ function runnerCredentialErrorDetail2(error) {
8831
9652
  async function waitForRunnerCredentialRetry2() {
8832
9653
  await new Promise((resolve) => setTimeout(resolve, RUNNER_CREDENTIAL_MINT_RETRY_DELAY_MS2));
8833
9654
  }
8834
- function parseDaemonCliArgs(args) {
9655
+ function parseDaemonCliArgs(args, env = {}) {
8835
9656
  let serverUrl = "";
8836
- let apiKey = "";
9657
+ let apiKey = env[DAEMON_API_KEY_ENV] ?? "";
8837
9658
  for (let i = 0; i < args.length; i++) {
8838
9659
  if (args[i] === "--server-url" && args[i + 1]) serverUrl = args[++i];
8839
9660
  if (args[i] === "--api-key" && args[i + 1]) apiKey = args[++i];
@@ -8850,23 +9671,23 @@ function readDaemonVersion(moduleUrl = import.meta.url) {
8850
9671
  }
8851
9672
  }
8852
9673
  function resolveChatBridgePath(moduleUrl = import.meta.url) {
8853
- const dirname = path16.dirname(fileURLToPath(moduleUrl));
8854
- const jsPath = path16.resolve(dirname, "chat-bridge.js");
9674
+ const dirname = path17.dirname(fileURLToPath2(moduleUrl));
9675
+ const jsPath = path17.resolve(dirname, "chat-bridge.js");
8855
9676
  try {
8856
9677
  accessSync(jsPath);
8857
9678
  return jsPath;
8858
9679
  } catch {
8859
- return path16.resolve(dirname, "chat-bridge.ts");
9680
+ return path17.resolve(dirname, "chat-bridge.ts");
8860
9681
  }
8861
9682
  }
8862
9683
  function resolveSlockCliPath(moduleUrl = import.meta.url) {
8863
- const thisDir = path16.dirname(fileURLToPath(moduleUrl));
8864
- const bundledDistPath = path16.resolve(thisDir, "cli", "index.js");
9684
+ const thisDir = path17.dirname(fileURLToPath2(moduleUrl));
9685
+ const bundledDistPath = path17.resolve(thisDir, "cli", "index.js");
8865
9686
  try {
8866
9687
  accessSync(bundledDistPath);
8867
9688
  return bundledDistPath;
8868
9689
  } catch {
8869
- const workspaceDistPath = path16.resolve(thisDir, "..", "..", "cli", "dist", "index.js");
9690
+ const workspaceDistPath = path17.resolve(thisDir, "..", "..", "cli", "dist", "index.js");
8870
9691
  accessSync(workspaceDistPath);
8871
9692
  return workspaceDistPath;
8872
9693
  }
@@ -9045,7 +9866,7 @@ var DaemonCore = class {
9045
9866
  }
9046
9867
  resolveMachineStateRoot() {
9047
9868
  if (this.options.machineStateDir) return this.options.machineStateDir;
9048
- if (this.options.dataDir) return path16.join(path16.dirname(this.options.dataDir), "machines");
9869
+ if (this.options.dataDir) return path17.join(path17.dirname(this.options.dataDir), "machines");
9049
9870
  return resolveDefaultMachineStateRoot();
9050
9871
  }
9051
9872
  shouldEnableLocalTrace() {
@@ -9547,6 +10368,8 @@ var DaemonCore = class {
9547
10368
  };
9548
10369
 
9549
10370
  export {
10371
+ DAEMON_API_KEY_ENV,
10372
+ scrubDaemonAuthEnv,
9550
10373
  resolveWorkspaceDirectoryPath,
9551
10374
  scanWorkspaceDirectories,
9552
10375
  deleteWorkspaceDirectory,