@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.
- package/dist/{chunk-JDSI7JD5.js → chunk-WGO5H7XX.js} +354 -95
- package/dist/cli/index.js +118 -81
- package/dist/core.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
|
@@ -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,
|
|
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
|
|
1282
|
-
-
|
|
1283
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
|
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 =
|
|
1685
|
+
const target = sideEffectTarget(action, body);
|
|
1554
1686
|
const coordinator = registration.inboxCoordinator;
|
|
1555
|
-
if (!target || !coordinator
|
|
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
|
|
1561
|
-
|
|
1562
|
-
|
|
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 {
|
|
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: "
|
|
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
|
-
|
|
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:**
|
|
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.
|
|
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
|
|
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,
|
|
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,
|
|
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
|
-
|
|
5317
|
-
const collect = (messages) => (messages ?? []).filter((message) =>
|
|
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
|
-
|
|
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
|
|
7705
|
-
|
|
7706
|
-
|
|
7707
|
-
|
|
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
|
|
7741
|
-
|
|
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
|
-
|
|
8002
|
-
|
|
8003
|
-
|
|
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
|
|
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
|
-
|
|
15128
|
-
|
|
15129
|
-
|
|
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
|
-
|
|
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
package/dist/index.js
CHANGED