@holon-run/agentinbox 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ActivationDispatcher = exports.AgentInboxService = void 0;
4
+ const node_crypto_1 = require("node:crypto");
4
5
  const backend_1 = require("./backend");
5
6
  const util_1 = require("./util");
6
7
  const source_schema_1 = require("./source_schema");
@@ -1407,15 +1408,26 @@ class AgentInboxService {
1407
1408
  const state = this.store.getActivationDispatchState(buffer.agentId, buffer.targetId);
1408
1409
  if (!state) {
1409
1410
  const inbox = this.ensureInboxForAgent(buffer.agentId);
1411
+ const unackedItems = this.store.listInboxItems(inbox.inboxId, { includeAcked: false }).map((item) => ({
1412
+ itemId: item.itemId,
1413
+ sourceId: item.sourceId,
1414
+ sourceNativeId: item.sourceNativeId,
1415
+ eventVariant: item.eventVariant,
1416
+ inboxId: item.inboxId,
1417
+ occurredAt: item.occurredAt,
1418
+ metadata: item.metadata,
1419
+ rawPayload: item.rawPayload,
1420
+ deliveryHandle: item.deliveryHandle,
1421
+ }));
1410
1422
  const dispatched = await this.dispatchActivationTarget({
1411
1423
  agentId: buffer.agentId,
1412
1424
  targetId: buffer.targetId,
1413
1425
  newItemCount: entries.length,
1414
- totalUnackedCount: this.store.countInboxItems(inbox.inboxId, false),
1415
- summary: entries[0]?.summary ?? null,
1426
+ totalUnackedCount: unackedItems.length,
1427
+ summary: latestSummary(entries),
1416
1428
  subscriptionIds: uniqueSortedNullable(entries.map((entry) => entry.subscriptionId)),
1417
1429
  sourceIds: uniqueSorted(entries.map((entry) => entry.sourceId)),
1418
- items: entries.map((entry) => entry.item),
1430
+ items: unackedItems,
1419
1431
  });
1420
1432
  if (dispatched === "retryable_failure") {
1421
1433
  this.upsertDirtyDispatchState(buffer.agentId, buffer.targetId, entries);
@@ -1427,8 +1439,9 @@ class AgentInboxService {
1427
1439
  targetId: buffer.targetId,
1428
1440
  status: "dirty",
1429
1441
  leaseExpiresAt: state.leaseExpiresAt,
1442
+ lastNotifiedFingerprint: state.lastNotifiedFingerprint,
1430
1443
  pendingNewItemCount: state.pendingNewItemCount + entries.length,
1431
- pendingSummary: state.pendingSummary ?? entries[0]?.summary ?? null,
1444
+ pendingSummary: latestSummary(entries) ?? state.pendingSummary,
1432
1445
  pendingSubscriptionIds: uniqueSortedNullable([...state.pendingSubscriptionIds, ...entries.map((entry) => entry.subscriptionId)]),
1433
1446
  pendingSourceIds: uniqueSorted([...state.pendingSourceIds, ...entries.map((entry) => entry.sourceId)]),
1434
1447
  updatedAt: (0, util_1.nowIso)(),
@@ -1477,6 +1490,20 @@ class AgentInboxService {
1477
1490
  if (reason === "ack" && state.status !== "dirty") {
1478
1491
  return;
1479
1492
  }
1493
+ if (reason === "lease" && state.status === "notified") {
1494
+ const target = this.getActivationTarget(targetId);
1495
+ if (target.kind !== "terminal") {
1496
+ // Webhook targets keep the existing lease-based behavior.
1497
+ }
1498
+ else {
1499
+ this.store.upsertActivationDispatchState({
1500
+ ...state,
1501
+ leaseExpiresAt: new Date(Date.now() + target.notifyLeaseMs).toISOString(),
1502
+ updatedAt: (0, util_1.nowIso)(),
1503
+ });
1504
+ return;
1505
+ }
1506
+ }
1480
1507
  const dispatched = await this.dispatchActivationTarget({
1481
1508
  agentId,
1482
1509
  targetId,
@@ -1515,9 +1542,11 @@ class AgentInboxService {
1515
1542
  if (target.status === "offline") {
1516
1543
  return "offline";
1517
1544
  }
1545
+ const state = this.store.getActivationDispatchState(input.agentId, input.targetId);
1518
1546
  const inbox = this.ensureInboxForAgent(input.agentId);
1519
1547
  const summary = summarizeActivation(inbox.inboxId, input.newItemCount, input.summary);
1520
1548
  const totalUnackedCount = input.totalUnackedCount ?? this.store.countInboxItems(inbox.inboxId, false);
1549
+ let terminalPrompt = null;
1521
1550
  try {
1522
1551
  if (target.kind === "terminal") {
1523
1552
  const gate = await this.activationGate.evaluate(target);
@@ -1532,6 +1561,7 @@ class AgentInboxService {
1532
1561
  targetId: input.targetId,
1533
1562
  status: "dirty",
1534
1563
  leaseExpiresAt: new Date(Date.now() + DEFAULT_NOTIFY_RETRY_MS).toISOString(),
1564
+ lastNotifiedFingerprint: state?.lastNotifiedFingerprint ?? null,
1535
1565
  pendingNewItemCount: input.newItemCount,
1536
1566
  pendingSummary: input.summary,
1537
1567
  pendingSubscriptionIds: uniqueSortedNullable(input.subscriptionIds),
@@ -1540,11 +1570,32 @@ class AgentInboxService {
1540
1570
  });
1541
1571
  return "deferred";
1542
1572
  }
1573
+ const preview = totalUnackedCount === 1 && input.items.length === 1
1574
+ ? (0, terminal_1.deriveInlineItemPreview)(input.items[0], input.summary)
1575
+ : null;
1543
1576
  const prompt = (0, terminal_1.renderAgentPrompt)({
1544
1577
  inboxId: inbox.inboxId,
1545
1578
  totalUnackedCount,
1546
1579
  summary: input.summary,
1580
+ preview,
1547
1581
  });
1582
+ terminalPrompt = prompt;
1583
+ const promptFingerprint = terminalReminderFingerprint(prompt, input.items);
1584
+ if (state?.status === "dirty" && state.lastNotifiedFingerprint === promptFingerprint) {
1585
+ this.store.upsertActivationDispatchState({
1586
+ agentId: input.agentId,
1587
+ targetId: input.targetId,
1588
+ status: "notified",
1589
+ leaseExpiresAt: new Date(Date.now() + target.notifyLeaseMs).toISOString(),
1590
+ lastNotifiedFingerprint: promptFingerprint,
1591
+ pendingNewItemCount: 0,
1592
+ pendingSummary: null,
1593
+ pendingSubscriptionIds: [],
1594
+ pendingSourceIds: [],
1595
+ updatedAt: (0, util_1.nowIso)(),
1596
+ });
1597
+ return "suppressed";
1598
+ }
1548
1599
  await this.terminalDispatcher.dispatch(target, prompt);
1549
1600
  }
1550
1601
  else {
@@ -1573,6 +1624,9 @@ class AgentInboxService {
1573
1624
  targetId: input.targetId,
1574
1625
  status: "notified",
1575
1626
  leaseExpiresAt: new Date(Date.now() + target.notifyLeaseMs).toISOString(),
1627
+ lastNotifiedFingerprint: target.kind === "terminal"
1628
+ ? terminalReminderFingerprint(terminalPrompt ?? "", input.items)
1629
+ : state?.lastNotifiedFingerprint ?? null,
1576
1630
  pendingNewItemCount: 0,
1577
1631
  pendingSummary: null,
1578
1632
  pendingSubscriptionIds: [],
@@ -1602,8 +1656,9 @@ class AgentInboxService {
1602
1656
  targetId,
1603
1657
  status: "dirty",
1604
1658
  leaseExpiresAt: new Date(Date.now() + DEFAULT_NOTIFY_RETRY_MS).toISOString(),
1659
+ lastNotifiedFingerprint: this.store.getActivationDispatchState(agentId, targetId)?.lastNotifiedFingerprint ?? null,
1605
1660
  pendingNewItemCount: entries.length,
1606
- pendingSummary: entries[0]?.summary ?? null,
1661
+ pendingSummary: latestSummary(entries),
1607
1662
  pendingSubscriptionIds: uniqueSortedNullable(entries.map((entry) => entry.subscriptionId)),
1608
1663
  pendingSourceIds: uniqueSorted(entries.map((entry) => entry.sourceId)),
1609
1664
  updatedAt: (0, util_1.nowIso)(),
@@ -2288,6 +2343,24 @@ function summarizeActivation(inboxId, newItemCount, firstSummary) {
2288
2343
  }
2289
2344
  return `${newItemCount} new ${itemWord} in ${inboxId}`;
2290
2345
  }
2346
+ function latestSummary(entries) {
2347
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
2348
+ const summary = entries[index]?.summary;
2349
+ if (summary) {
2350
+ return summary;
2351
+ }
2352
+ }
2353
+ return null;
2354
+ }
2355
+ function terminalReminderFingerprint(prompt, items) {
2356
+ const payload = JSON.stringify({
2357
+ prompt,
2358
+ itemIds: items
2359
+ .map((item) => item.itemId)
2360
+ .sort((left, right) => left.localeCompare(right)),
2361
+ });
2362
+ return (0, node_crypto_1.createHash)("sha256").update(payload).digest("hex");
2363
+ }
2291
2364
  function summarizeSourceEvent(sourceType, sourceKey, eventVariant) {
2292
2365
  if (sourceType === "github_repo_ci") {
2293
2366
  const parts = eventVariant.split(".");
package/dist/src/store.js CHANGED
@@ -729,12 +729,13 @@ class AgentInboxStore {
729
729
  upsertActivationDispatchState(state) {
730
730
  this.db.run(`
731
731
  insert into activation_dispatch_states (
732
- agent_id, target_id, status, lease_expires_at, pending_new_item_count, pending_summary,
732
+ agent_id, target_id, status, lease_expires_at, last_notified_fingerprint, pending_new_item_count, pending_summary,
733
733
  pending_subscription_ids_json, pending_source_ids_json, updated_at
734
- ) values (?, ?, ?, ?, ?, ?, ?, ?, ?)
734
+ ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
735
735
  on conflict(agent_id, target_id) do update set
736
736
  status = excluded.status,
737
737
  lease_expires_at = excluded.lease_expires_at,
738
+ last_notified_fingerprint = excluded.last_notified_fingerprint,
738
739
  pending_new_item_count = excluded.pending_new_item_count,
739
740
  pending_summary = excluded.pending_summary,
740
741
  pending_subscription_ids_json = excluded.pending_subscription_ids_json,
@@ -745,6 +746,7 @@ class AgentInboxStore {
745
746
  state.targetId,
746
747
  state.status,
747
748
  state.leaseExpiresAt ?? null,
749
+ state.lastNotifiedFingerprint ?? null,
748
750
  state.pendingNewItemCount,
749
751
  state.pendingSummary ?? null,
750
752
  JSON.stringify(state.pendingSubscriptionIds),
@@ -1251,6 +1253,7 @@ class AgentInboxStore {
1251
1253
  targetId: String(row.target_id),
1252
1254
  status: row.status,
1253
1255
  leaseExpiresAt: row.lease_expires_at ? String(row.lease_expires_at) : null,
1256
+ lastNotifiedFingerprint: row.last_notified_fingerprint ? String(row.last_notified_fingerprint) : null,
1254
1257
  pendingNewItemCount: Number(row.pending_new_item_count),
1255
1258
  pendingSummary: row.pending_summary ? String(row.pending_summary) : null,
1256
1259
  pendingSubscriptionIds: parseJson(row.pending_subscription_ids_json),
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.TerminalDispatcher = void 0;
7
7
  exports.detectTerminalContext = detectTerminalContext;
8
8
  exports.renderAgentPrompt = renderAgentPrompt;
9
+ exports.deriveInlineItemPreview = deriveInlineItemPreview;
9
10
  exports.assignedAgentIdFromContext = assignedAgentIdFromContext;
10
11
  const node_child_process_1 = require("node:child_process");
11
12
  const node_util_1 = require("node:util");
@@ -49,11 +50,41 @@ function renderAgentPrompt(input) {
49
50
  const totalUnackedCount = input.totalUnackedCount ?? input.newItemCount ?? 0;
50
51
  const itemWord = totalUnackedCount === 1 ? "item" : "items";
51
52
  const base = `AgentInbox: ${totalUnackedCount} unacked ${itemWord} in inbox ${input.inboxId}.`;
53
+ if (totalUnackedCount === 1 && input.preview) {
54
+ const suffix = /(?:[.!?…]|\.{3})$/.test(input.preview) ? "" : ".";
55
+ return `${base} Preview: ${input.preview}${suffix} Read the inbox for full details if needed.`;
56
+ }
52
57
  if (input.summary) {
53
58
  return `${base} Summary: ${input.summary}. Please read the inbox, process them, and ack when finished.`;
54
59
  }
55
60
  return `${base} Please read the inbox, process them, and ack when finished.`;
56
61
  }
62
+ const MAX_INLINE_PREVIEW_INPUT_CHARS = 240;
63
+ const MAX_INLINE_PREVIEW_CHARS = 96;
64
+ const INLINE_PREVIEW_KEYS = ["message", "text", "body", "title"];
65
+ function deriveInlineItemPreview(item, summary) {
66
+ const candidates = [];
67
+ for (const key of INLINE_PREVIEW_KEYS) {
68
+ const rawPayloadValue = item.rawPayload[key];
69
+ if (typeof rawPayloadValue === "string") {
70
+ candidates.push(rawPayloadValue);
71
+ }
72
+ const metadataValue = item.metadata[key];
73
+ if (typeof metadataValue === "string") {
74
+ candidates.push(metadataValue);
75
+ }
76
+ }
77
+ if (isPreviewFriendlySummary(summary)) {
78
+ candidates.push(summary);
79
+ }
80
+ for (const candidate of candidates) {
81
+ const preview = normalizeInlinePreview(candidate);
82
+ if (preview) {
83
+ return preview;
84
+ }
85
+ }
86
+ return null;
87
+ }
57
88
  function assignedAgentIdFromContext(input) {
58
89
  const primary = normalizeOptionalString(input.runtimeSessionId)
59
90
  ?? normalizeOptionalString(input.tmuxPaneId)
@@ -176,6 +207,44 @@ function normalizeItermSessionId(raw) {
176
207
  const parts = value.split(":");
177
208
  return parts[parts.length - 1] || null;
178
209
  }
210
+ function normalizeInlinePreview(value) {
211
+ const normalized = value.replace(/\s+/g, " ").trim();
212
+ if (!normalized) {
213
+ return null;
214
+ }
215
+ if (normalized.length > MAX_INLINE_PREVIEW_INPUT_CHARS) {
216
+ return null;
217
+ }
218
+ if (looksStructured(normalized)) {
219
+ return null;
220
+ }
221
+ if (normalized.length <= MAX_INLINE_PREVIEW_CHARS) {
222
+ return normalized;
223
+ }
224
+ return `${normalized.slice(0, MAX_INLINE_PREVIEW_CHARS - 3).trimEnd()}...`;
225
+ }
226
+ function isPreviewFriendlySummary(summary) {
227
+ if (typeof summary !== "string") {
228
+ return false;
229
+ }
230
+ const normalized = summary.trim();
231
+ if (!normalized) {
232
+ return false;
233
+ }
234
+ if (!normalized.includes(" ")) {
235
+ return false;
236
+ }
237
+ if (/^[a-z0-9_.-]+:/i.test(normalized)) {
238
+ return false;
239
+ }
240
+ return true;
241
+ }
242
+ function looksStructured(value) {
243
+ if ((value.startsWith("{") && value.endsWith("}")) || (value.startsWith("[") && value.endsWith("]"))) {
244
+ return true;
245
+ }
246
+ return false;
247
+ }
179
248
  function detectRuntimeContext(env) {
180
249
  const codexThreadId = normalizeOptionalString(env.CODEX_THREAD_ID);
181
250
  if (codexThreadId) {
@@ -0,0 +1,2 @@
1
+ alter table activation_dispatch_states
2
+ add column last_notified_fingerprint text;
package/drizzle/schema.ts CHANGED
@@ -122,6 +122,7 @@ export const activationDispatchStates = sqliteTable("activation_dispatch_states"
122
122
  targetId: text("target_id").notNull(),
123
123
  status: text("status").notNull(),
124
124
  leaseExpiresAt: text("lease_expires_at"),
125
+ lastNotifiedFingerprint: text("last_notified_fingerprint"),
125
126
  pendingNewItemCount: integer("pending_new_item_count").notNull(),
126
127
  pendingSummary: text("pending_summary"),
127
128
  pendingSubscriptionIdsJson: text("pending_subscription_ids_json").notNull(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@holon-run/agentinbox",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Local event subscription and delivery service for agents.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -49,8 +49,8 @@
49
49
  "devDependencies": {
50
50
  "@types/node": "^24.5.2",
51
51
  "@types/sql.js": "^1.4.9",
52
- "drizzle-kit": "^0.31.6",
53
- "drizzle-orm": "^0.44.2",
52
+ "drizzle-kit": "1.0.0-beta.21",
53
+ "drizzle-orm": "^0.45.2",
54
54
  "mdorigin": "^0.5.0",
55
55
  "ts-node": "^10.9.2",
56
56
  "typescript": "^5.9.2"