larkin 0.4.7 → 0.4.8

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.
@@ -1165,6 +1165,9 @@ function createHostShell({ env = process.env, runtimeHost, channelPackage, event
1165
1165
  }, 80);
1166
1166
  hostState.updateStatus(agent, { droughtReconnectAt, droughtReconnectAbandonedAt: null });
1167
1167
  reconnectFns.get(agent.agentId)?.();
1168
+ void runtimeHost.scanPendingInboxUpdates?.(agent.agentId)?.catch((error) => {
1169
+ log(`drought reconnect inbox scan failed agent=${agent.name}: ${errorMessage(error)}`);
1170
+ });
1168
1171
  }).catch((error) => {
1169
1172
  maintenance.inFlight = false;
1170
1173
  if (shuttingDown || fataling)
@@ -1125,6 +1125,9 @@ export function createHostShell({ env = process.env, runtimeHost, channelPackage
1125
1125
  }, 80);
1126
1126
  hostState.updateStatus(agent, { droughtReconnectAt, droughtReconnectAbandonedAt: null });
1127
1127
  reconnectFns.get(agent.agentId)?.();
1128
+ void runtimeHost.scanPendingInboxUpdates?.(agent.agentId)?.catch((error) => {
1129
+ log(`drought reconnect inbox scan failed agent=${agent.name}: ${errorMessage(error)}`);
1130
+ });
1128
1131
  }).catch((error) => {
1129
1132
  maintenance.inFlight = false;
1130
1133
  if (shuttingDown || fataling)
@@ -120,8 +120,20 @@ function boundedRecords(agent) {
120
120
  const bounded = [...active, ...terminal].slice(-MAX_DELIVERIES);
121
121
  agent.records = new Map(bounded.map((record) => [record.deliveryId, record]));
122
122
  agent.byMessage = new Map(bounded.map((record) => [record.messageId, record.deliveryId]));
123
+ prunePromotedInboxUpdateIds(agent);
123
124
  return bounded;
124
125
  }
126
+ function prunePromotedInboxUpdateIds(agent) {
127
+ if (!agent.promotedInboxUpdateIds?.size)
128
+ return;
129
+ for (const id of agent.promotedInboxUpdateIds) {
130
+ if (!agent.records.has(id))
131
+ agent.promotedInboxUpdateIds.delete(id);
132
+ }
133
+ if (agent.promotedInboxUpdateIds.size <= MAX_DELIVERIES)
134
+ return;
135
+ agent.promotedInboxUpdateIds = new Set([...agent.promotedInboxUpdateIds].slice(-MAX_DELIVERIES));
136
+ }
125
137
  function createRuntimeHost(options) {
126
138
  const managed = new Map();
127
139
  const listeners = new Set();
@@ -294,6 +306,20 @@ function createRuntimeHost(options) {
294
306
  }
295
307
  };
296
308
  const TURN_END_RETRY_REASON = "runtime turn ended before Inbox consumption was observed";
309
+ const INBOX_UPDATE_PROMOTE_REASON = "accepted inbox_update promoted after Agent became idle";
310
+ const isInboxUpdateKind = (kind) => kind === "inbox_update" || kind === "inbox-update";
311
+ const agentIsIdleForInboxScan = (agent) => !agent.stopped && !agent.busy && !agent.submitting && !agent.turnInProgress && !agent.starting && Boolean(agent.session);
312
+ const acceptedInboxStillPending = (agent, record) => {
313
+ if (!agent.stateStore?.readNdjson)
314
+ return true;
315
+ try {
316
+ return agent.stateStore.readNdjson("inbox")
317
+ .some((row) => row?.message_id === record.messageId);
318
+ }
319
+ catch {
320
+ return false;
321
+ }
322
+ };
297
323
  /**
298
324
  * Runtime acceptance acknowledges only the notification. At the safe turn
299
325
  * boundary, canonical Inbox presence decides whether that same delivery
@@ -494,6 +520,7 @@ function createRuntimeHost(options) {
494
520
  agent.retryAfterSubmit = false;
495
521
  queueMicrotask(() => { void retryPending(agent); });
496
522
  }
523
+ queueMicrotask(() => { void scanAndPromoteAcceptedInboxUpdates(agent); });
497
524
  }
498
525
  };
499
526
  const retryPending = async (agent) => {
@@ -533,6 +560,45 @@ function createRuntimeHost(options) {
533
560
  agent.retryPendingInFlight = null;
534
561
  }
535
562
  };
563
+ /**
564
+ * After busy/submitting clears, accepted inbox_update notices have no later
565
+ * turn boundary. Promote each delivery id at most once to a wake.
566
+ */
567
+ const scanAndPromoteAcceptedInboxUpdates = async (agent) => {
568
+ if (!agentIsIdleForInboxScan(agent))
569
+ return;
570
+ if (agent.compactionMachines.size > 0 || agent.compactionRecoveryInFlight.size > 0)
571
+ return;
572
+ const candidates = [...agent.records.values()].filter((record) => record.status === "accepted"
573
+ && isInboxUpdateKind(record.input?.kind) && !agent.promotedInboxUpdateIds.has(record.deliveryId));
574
+ if (!candidates.length)
575
+ return;
576
+ reconcileExternalConsumption(agent);
577
+ let promoted = 0;
578
+ for (const record of candidates) {
579
+ const current = agent.records.get(record.deliveryId);
580
+ if (current?.status !== "accepted")
581
+ continue;
582
+ if (!acceptedInboxStillPending(agent, current)) {
583
+ reconcileAbsentCanonical(agent, current);
584
+ continue;
585
+ }
586
+ agent.promotedInboxUpdateIds.add(current.deliveryId);
587
+ current.reason = INBOX_UPDATE_PROMOTE_REASON;
588
+ current.retryable = true;
589
+ const finalRecord = setRecord(agent, current, "pending");
590
+ if (finalRecord.status === "consumed")
591
+ continue;
592
+ emit({ type: "delivery", agentId: agent.config.agentId, deliveryId: finalRecord.deliveryId,
593
+ messageId: finalRecord.messageId, status: "deferred", reason: INBOX_UPDATE_PROMOTE_REASON });
594
+ promoted += 1;
595
+ }
596
+ if (!promoted)
597
+ return;
598
+ if (agent.retryPendingInFlight)
599
+ agent.retryAfterSubmit = true;
600
+ await retryPending(agent);
601
+ };
536
602
  const scheduleRecreate = (agent, reason) => {
537
603
  if (agent.stopped || agent.retryTimer || agent.starting || agent.session)
538
604
  return;
@@ -870,6 +936,7 @@ function createRuntimeHost(options) {
870
936
  if (outcome !== "failed")
871
937
  return retryPending(agent);
872
938
  });
939
+ queueMicrotask(() => { void scanAndPromoteAcceptedInboxUpdates(agent); });
873
940
  }
874
941
  else if (event.type === "activity") {
875
942
  if (agent.turnInProgress && event.activity !== "internal")
@@ -1230,6 +1297,14 @@ function createRuntimeHost(options) {
1230
1297
  };
1231
1298
  },
1232
1299
  isBusy(agentId) { const agent = managed.get(agentId); return Boolean(agent?.busy || agent?.submitting || agent?.starting); },
1300
+ async scanPendingInboxUpdates(agentId) {
1301
+ const agents = agentId ? [managed.get(agentId)] : [...managed.values()];
1302
+ for (const agent of agents) {
1303
+ if (!agent || agent.stopped)
1304
+ continue;
1305
+ await scanAndPromoteAcceptedInboxUpdates(agent);
1306
+ }
1307
+ },
1233
1308
  async resetSession(agentId) {
1234
1309
  const agent = managed.get(agentId);
1235
1310
  if (!agent || agent.stopped)
@@ -1546,7 +1621,8 @@ function createRuntimeHost(options) {
1546
1621
  compactionMachines: new Map(), compactionRecoveryInFlight: new Set(), piOverflowCompactionFailed: new Set(),
1547
1622
  piProactiveCompaction: null, piProactiveCompactionGeneration: null, piProactiveCompactionSession: null,
1548
1623
  piProactiveCompactionFailedGeneration: null, readiness: null,
1549
- turnInProgress: false, turnHadFailure: false, turnHadAuthenticatedOutput: false, authFailureActive: false };
1624
+ turnInProgress: false, turnHadFailure: false, turnHadAuthenticatedOutput: false, authFailureActive: false,
1625
+ promotedInboxUpdateIds: new Set() };
1550
1626
  const startupConsumed = [];
1551
1627
  const startupQuarantined = [];
1552
1628
  for (const record of agent.records.values()) {
@@ -1648,6 +1724,7 @@ function createRuntimeHost(options) {
1648
1724
  : { inputId: existing.deliveryId, deliveryId: existing.deliveryId, kind: "wake", text: "", attempt: priorAttempt + 1 };
1649
1725
  delete existing.reason;
1650
1726
  delete existing.retryable;
1727
+ agent.promotedInboxUpdateIds.delete(existing.deliveryId);
1651
1728
  setRecord(agent, existing, "pending");
1652
1729
  const receipt = await (telemetry?.phase(messageId, "runtime.deliver", api_1.SpanKind.PRODUCER, () => submit(agent, existing, agent.busy || agent.submitting)) ?? submit(agent, existing, agent.busy || agent.submitting));
1653
1730
  telemetry?.delivery(agentId, messageId, receipt.status);
@@ -111,8 +111,20 @@ function boundedRecords(agent) {
111
111
  const bounded = [...active, ...terminal].slice(-MAX_DELIVERIES);
112
112
  agent.records = new Map(bounded.map((record) => [record.deliveryId, record]));
113
113
  agent.byMessage = new Map(bounded.map((record) => [record.messageId, record.deliveryId]));
114
+ prunePromotedInboxUpdateIds(agent);
114
115
  return bounded;
115
116
  }
117
+ function prunePromotedInboxUpdateIds(agent) {
118
+ if (!agent.promotedInboxUpdateIds?.size)
119
+ return;
120
+ for (const id of agent.promotedInboxUpdateIds) {
121
+ if (!agent.records.has(id))
122
+ agent.promotedInboxUpdateIds.delete(id);
123
+ }
124
+ if (agent.promotedInboxUpdateIds.size <= MAX_DELIVERIES)
125
+ return;
126
+ agent.promotedInboxUpdateIds = new Set([...agent.promotedInboxUpdateIds].slice(-MAX_DELIVERIES));
127
+ }
116
128
  export function createRuntimeHost(options) {
117
129
  const managed = new Map();
118
130
  const listeners = new Set();
@@ -285,6 +297,20 @@ export function createRuntimeHost(options) {
285
297
  }
286
298
  };
287
299
  const TURN_END_RETRY_REASON = "runtime turn ended before Inbox consumption was observed";
300
+ const INBOX_UPDATE_PROMOTE_REASON = "accepted inbox_update promoted after Agent became idle";
301
+ const isInboxUpdateKind = (kind) => kind === "inbox_update" || kind === "inbox-update";
302
+ const agentIsIdleForInboxScan = (agent) => !agent.stopped && !agent.busy && !agent.submitting && !agent.turnInProgress && !agent.starting && Boolean(agent.session);
303
+ const acceptedInboxStillPending = (agent, record) => {
304
+ if (!agent.stateStore?.readNdjson)
305
+ return true;
306
+ try {
307
+ return agent.stateStore.readNdjson("inbox")
308
+ .some((row) => row?.message_id === record.messageId);
309
+ }
310
+ catch {
311
+ return false;
312
+ }
313
+ };
288
314
  /**
289
315
  * Runtime acceptance acknowledges only the notification. At the safe turn
290
316
  * boundary, canonical Inbox presence decides whether that same delivery
@@ -485,6 +511,7 @@ export function createRuntimeHost(options) {
485
511
  agent.retryAfterSubmit = false;
486
512
  queueMicrotask(() => { void retryPending(agent); });
487
513
  }
514
+ queueMicrotask(() => { void scanAndPromoteAcceptedInboxUpdates(agent); });
488
515
  }
489
516
  };
490
517
  const retryPending = async (agent) => {
@@ -524,6 +551,45 @@ export function createRuntimeHost(options) {
524
551
  agent.retryPendingInFlight = null;
525
552
  }
526
553
  };
554
+ /**
555
+ * After busy/submitting clears, accepted inbox_update notices have no later
556
+ * turn boundary. Promote each delivery id at most once to a wake.
557
+ */
558
+ const scanAndPromoteAcceptedInboxUpdates = async (agent) => {
559
+ if (!agentIsIdleForInboxScan(agent))
560
+ return;
561
+ if (agent.compactionMachines.size > 0 || agent.compactionRecoveryInFlight.size > 0)
562
+ return;
563
+ const candidates = [...agent.records.values()].filter((record) => record.status === "accepted"
564
+ && isInboxUpdateKind(record.input?.kind) && !agent.promotedInboxUpdateIds.has(record.deliveryId));
565
+ if (!candidates.length)
566
+ return;
567
+ reconcileExternalConsumption(agent);
568
+ let promoted = 0;
569
+ for (const record of candidates) {
570
+ const current = agent.records.get(record.deliveryId);
571
+ if (current?.status !== "accepted")
572
+ continue;
573
+ if (!acceptedInboxStillPending(agent, current)) {
574
+ reconcileAbsentCanonical(agent, current);
575
+ continue;
576
+ }
577
+ agent.promotedInboxUpdateIds.add(current.deliveryId);
578
+ current.reason = INBOX_UPDATE_PROMOTE_REASON;
579
+ current.retryable = true;
580
+ const finalRecord = setRecord(agent, current, "pending");
581
+ if (finalRecord.status === "consumed")
582
+ continue;
583
+ emit({ type: "delivery", agentId: agent.config.agentId, deliveryId: finalRecord.deliveryId,
584
+ messageId: finalRecord.messageId, status: "deferred", reason: INBOX_UPDATE_PROMOTE_REASON });
585
+ promoted += 1;
586
+ }
587
+ if (!promoted)
588
+ return;
589
+ if (agent.retryPendingInFlight)
590
+ agent.retryAfterSubmit = true;
591
+ await retryPending(agent);
592
+ };
527
593
  const scheduleRecreate = (agent, reason) => {
528
594
  if (agent.stopped || agent.retryTimer || agent.starting || agent.session)
529
595
  return;
@@ -861,6 +927,7 @@ export function createRuntimeHost(options) {
861
927
  if (outcome !== "failed")
862
928
  return retryPending(agent);
863
929
  });
930
+ queueMicrotask(() => { void scanAndPromoteAcceptedInboxUpdates(agent); });
864
931
  }
865
932
  else if (event.type === "activity") {
866
933
  if (agent.turnInProgress && event.activity !== "internal")
@@ -1221,6 +1288,14 @@ export function createRuntimeHost(options) {
1221
1288
  };
1222
1289
  },
1223
1290
  isBusy(agentId) { const agent = managed.get(agentId); return Boolean(agent?.busy || agent?.submitting || agent?.starting); },
1291
+ async scanPendingInboxUpdates(agentId) {
1292
+ const agents = agentId ? [managed.get(agentId)] : [...managed.values()];
1293
+ for (const agent of agents) {
1294
+ if (!agent || agent.stopped)
1295
+ continue;
1296
+ await scanAndPromoteAcceptedInboxUpdates(agent);
1297
+ }
1298
+ },
1224
1299
  async resetSession(agentId) {
1225
1300
  const agent = managed.get(agentId);
1226
1301
  if (!agent || agent.stopped)
@@ -1537,7 +1612,8 @@ export function createRuntimeHost(options) {
1537
1612
  compactionMachines: new Map(), compactionRecoveryInFlight: new Set(), piOverflowCompactionFailed: new Set(),
1538
1613
  piProactiveCompaction: null, piProactiveCompactionGeneration: null, piProactiveCompactionSession: null,
1539
1614
  piProactiveCompactionFailedGeneration: null, readiness: null,
1540
- turnInProgress: false, turnHadFailure: false, turnHadAuthenticatedOutput: false, authFailureActive: false };
1615
+ turnInProgress: false, turnHadFailure: false, turnHadAuthenticatedOutput: false, authFailureActive: false,
1616
+ promotedInboxUpdateIds: new Set() };
1541
1617
  const startupConsumed = [];
1542
1618
  const startupQuarantined = [];
1543
1619
  for (const record of agent.records.values()) {
@@ -1639,6 +1715,7 @@ export function createRuntimeHost(options) {
1639
1715
  : { inputId: existing.deliveryId, deliveryId: existing.deliveryId, kind: "wake", text: "", attempt: priorAttempt + 1 };
1640
1716
  delete existing.reason;
1641
1717
  delete existing.retryable;
1718
+ agent.promotedInboxUpdateIds.delete(existing.deliveryId);
1642
1719
  setRecord(agent, existing, "pending");
1643
1720
  const receipt = await (telemetry?.phase(messageId, "runtime.deliver", SpanKind.PRODUCER, () => submit(agent, existing, agent.busy || agent.submitting)) ?? submit(agent, existing, agent.busy || agent.submitting));
1644
1721
  telemetry?.delivery(agentId, messageId, receipt.status);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "larkin",
3
- "version": "0.4.7",
3
+ "version": "0.4.8",
4
4
  "private": false,
5
5
  "larkinPackageRole": "npm-published",
6
6
  "files": [