@slock-ai/daemon 0.51.1 → 0.52.1

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.
@@ -1275,13 +1275,12 @@ You may develop a specialized role over time through your interactions. Embrace
1275
1275
 
1276
1276
  ## Message Notifications
1277
1277
 
1278
- While you are working, new messages may be delivered directly into your current thread.
1278
+ While you are working, the daemon may write a batched inbox-count notification into your current turn.
1279
1279
 
1280
1280
  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.`;
1281
+ - Treat the notification as a signal that new Slock messages are waiting; it does not include the message content.
1282
+ - Call ${checkCmd} at the next safe breakpoint to materialize the pending messages before taking side-effect actions that depend on current context.
1283
+ - If the new message is higher priority, pivot after reading it. If not, continue your current work.`;
1285
1284
  } else {
1286
1285
  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
1286
  prompt += `
@@ -1356,6 +1355,7 @@ import { URL as URL2 } from "url";
1356
1355
  var registrations = /* @__PURE__ */ new Map();
1357
1356
  var servers = /* @__PURE__ */ new Map();
1358
1357
  var DECODED_RESPONSE_HEADERS = /* @__PURE__ */ new Set(["content-encoding", "content-length", "transfer-encoding"]);
1358
+ var LOCAL_HELD_CONTEXT_LIMIT = 3;
1359
1359
  function allocatePort() {
1360
1360
  return 43e3 + randomBytes(2).readUInt16BE(0) % 1e4;
1361
1361
  }
@@ -1388,8 +1388,10 @@ async function handleProxyRequest(req, res) {
1388
1388
  res.end(JSON.stringify({ error: "invalid local agent proxy token", code: "invalid_agent_proxy_token" }));
1389
1389
  return;
1390
1390
  }
1391
+ const method = req.method ?? "GET";
1392
+ let target;
1391
1393
  try {
1392
- const target = new URL2(req.url ?? "/", registration.serverUrl);
1394
+ target = new URL2(req.url ?? "/", registration.serverUrl);
1393
1395
  const headers = new Headers();
1394
1396
  for (const [name, value] of Object.entries(req.headers)) {
1395
1397
  if (value === void 0) continue;
@@ -1405,12 +1407,20 @@ async function handleProxyRequest(req, res) {
1405
1407
  headers.set("X-Agent-Id", registration.agentId);
1406
1408
  headers.set("X-Slock-Client", "cli");
1407
1409
  headers.set("X-Slock-Agent-Active-Capabilities", registration.activeCapabilities);
1408
- const method = req.method ?? "GET";
1409
1410
  let body = method === "GET" || method === "HEAD" ? void 0 : req;
1410
1411
  let sendTarget;
1411
- if (method === "POST" && target.pathname === "/internal/agent-api/send") {
1412
+ const sideEffectAction = agentApiSideEffectAction(target.pathname);
1413
+ if (method === "GET" && target.pathname === "/internal/agent-api/events") {
1414
+ const localEvents = localAgentApiEventsResponse(registration, target);
1415
+ if (localEvents) {
1416
+ res.writeHead(localEvents.status, { "content-type": "application/json" });
1417
+ res.end(JSON.stringify(localEvents.body));
1418
+ return;
1419
+ }
1420
+ }
1421
+ if (method === "POST" && sideEffectAction) {
1412
1422
  const rawBody = await readRequestBody(req);
1413
- const prepared = await prepareAgentApiSendForward(registration, headers, rawBody);
1423
+ const prepared = await prepareAgentApiSideEffectForward(registration, headers, rawBody, sideEffectAction);
1414
1424
  if (prepared.localResponse) {
1415
1425
  const responseText = JSON.stringify(prepared.localResponse);
1416
1426
  res.writeHead(200, { "content-type": "application/json" });
@@ -1418,7 +1428,7 @@ async function handleProxyRequest(req, res) {
1418
1428
  return;
1419
1429
  }
1420
1430
  body = prepared.bodyText;
1421
- sendTarget = prepared.target;
1431
+ if (sideEffectAction === "send") sendTarget = prepared.target;
1422
1432
  headers.set("content-type", "application/json");
1423
1433
  headers.set("content-length", String(Buffer.byteLength(prepared.bodyText)));
1424
1434
  }
@@ -1452,14 +1462,33 @@ async function handleProxyRequest(req, res) {
1452
1462
  res.end();
1453
1463
  }
1454
1464
  } catch (err) {
1465
+ const failure = proxyFailureForError(method, target, err);
1466
+ logger.warn(
1467
+ `[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}`
1468
+ );
1469
+ registration.inboxCoordinator?.recordProxyFailure?.(failure);
1455
1470
  res.writeHead(502, { "content-type": "application/json" });
1456
1471
  res.end(JSON.stringify({
1457
1472
  error: "failed to proxy local agent request",
1458
1473
  code: "agent_proxy_failed",
1459
- detail: err instanceof Error ? err.message : String(err)
1474
+ detail: failure.errorMessage
1460
1475
  }));
1461
1476
  }
1462
1477
  }
1478
+ function proxyFailureForError(method, target, err) {
1479
+ const queryKeys = target ? [.../* @__PURE__ */ new Set([...target.searchParams.keys()])].sort() : [];
1480
+ return {
1481
+ method,
1482
+ pathname: target?.pathname ?? "unknown",
1483
+ queryKeys,
1484
+ errorName: err instanceof Error ? err.name : typeof err,
1485
+ errorMessage: truncateProxyErrorMessage(err instanceof Error ? err.message : String(err))
1486
+ };
1487
+ }
1488
+ function truncateProxyErrorMessage(message) {
1489
+ const normalized = message.replace(/\s+/g, " ").trim();
1490
+ return normalized.length > 500 ? `${normalized.slice(0, 497)}...` : normalized;
1491
+ }
1463
1492
  async function readRequestBody(req) {
1464
1493
  let body = "";
1465
1494
  req.setEncoding("utf8");
@@ -1471,14 +1500,6 @@ async function readRequestBody(req) {
1471
1500
  function messageSeq(message) {
1472
1501
  return Number(message.seq ?? 0);
1473
1502
  }
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
1503
  function maxMessageSeq(messages) {
1483
1504
  let maxSeq = 0;
1484
1505
  for (const message of messages) {
@@ -1487,6 +1508,117 @@ function maxMessageSeq(messages) {
1487
1508
  }
1488
1509
  return maxSeq > 0 ? maxSeq : void 0;
1489
1510
  }
1511
+ function sortBySeq(messages) {
1512
+ return [...messages].sort((a, b) => messageSeq(a) - messageSeq(b));
1513
+ }
1514
+ function latestVisibleMessages(messages, limit) {
1515
+ const sorted = sortBySeq(messages);
1516
+ return sorted.slice(Math.max(0, sorted.length - limit));
1517
+ }
1518
+ function parseAgentApiEventsQuery(target) {
1519
+ const limit = Math.min(Math.max(Number(target.searchParams.get("limit")) || 50, 1), 200);
1520
+ const sinceRaw = target.searchParams.get("since")?.trim() ?? "";
1521
+ if (!sinceRaw) return { limit, sinceSeq: null, sinceCursorKind: null };
1522
+ if (sinceRaw === "latest") return { limit, sinceSeq: null, sinceCursorKind: "latest" };
1523
+ const parsed = Number(sinceRaw);
1524
+ if (!Number.isFinite(parsed) || parsed < 0) {
1525
+ return {
1526
+ limit,
1527
+ sinceSeq: null,
1528
+ sinceCursorKind: null,
1529
+ error: {
1530
+ error: "since must be a non-negative integer (messageSeq) or 'latest'",
1531
+ code: "since_invalid"
1532
+ }
1533
+ };
1534
+ }
1535
+ return { limit, sinceSeq: Math.floor(parsed), sinceCursorKind: "seq" };
1536
+ }
1537
+ function localAgentApiEventsResponse(registration, target) {
1538
+ const coordinator = registration.inboxCoordinator;
1539
+ if (!coordinator) return void 0;
1540
+ const pending = coordinator.getAllPendingMessages?.() ?? [];
1541
+ const parsedQuery = parseAgentApiEventsQuery(target);
1542
+ if (parsedQuery.error && pending.length > 0) {
1543
+ return { status: 400, body: parsedQuery.error };
1544
+ }
1545
+ if (pending.length === 0) return void 0;
1546
+ const normalized = sortBySeq(normalizeVisibleMessages(pending));
1547
+ const filtered = parsedQuery.sinceSeq !== null ? normalized.filter((message) => {
1548
+ const seq = messageSeq(message);
1549
+ return Number.isFinite(seq) && seq > parsedQuery.sinceSeq;
1550
+ }) : normalized;
1551
+ const events = filtered.slice(0, parsedQuery.limit);
1552
+ const hasMore = filtered.length > events.length;
1553
+ const newestEvent = events[events.length - 1];
1554
+ const lastSeenMsgId = newestEvent?.message_id ?? newestEvent?.id ?? null;
1555
+ const lastSeenSeq = newestEvent?.seq ?? parsedQuery.sinceSeq;
1556
+ if (events.length > 0) {
1557
+ coordinator.consumeVisibleMessages({ messages: events, source: "agent_api_events" });
1558
+ }
1559
+ coordinator.recordDrainOutcome?.({
1560
+ source: "daemon_pending",
1561
+ sinceCursorKind: parsedQuery.sinceCursorKind,
1562
+ notifiedCount: pending.length,
1563
+ drainedCount: events.length,
1564
+ hasMore
1565
+ });
1566
+ return {
1567
+ status: 200,
1568
+ body: {
1569
+ events,
1570
+ last_seen_msgId: lastSeenMsgId,
1571
+ last_seen_seq: lastSeenSeq,
1572
+ reply_target: null,
1573
+ pending_notice_ids: [],
1574
+ wake_reason: null,
1575
+ has_more: hasMore
1576
+ }
1577
+ };
1578
+ }
1579
+ function heldAvailableActions(action) {
1580
+ return action === "send" ? ["check_messages", "send_draft", "send_anyway"] : ["check_messages", "retry_action"];
1581
+ }
1582
+ function localHeldResponse(input) {
1583
+ if (input.messages.length === 0) return void 0;
1584
+ const normalized = sortBySeq(normalizeVisibleMessages(input.messages, input.target));
1585
+ const heldMessages = latestVisibleMessages(normalized, LOCAL_HELD_CONTEXT_LIMIT);
1586
+ const omittedMessageCount = Math.max(0, normalized.length - heldMessages.length);
1587
+ const seenUpToSeq = maxMessageSeq(normalized);
1588
+ if (seenUpToSeq === void 0) return void 0;
1589
+ input.coordinator.consumeVisibleMessages({
1590
+ target: input.target,
1591
+ messages: heldMessages,
1592
+ boundarySeq: seenUpToSeq,
1593
+ source: input.source
1594
+ });
1595
+ const response = {
1596
+ state: "held",
1597
+ outcome: "held",
1598
+ subtype: "freshness",
1599
+ reason: "newer_messages_available",
1600
+ available_actions: heldAvailableActions(input.action),
1601
+ heldMessages,
1602
+ newMessageCount: normalized.length,
1603
+ shownMessageCount: heldMessages.length,
1604
+ omittedMessageCount
1605
+ };
1606
+ response.seenUpToSeq = seenUpToSeq;
1607
+ return response;
1608
+ }
1609
+ function recordFreshnessDecision(coordinator, decision) {
1610
+ coordinator?.recordFreshnessDecision?.(decision);
1611
+ }
1612
+ function agentApiSideEffectAction(pathname) {
1613
+ if (pathname === "/internal/agent-api/send") return "send";
1614
+ if (pathname === "/internal/agent-api/tasks/claim") return "task_claim";
1615
+ if (pathname === "/internal/agent-api/tasks/update-status") return "task_update";
1616
+ return void 0;
1617
+ }
1618
+ function sideEffectTarget(action, body) {
1619
+ const field = action === "send" ? body.target : body.channel;
1620
+ return typeof field === "string" && field.length > 0 ? field : void 0;
1621
+ }
1490
1622
  function parseTargetFields(target) {
1491
1623
  if (target.startsWith("dm:@")) {
1492
1624
  const rest = target.slice("dm:@".length);
@@ -1543,42 +1675,98 @@ async function loadRecentTargetMessages(registration, headers, target) {
1543
1675
  const parsed = await res.json().catch(() => null);
1544
1676
  return Array.isArray(parsed?.messages) ? normalizeVisibleMessages(parsed.messages, target) : [];
1545
1677
  }
1546
- async function prepareAgentApiSendForward(registration, headers, rawBody) {
1678
+ async function prepareAgentApiSideEffectForward(registration, headers, rawBody, action) {
1547
1679
  let body;
1548
1680
  try {
1549
1681
  body = rawBody ? JSON.parse(rawBody) : {};
1550
1682
  } catch {
1551
1683
  return { bodyText: rawBody };
1552
1684
  }
1553
- const target = typeof body.target === "string" ? body.target : void 0;
1685
+ const target = sideEffectTarget(action, body);
1554
1686
  const coordinator = registration.inboxCoordinator;
1555
- if (!target || !coordinator || body.continueAnyway === true) {
1687
+ if (!target || !coordinator) {
1688
+ return { bodyText: JSON.stringify(body), target };
1689
+ }
1690
+ if (action === "send" && body.continueAnyway === true) {
1691
+ recordFreshnessDecision(coordinator, {
1692
+ action,
1693
+ decision: "bypass",
1694
+ target,
1695
+ inboxTrustState: "trusted",
1696
+ reason: "continue_anyway"
1697
+ });
1556
1698
  return { bodyText: JSON.stringify(body), target };
1557
1699
  }
1558
1700
  const pending = coordinator.getPendingMessages(target);
1559
1701
  if (pending.length > 0) {
1560
- const staleBoundary = boundaryBefore(pending);
1561
- if (staleBoundary !== void 0) {
1562
- body.seenUpToSeq = staleBoundary;
1702
+ const localResponse = localHeldResponse({
1703
+ action,
1704
+ target,
1705
+ messages: pending,
1706
+ coordinator,
1707
+ source: "side_effect_preflight_context"
1708
+ });
1709
+ if (localResponse) {
1710
+ recordFreshnessDecision(coordinator, {
1711
+ action,
1712
+ decision: "local_hold",
1713
+ target,
1714
+ inboxTrustState: "trusted",
1715
+ reason: "exact_target_pending",
1716
+ pendingCount: pending.length,
1717
+ pendingMaxSeq: maxMessageSeq(pending),
1718
+ modelSeenSeq: coordinator.getBoundary(target),
1719
+ heldMessageCount: typeof localResponse.shownMessageCount === "number" ? localResponse.shownMessageCount : void 0,
1720
+ omittedMessageCount: typeof localResponse.omittedMessageCount === "number" ? localResponse.omittedMessageCount : void 0
1721
+ });
1563
1722
  }
1564
- return { bodyText: JSON.stringify(body), target };
1723
+ return {
1724
+ bodyText: JSON.stringify(body),
1725
+ target,
1726
+ localResponse
1727
+ };
1565
1728
  }
1566
1729
  const existingBoundary = typeof body.seenUpToSeq === "number" && Number.isFinite(body.seenUpToSeq) ? Math.max(0, Math.floor(body.seenUpToSeq)) : void 0;
1567
1730
  const loadedBoundary = coordinator.getBoundary(target);
1568
1731
  const boundary = Math.max(existingBoundary ?? 0, loadedBoundary ?? 0);
1569
1732
  if (boundary > 0) {
1570
- body.seenUpToSeq = boundary;
1733
+ if (action === "send") body.seenUpToSeq = boundary;
1734
+ recordFreshnessDecision(coordinator, {
1735
+ action,
1736
+ decision: "forward",
1737
+ target,
1738
+ inboxTrustState: "trusted",
1739
+ reason: "model_seen_boundary",
1740
+ pendingCount: 0,
1741
+ modelSeenSeq: boundary
1742
+ });
1571
1743
  return { bodyText: JSON.stringify(body), target };
1572
1744
  }
1573
1745
  const recent = await loadRecentTargetMessages(registration, headers, target);
1574
1746
  if (recent.length > 0) {
1575
1747
  const seenUpToSeq = maxMessageSeq(recent);
1576
- coordinator.consumeVisibleMessages({ target, messages: recent, boundarySeq: seenUpToSeq, source: "send_preflight_context" });
1748
+ coordinator.consumeVisibleMessages({ target, messages: recent, boundarySeq: seenUpToSeq, source: "side_effect_preflight_context" });
1749
+ recordFreshnessDecision(coordinator, {
1750
+ action,
1751
+ decision: "syncing_hold",
1752
+ target,
1753
+ inboxTrustState: "untrusted",
1754
+ reason: "target_first_touch_recent_context",
1755
+ pendingCount: 0,
1756
+ pendingMaxSeq: seenUpToSeq,
1757
+ modelSeenSeq: 0,
1758
+ heldMessageCount: recent.length,
1759
+ omittedMessageCount: 0
1760
+ });
1577
1761
  return {
1578
1762
  bodyText: JSON.stringify(body),
1579
1763
  target,
1580
1764
  localResponse: {
1581
1765
  state: "held",
1766
+ outcome: "held",
1767
+ subtype: "freshness",
1768
+ reason: "newer_messages_available",
1769
+ available_actions: heldAvailableActions(action),
1582
1770
  seenUpToSeq,
1583
1771
  heldMessages: recent,
1584
1772
  newMessageCount: recent.length,
@@ -1587,6 +1775,15 @@ async function prepareAgentApiSendForward(registration, headers, rawBody) {
1587
1775
  }
1588
1776
  };
1589
1777
  }
1778
+ recordFreshnessDecision(coordinator, {
1779
+ action,
1780
+ decision: "forward",
1781
+ target,
1782
+ inboxTrustState: "trusted",
1783
+ reason: "no_exact_target_pending_or_recent_context",
1784
+ pendingCount: 0,
1785
+ modelSeenSeq: 0
1786
+ });
1590
1787
  return { bodyText: JSON.stringify(body), target };
1591
1788
  }
1592
1789
  function shouldBufferJsonResponse(upstream, pathname, registration) {
@@ -1614,7 +1811,15 @@ function consumeVisibleResponse(registration, targetUrl, sendTarget, responseTex
1614
1811
  return;
1615
1812
  }
1616
1813
  if (targetUrl.pathname === "/internal/agent-api/events" && Array.isArray(parsed.events)) {
1617
- coordinator.consumeVisibleMessages({ messages: normalizeVisibleMessages(parsed.events), source: "agent_api_events" });
1814
+ const messages = normalizeVisibleMessages(parsed.events);
1815
+ coordinator.consumeVisibleMessages({ messages, source: "agent_api_events" });
1816
+ coordinator.recordDrainOutcome?.({
1817
+ source: "server_events",
1818
+ sinceCursorKind: parseAgentApiEventsQuery(targetUrl).sinceCursorKind,
1819
+ notifiedCount: 0,
1820
+ drainedCount: messages.length,
1821
+ hasMore: Boolean(parsed.has_more)
1822
+ });
1618
1823
  return;
1619
1824
  }
1620
1825
  if (targetUrl.pathname === "/internal/agent-api/history" && Array.isArray(parsed.messages)) {
@@ -2159,8 +2364,7 @@ var ClaudeDriver = class {
2159
2364
  "- 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
2365
  ],
2161
2366
  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."
2367
+ "**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
2368
  ],
2165
2369
  includeStdinNotificationSection: true,
2166
2370
  messageNotificationStyle: "direct"
@@ -2654,7 +2858,7 @@ var CodexDriver = class {
2654
2858
  toolPrefix: "",
2655
2859
  extraCriticalRules: [],
2656
2860
  postStartupNotes: [
2657
- "**IMPORTANT**: Your process stays alive across turns. New messages may be delivered directly into the current thread while you are working."
2861
+ "**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
2862
  ],
2659
2863
  includeStdinNotificationSection: true,
2660
2864
  messageNotificationStyle: "direct"
@@ -3820,11 +4024,14 @@ function detectOpenCodeModels(home = os5.homedir(), runCommand = runOpenCodeMode
3820
4024
  if (commandResult.error || commandResult.status !== 0) return null;
3821
4025
  return parseOpenCodeModelsOutput(commandResult.stdout);
3822
4026
  }
3823
- function runOpenCodeModelsCommand(home) {
3824
- const result = spawnSync2("opencode", ["models"], {
4027
+ function runOpenCodeModelsCommand(home, deps = {}) {
4028
+ const platform = deps.platform ?? process.platform;
4029
+ const spawnSyncFn = deps.spawnSyncFn ?? spawnSync2;
4030
+ const result = spawnSyncFn("opencode", ["models"], {
3825
4031
  env: { ...process.env, HOME: home, FORCE_COLOR: "0", NO_COLOR: "1" },
3826
4032
  encoding: "utf8",
3827
- timeout: 5e3
4033
+ timeout: 5e3,
4034
+ shell: platform === "win32"
3828
4035
  });
3829
4036
  return {
3830
4037
  status: result.status,
@@ -5234,10 +5441,10 @@ function getMessageDeliveryText(driver) {
5234
5441
  function getBusyDeliveryNote(driver) {
5235
5442
  if (!driver.supportsStdinNotification) return "";
5236
5443
  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.";
5444
+ 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
5445
  }
5239
5446
  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.";
5447
+ 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
5448
  }
5242
5449
  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
5450
  }
@@ -5313,13 +5520,16 @@ var AgentProcessManager = class _AgentProcessManager {
5313
5520
  getVisibleBoundary(agentId, target) {
5314
5521
  return this.agentVisibleBoundaries.get(agentId)?.get(target);
5315
5522
  }
5316
- pendingVisibleMessages(agentId, target) {
5317
- const collect = (messages) => (messages ?? []).filter((message) => formatMessageTarget(message) === target && typeof message.seq === "number" && message.seq > 0);
5523
+ allPendingVisibleMessages(agentId) {
5524
+ const collect = (messages) => (messages ?? []).filter((message) => typeof message.seq === "number" && message.seq > 0);
5318
5525
  return [
5319
5526
  ...collect(this.agents.get(agentId)?.inbox),
5320
5527
  ...collect(this.startingInboxes.get(agentId))
5321
5528
  ];
5322
5529
  }
5530
+ pendingVisibleMessages(agentId, target) {
5531
+ return this.allPendingVisibleMessages(agentId).filter((message) => formatMessageTarget(message) === target);
5532
+ }
5323
5533
  /**
5324
5534
  * Single-inbox consume point for messages that have been rendered into the
5325
5535
  * agent-visible input surface. Daemon pending inbox is only a runner cache:
@@ -5390,9 +5600,72 @@ var AgentProcessManager = class _AgentProcessManager {
5390
5600
  return {
5391
5601
  getBoundary: (target) => this.getVisibleBoundary(agentId, target),
5392
5602
  getPendingMessages: (target) => this.pendingVisibleMessages(agentId, target),
5393
- consumeVisibleMessages: (input) => this.consumeVisibleMessages(agentId, input)
5603
+ getAllPendingMessages: () => this.allPendingVisibleMessages(agentId),
5604
+ consumeVisibleMessages: (input) => this.consumeVisibleMessages(agentId, input),
5605
+ recordDrainOutcome: (input) => {
5606
+ this.recordDaemonTrace("daemon.agent.drain.outcome", {
5607
+ agentId,
5608
+ source: input.source,
5609
+ since_cursor_kind: input.sinceCursorKind ?? void 0,
5610
+ notified_count: input.notifiedCount,
5611
+ drained_count: input.drainedCount,
5612
+ has_more: input.hasMore
5613
+ });
5614
+ },
5615
+ recordProxyFailure: (input) => this.recordAgentProxyFailure(agentId, input),
5616
+ recordFreshnessDecision: (input) => {
5617
+ this.recordDaemonTrace("daemon.agent.inbox.freshness_decision", {
5618
+ agentId,
5619
+ action: input.action,
5620
+ decision: input.decision,
5621
+ target: input.target,
5622
+ inbox_trust_state: input.inboxTrustState,
5623
+ reason: input.reason,
5624
+ pending_count: input.pendingCount,
5625
+ pending_max_seq: input.pendingMaxSeq,
5626
+ model_seen_seq: input.modelSeenSeq,
5627
+ held_message_count: input.heldMessageCount,
5628
+ omitted_message_count: input.omittedMessageCount
5629
+ });
5630
+ this.recordFreshnessDecisionActivity(agentId, input);
5631
+ }
5394
5632
  };
5395
5633
  }
5634
+ recordAgentProxyFailure(agentId, input) {
5635
+ this.recordDaemonTrace("daemon.agent.proxy.failure", {
5636
+ agentId,
5637
+ method: input.method,
5638
+ path: input.pathname,
5639
+ query_keys: input.queryKeys,
5640
+ error_name: input.errorName,
5641
+ error_message: input.errorMessage
5642
+ }, "error");
5643
+ }
5644
+ recordFreshnessDecisionActivity(agentId, input) {
5645
+ if (input.decision !== "local_hold" && input.decision !== "syncing_hold") return;
5646
+ const ap = this.agents.get(agentId);
5647
+ const messageCount = input.pendingCount ?? input.heldMessageCount ?? 0;
5648
+ 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";
5649
+ const entry = {
5650
+ kind: "slock_action",
5651
+ title,
5652
+ text: [
5653
+ input.target ? `target: ${input.target}` : null,
5654
+ `new messages: ${messageCount} newer message${messageCount === 1 ? "" : "s"}`,
5655
+ `decision: ${input.decision === "syncing_hold" ? "syncing hold" : "local hold"}; review the newer context before retrying`
5656
+ ].filter((line) => Boolean(line)).join("\n")
5657
+ };
5658
+ if (ap) ap.activityClientSeq += 1;
5659
+ this.sendToServer({
5660
+ type: "agent:activity",
5661
+ agentId,
5662
+ activity: ap?.lastActivity || "online",
5663
+ detail: ap?.lastActivityDetail || "",
5664
+ entries: [entry],
5665
+ launchId: ap?.launchId || void 0,
5666
+ clientSeq: ap?.activityClientSeq
5667
+ });
5668
+ }
5396
5669
  recordDaemonTrace(name, attrs, status = "ok", parentTraceparent) {
5397
5670
  const span = this.tracer.startSpan(name, {
5398
5671
  parent: parseTraceparent(parentTraceparent),
@@ -6446,6 +6719,11 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
6446
6719
  }
6447
6720
  if (ap.driver.busyDeliveryMode === "gated") {
6448
6721
  ap.pendingNotificationCount++;
6722
+ if (!ap.notificationTimer) {
6723
+ ap.notificationTimer = setTimeout(() => {
6724
+ this.sendStdinNotification(agentId);
6725
+ }, 3e3);
6726
+ }
6449
6727
  this.recordGatedSteeringEvent(agentId, ap, "buffer", {
6450
6728
  reason: "busy_message",
6451
6729
  pendingMessages: ap.inbox.length
@@ -6458,7 +6736,8 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
6458
6736
  session_id_present: true,
6459
6737
  launchId: ap.launchId || void 0,
6460
6738
  inbox_count: ap.inbox.length,
6461
- pending_notification_count: ap.pendingNotificationCount
6739
+ pending_notification_count: ap.pendingNotificationCount,
6740
+ notification_timer_present: Boolean(ap.notificationTimer)
6462
6741
  }));
6463
6742
  return true;
6464
6743
  }
@@ -7283,11 +7562,17 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
7283
7562
  if (reason !== "turn_end") {
7284
7563
  if (ap.gatedSteering.toolBoundaryFlushDisabled) return false;
7285
7564
  if (ap.gatedSteering.compacting || ap.gatedSteering.outstandingToolUses > 0) return false;
7565
+ this.recordGatedSteeringEvent(agentId, ap, "notify", { reason, pendingMessages: ap.inbox.length });
7566
+ return this.sendStdinNotification(agentId);
7286
7567
  }
7287
7568
  const pendingMessages = ap.inbox.length;
7288
7569
  const pendingNotificationCount = ap.pendingNotificationCount;
7289
7570
  const nextMessages = ap.inbox.splice(0, ap.inbox.length);
7290
7571
  ap.pendingNotificationCount = 0;
7572
+ if (ap.notificationTimer) {
7573
+ clearTimeout(ap.notificationTimer);
7574
+ ap.notificationTimer = null;
7575
+ }
7291
7576
  ap.gatedSteering.lastFlushReason = reason;
7292
7577
  if (reason !== "turn_end") {
7293
7578
  ap.gatedSteering.inFlightBatch = {
@@ -7315,28 +7600,6 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
7315
7600
  }
7316
7601
  flushCompactionBoundaryMessages(agentId, ap) {
7317
7602
  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
7603
  if (ap.pendingNotificationCount > 0) {
7341
7604
  if (ap.notificationTimer) {
7342
7605
  clearTimeout(ap.notificationTimer);
@@ -7555,6 +7818,10 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
7555
7818
  if (ap.inbox.length > 0 && ap.driver.supportsStdinNotification && ap.sessionId) {
7556
7819
  const nextMessages = ap.inbox.splice(0, ap.inbox.length);
7557
7820
  ap.pendingNotificationCount = 0;
7821
+ if (ap.notificationTimer) {
7822
+ clearTimeout(ap.notificationTimer);
7823
+ ap.notificationTimer = null;
7824
+ }
7558
7825
  if (ap.driver.busyDeliveryMode === "gated") {
7559
7826
  ap.gatedSteering.lastFlushReason = "turn_end";
7560
7827
  this.recordGatedSteeringEvent(agentId, ap, "flush", {
@@ -7698,13 +7965,16 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
7698
7965
  /** Send a batched notification to the agent via stdin about pending messages */
7699
7966
  sendStdinNotification(agentId) {
7700
7967
  const ap = this.agents.get(agentId);
7701
- if (!ap) return;
7968
+ if (!ap) return false;
7702
7969
  const count = ap.pendingNotificationCount;
7703
7970
  ap.pendingNotificationCount = 0;
7704
- ap.notificationTimer = null;
7705
- if (count === 0) return;
7706
- if (ap.isIdle) return;
7707
- if (!ap.sessionId) return;
7971
+ if (ap.notificationTimer) {
7972
+ clearTimeout(ap.notificationTimer);
7973
+ ap.notificationTimer = null;
7974
+ }
7975
+ if (count === 0) return false;
7976
+ if (ap.isIdle) return false;
7977
+ if (!ap.sessionId) return false;
7708
7978
  if (ap.gatedSteering.compacting) {
7709
7979
  this.recordRuntimeTraceEvent(agentId, ap, "runtime.compaction_boundary.delivery_suppressed", {
7710
7980
  pendingNotificationCount: count,
@@ -7715,30 +7985,12 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
7715
7985
  logger.info(
7716
7986
  `[Agent ${agentId}] Suppressing stdin delivery until context compaction finishes; pending=${ap.inbox.length}`
7717
7987
  );
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;
7988
+ return false;
7739
7989
  }
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)`);
7990
+ const inboxCount = ap.inbox.length;
7991
+ if (inboxCount === 0) return false;
7992
+ 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.]`;
7993
+ logger.info(`[Agent ${agentId}] Sending stdin notification: ${inboxCount} pending inbox message(s)`);
7742
7994
  const encoded = ap.driver.encodeStdinMessage(notification, ap.sessionId, { mode: "busy" });
7743
7995
  if (encoded) {
7744
7996
  ap.process.stdin?.write(encoded + "\n");
@@ -7750,9 +8002,12 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
7750
8002
  outcome: "written",
7751
8003
  mode: "busy",
7752
8004
  pending_notification_count: count,
8005
+ inbox_count: inboxCount,
7753
8006
  session_id_present: true
7754
8007
  });
8008
+ return true;
7755
8009
  } else {
8010
+ ap.pendingNotificationCount += count;
7756
8011
  this.recordDaemonTrace("daemon.agent.stdin_notification", {
7757
8012
  agentId,
7758
8013
  runtime: ap.config.runtime,
@@ -7761,8 +8016,10 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
7761
8016
  outcome: "encode_failed",
7762
8017
  mode: "busy",
7763
8018
  pending_notification_count: count,
8019
+ inbox_count: inboxCount,
7764
8020
  session_id_present: true
7765
8021
  }, "error");
8022
+ return false;
7766
8023
  }
7767
8024
  }
7768
8025
  /** Deliver a message to an agent via stdin, formatting it the same way as the MCP bridge */
@@ -7998,10 +8255,12 @@ var DaemonConnection = class {
7998
8255
  messageKind = msg.type;
7999
8256
  this.markInbound(messageKind);
8000
8257
  this.resetWatchdog();
8001
- this.trace("daemon.connection.inbound_received", {
8002
- message_type: messageKind,
8003
- last_inbound_age_ms_bucket: "0"
8004
- });
8258
+ if (messageKind !== "ping") {
8259
+ this.trace("daemon.connection.inbound_received", {
8260
+ message_type: messageKind,
8261
+ last_inbound_age_ms_bucket: "0"
8262
+ });
8263
+ }
8005
8264
  this.options.onMessage(msg);
8006
8265
  } catch (err) {
8007
8266
  this.markInbound("invalid_json");
package/dist/cli/index.js CHANGED
@@ -14832,69 +14832,6 @@ function registerThreadUnfollowCommand(parent) {
14832
14832
  });
14833
14833
  }
14834
14834
 
14835
- // src/commands/message/_continueDraftState.ts
14836
- import fs2 from "fs";
14837
- import os from "os";
14838
- import path from "path";
14839
- var DEFAULT_LOCAL_DRAFT_TTL_MS = 10 * 60 * 1e3;
14840
- function stateFilePath(agentId) {
14841
- return path.join(process.env.SLOCK_CLI_DRAFT_STATE_DIR ?? os.tmpdir(), "slock-cli-attested-send", agentId, "continue-state.json");
14842
- }
14843
- function readState(agentId) {
14844
- const filePath = stateFilePath(agentId);
14845
- try {
14846
- const raw = fs2.readFileSync(filePath, "utf8");
14847
- const parsed = JSON.parse(raw);
14848
- return typeof parsed === "object" && parsed ? parsed : {};
14849
- } catch {
14850
- return {};
14851
- }
14852
- }
14853
- function writeState(agentId, state) {
14854
- const filePath = stateFilePath(agentId);
14855
- fs2.mkdirSync(path.dirname(filePath), { recursive: true });
14856
- fs2.writeFileSync(filePath, JSON.stringify(state), "utf8");
14857
- }
14858
- function getSavedDraft(agentId, target) {
14859
- const state = readState(agentId);
14860
- const draft = state.targets?.[target];
14861
- if (!draft || typeof draft === "string") return null;
14862
- if (typeof draft.content !== "string") return null;
14863
- const attachmentIds = Array.isArray(draft.attachmentIds) ? draft.attachmentIds.filter((item) => typeof item === "string") : [];
14864
- const savedAt = Number.isFinite(draft.savedAt) ? draft.savedAt : Date.now();
14865
- const reholdCount = Number.isFinite(draft.reholdCount) ? draft.reholdCount : 0;
14866
- const seenUpToSeq = Number.isFinite(draft.seenUpToSeq) ? draft.seenUpToSeq : void 0;
14867
- if (Date.now() - savedAt > DEFAULT_LOCAL_DRAFT_TTL_MS) {
14868
- clearSavedDraft(agentId, target);
14869
- return null;
14870
- }
14871
- return {
14872
- content: draft.content,
14873
- attachmentIds,
14874
- savedAt,
14875
- reholdCount,
14876
- seenUpToSeq
14877
- };
14878
- }
14879
- function setSavedDraft(agentId, target, draft) {
14880
- const state = readState(agentId);
14881
- const targets = state.targets ?? {};
14882
- targets[target] = {
14883
- content: draft.content,
14884
- attachmentIds: draft.attachmentIds,
14885
- savedAt: draft.savedAt,
14886
- reholdCount: draft.reholdCount,
14887
- ...draft.seenUpToSeq !== void 0 ? { seenUpToSeq: draft.seenUpToSeq } : {}
14888
- };
14889
- writeState(agentId, { targets });
14890
- }
14891
- function clearSavedDraft(agentId, target) {
14892
- const state = readState(agentId);
14893
- if (!state.targets || !(target in state.targets)) return;
14894
- delete state.targets[target];
14895
- writeState(agentId, state);
14896
- }
14897
-
14898
14835
  // src/commands/message/_format.ts
14899
14836
  function toLocalTime(iso) {
14900
14837
  const d = new Date(iso);
@@ -14938,7 +14875,13 @@ function formatMessages(messages) {
14938
14875
  if (messages.length === 0) return "No new messages.";
14939
14876
  return messages.map(formatMessageLine).join("\n");
14940
14877
  }
14941
- function formatHistoryMessageLine(m) {
14878
+ function buildReplyTarget(channel, messageId) {
14879
+ if (!messageId) return null;
14880
+ const isThreadTarget = /^#[^:]+:[0-9a-f]{8}$/i.test(channel) || /^dm:@[^:]+:[0-9a-f]{8}$/i.test(channel);
14881
+ if (isThreadTarget) return null;
14882
+ return `${channel}:${messageId.slice(0, 8)}`;
14883
+ }
14884
+ function formatHistoryMessageLine(channel, m) {
14942
14885
  const senderName = m.senderName ?? m.sender_name ?? "unknown";
14943
14886
  const senderDescription = m.senderDescription ?? m.sender_description ?? null;
14944
14887
  const messageId = m.id ?? m.message_id ?? "-";
@@ -14952,6 +14895,10 @@ function formatHistoryMessageLine(m) {
14952
14895
  if (senderType) headerParts.push(`type=${senderType}`);
14953
14896
  if (m.threadId) headerParts.push(`threadId=${m.threadId}`);
14954
14897
  if ((m.replyCount ?? 0) > 0) headerParts.push(`replyCount=${m.replyCount}`);
14898
+ if (m.threadId || (m.replyCount ?? 0) > 0) {
14899
+ const replyTarget = buildReplyTarget(channel, messageId);
14900
+ if (replyTarget) headerParts.push(`replyTarget=${replyTarget}`);
14901
+ }
14955
14902
  const attachSuffix = formatAttachmentSuffix(m.attachments);
14956
14903
  const taskSuffix = m.taskStatus ? ` [task #${m.taskNumber} status=${m.taskStatus}${m.taskAssigneeId ? ` assignee=${m.taskAssigneeType}:${m.taskAssigneeId}` : ""}]` : "";
14957
14904
  const handle = senderDescription ? `@${senderName} \u2014 ${senderDescription}` : `@${senderName}`;
@@ -14959,7 +14906,7 @@ function formatHistoryMessageLine(m) {
14959
14906
  }
14960
14907
  function formatHistory(channel, data, opts) {
14961
14908
  if (!data.messages || data.messages.length === 0) return "No messages in this channel.";
14962
- const formatted = data.messages.map((m) => formatHistoryMessageLine({
14909
+ const formatted = data.messages.map((m) => formatHistoryMessageLine(channel, {
14963
14910
  ...m,
14964
14911
  senderName: m.senderName ?? m.sender_name ?? "unknown",
14965
14912
  senderDescription: m.senderDescription ?? m.sender_description ?? null
@@ -15030,6 +14977,91 @@ thread: ${result.parentChannelName} -> ${target}` : "";
15030
14977
  ${formatted}`;
15031
14978
  }
15032
14979
 
14980
+ // src/commands/freshnessHold.ts
14981
+ function isFreshnessHeldResponse(data) {
14982
+ return Boolean(data && typeof data === "object" && data.state === "held");
14983
+ }
14984
+ function formatFreshnessHoldOutput(target, data, opts) {
14985
+ const newMessageCount = data.newMessageCount ?? 0;
14986
+ const shownMessageCount = data.shownMessageCount ?? data.heldMessages?.length ?? 0;
14987
+ const omittedMessageCount = data.omittedMessageCount ?? Math.max(0, newMessageCount - shownMessageCount);
14988
+ const heldHistory = data.heldMessages && data.heldMessages.length > 0 ? `
14989
+
14990
+ ${formatHistory(target, { messages: data.heldMessages })}` : "";
14991
+ const mentionNote = (data.mentionAnnotation?.formalMentionCount ?? 0) > 0 ? `
14992
+
14993
+ Note: ${data.mentionAnnotation.formalMentionCount} of these messages formally @mention you.` : "";
14994
+ const omittedNote = omittedMessageCount > 0 ? ` ${omittedMessageCount} earlier same-target message${omittedMessageCount === 1 ? "" : "s"} omitted from this notice and no longer block this action; use slock message read explicitly if you need earlier context.` : "";
14995
+ const continueAnyway = data.continueAnywaySuggested && opts.continueAnywayInstruction ? opts.continueAnywayInstruction : "";
14996
+ return `Freshness hold: showing latest ${shownMessageCount} of ${newMessageCount} newer message${newMessageCount === 1 ? "" : "s"}.${omittedNote}
14997
+ ${opts.heldAction} Review the bounded context shown here, then choose one path.${mentionNote}${heldHistory}
14998
+
14999
+ ` + (opts.draftInstructions ?? "") + continueAnyway;
15000
+ }
15001
+
15002
+ // src/commands/message/_continueDraftState.ts
15003
+ import fs2 from "fs";
15004
+ import os from "os";
15005
+ import path from "path";
15006
+ var DEFAULT_LOCAL_DRAFT_TTL_MS = 10 * 60 * 1e3;
15007
+ function stateFilePath(agentId) {
15008
+ return path.join(process.env.SLOCK_CLI_DRAFT_STATE_DIR ?? os.tmpdir(), "slock-cli-attested-send", agentId, "continue-state.json");
15009
+ }
15010
+ function readState(agentId) {
15011
+ const filePath = stateFilePath(agentId);
15012
+ try {
15013
+ const raw = fs2.readFileSync(filePath, "utf8");
15014
+ const parsed = JSON.parse(raw);
15015
+ return typeof parsed === "object" && parsed ? parsed : {};
15016
+ } catch {
15017
+ return {};
15018
+ }
15019
+ }
15020
+ function writeState(agentId, state) {
15021
+ const filePath = stateFilePath(agentId);
15022
+ fs2.mkdirSync(path.dirname(filePath), { recursive: true });
15023
+ fs2.writeFileSync(filePath, JSON.stringify(state), "utf8");
15024
+ }
15025
+ function getSavedDraft(agentId, target) {
15026
+ const state = readState(agentId);
15027
+ const draft = state.targets?.[target];
15028
+ if (!draft || typeof draft === "string") return null;
15029
+ if (typeof draft.content !== "string") return null;
15030
+ const attachmentIds = Array.isArray(draft.attachmentIds) ? draft.attachmentIds.filter((item) => typeof item === "string") : [];
15031
+ const savedAt = Number.isFinite(draft.savedAt) ? draft.savedAt : Date.now();
15032
+ const reholdCount = Number.isFinite(draft.reholdCount) ? draft.reholdCount : 0;
15033
+ const seenUpToSeq = Number.isFinite(draft.seenUpToSeq) ? draft.seenUpToSeq : void 0;
15034
+ if (Date.now() - savedAt > DEFAULT_LOCAL_DRAFT_TTL_MS) {
15035
+ clearSavedDraft(agentId, target);
15036
+ return null;
15037
+ }
15038
+ return {
15039
+ content: draft.content,
15040
+ attachmentIds,
15041
+ savedAt,
15042
+ reholdCount,
15043
+ seenUpToSeq
15044
+ };
15045
+ }
15046
+ function setSavedDraft(agentId, target, draft) {
15047
+ const state = readState(agentId);
15048
+ const targets = state.targets ?? {};
15049
+ targets[target] = {
15050
+ content: draft.content,
15051
+ attachmentIds: draft.attachmentIds,
15052
+ savedAt: draft.savedAt,
15053
+ reholdCount: draft.reholdCount,
15054
+ ...draft.seenUpToSeq !== void 0 ? { seenUpToSeq: draft.seenUpToSeq } : {}
15055
+ };
15056
+ writeState(agentId, { targets });
15057
+ }
15058
+ function clearSavedDraft(agentId, target) {
15059
+ const state = readState(agentId);
15060
+ if (!state.targets || !(target in state.targets)) return;
15061
+ delete state.targets[target];
15062
+ writeState(agentId, state);
15063
+ }
15064
+
15033
15065
  // src/commands/message/send.ts
15034
15066
  var SendContentError = class extends Error {
15035
15067
  constructor(code, message) {
@@ -15124,28 +15156,19 @@ function rejectSendDraftStdin(content, target) {
15124
15156
  );
15125
15157
  }
15126
15158
  function formatHeldSendOutput(target, data) {
15127
- const newMessageCount = data.newMessageCount ?? 0;
15128
- const shownMessageCount = data.shownMessageCount ?? data.heldMessages?.length ?? 0;
15129
- const omittedMessageCount = data.omittedMessageCount ?? Math.max(0, newMessageCount - shownMessageCount);
15130
- const heldHistory = data.heldMessages && data.heldMessages.length > 0 ? `
15131
-
15132
- ${formatHistory(target, { messages: data.heldMessages })}` : "";
15133
- const mentionNote = (data.mentionAnnotation?.formalMentionCount ?? 0) > 0 ? `
15134
-
15135
- Note: ${data.mentionAnnotation.formalMentionCount} of these messages formally @mention you.` : "";
15136
- const omittedNote = omittedMessageCount > 0 ? ` ${omittedMessageCount} additional newer message${omittedMessageCount === 1 ? "" : "s"} omitted from this notice; use slock message read explicitly if you need more context.` : "";
15137
- return `Freshness hold: showing latest ${shownMessageCount} of ${newMessageCount} newer message${newMessageCount === 1 ? "" : "s"}.${omittedNote}
15138
- Your message has been saved as a draft. Review the bounded context shown here, then choose one path.${mentionNote}${heldHistory}
15139
-
15140
- To update the draft, send revised content normally:
15159
+ return formatFreshnessHoldOutput(target, data, {
15160
+ heldAction: "Your message has been saved as a draft.",
15161
+ draftInstructions: `To update the draft, send revised content normally:
15141
15162
  slock message send --target "${target}" <<'EOF'
15142
15163
  revised message
15143
15164
  EOF
15144
15165
  To send the current draft unchanged:
15145
15166
  slock message send --send-draft --target "${target}"
15146
- ` + (data.continueAnywaySuggested ? `If repeated updates keep blocking the same draft and this is still the right reply, you may use:
15167
+ `,
15168
+ continueAnywayInstruction: `If repeated updates keep blocking the same draft and this is still the right reply, you may use:
15147
15169
  slock message send --send-draft --anyway --target "${target}"
15148
- ` : "");
15170
+ `
15171
+ });
15149
15172
  }
15150
15173
  function registerSendCommand(parent) {
15151
15174
  parent.command("send").description("Send a message to a channel, DM, or thread").argument("[content...]", "Unsupported positional message content. Pipe content to stdin instead.").requiredOption("--target <target>", "Target: '#channel', 'dm:@peer', '#channel:threadId', 'dm:@peer:threadId'").option("--send-draft", "Send the current saved draft after reviewing newer messages").option("--anyway", "Escape hatch: send a saved draft even if freshness re-check is still stale").option("--content <content>", "Unsupported. Pipe message content to stdin instead.").option(
@@ -15799,6 +15822,13 @@ function registerTaskClaimCommand(parent) {
15799
15822
  const code = res.status >= 500 ? "SERVER_5XX" : "CLAIM_FAILED";
15800
15823
  fail(code, res.error ?? `HTTP ${res.status}`);
15801
15824
  }
15825
+ if (isFreshnessHeldResponse(res.data)) {
15826
+ process.stdout.write(formatFreshnessHoldOutput(opts.channel, res.data, {
15827
+ heldAction: "Your task claim was not applied.",
15828
+ draftInstructions: "After reviewing the newer context, rerun the task claim command if it is still correct.\n"
15829
+ }));
15830
+ return;
15831
+ }
15802
15832
  process.stdout.write(formatClaimResults(opts.channel, res.data) + "\n");
15803
15833
  });
15804
15834
  }
@@ -15865,6 +15895,13 @@ function registerTaskUpdateCommand(parent) {
15865
15895
  const code = res.status >= 500 ? "SERVER_5XX" : "UPDATE_FAILED";
15866
15896
  fail(code, res.error ?? `HTTP ${res.status}`);
15867
15897
  }
15898
+ if (isFreshnessHeldResponse(res.data)) {
15899
+ process.stdout.write(formatFreshnessHoldOutput(opts.channel, res.data, {
15900
+ heldAction: "Your task status update was not applied.",
15901
+ draftInstructions: "After reviewing the newer context, rerun the task update command if it is still correct.\n"
15902
+ }));
15903
+ return;
15904
+ }
15868
15905
  process.stdout.write(formatTaskStatusUpdated(n, opts.status) + "\n");
15869
15906
  });
15870
15907
  }
package/dist/core.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  resolveSlockCliPath,
10
10
  resolveWorkspaceDirectoryPath,
11
11
  scanWorkspaceDirectories
12
- } from "./chunk-JDSI7JD5.js";
12
+ } from "./chunk-WGO5H7XX.js";
13
13
  import {
14
14
  subscribeDaemonLogs
15
15
  } from "./chunk-KNMCE6WB.js";
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  DAEMON_CLI_USAGE,
4
4
  DaemonCore,
5
5
  parseDaemonCliArgs
6
- } from "./chunk-JDSI7JD5.js";
6
+ } from "./chunk-WGO5H7XX.js";
7
7
  import "./chunk-KNMCE6WB.js";
8
8
 
9
9
  // src/index.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slock-ai/daemon",
3
- "version": "0.51.1",
3
+ "version": "0.52.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "slock-daemon": "dist/index.js"