larkin 0.3.3 → 0.4.2

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.
Files changed (32) hide show
  1. package/README.md +72 -33
  2. package/assets/readme/larkin-hero.svg +111 -0
  3. package/dist/agent/agent-state-store.cjs +321 -22
  4. package/dist/agent/agent-state-store.mjs +320 -22
  5. package/dist/agent/agent-transport.cjs +6 -4
  6. package/dist/agent/agent-transport.mjs +6 -4
  7. package/dist/agent/host-reminder-orchestrator.cjs +9 -3
  8. package/dist/agent/host-reminder-orchestrator.mjs +9 -3
  9. package/dist/agent/inbox-projection.cjs +114 -23
  10. package/dist/agent/inbox-projection.mjs +112 -23
  11. package/dist/app/cli.mjs +3 -1
  12. package/dist/app/local-control.mjs +108 -2
  13. package/dist/app/runtime-process.mjs +4 -0
  14. package/dist/app/session-cli.mjs +55 -20
  15. package/dist/feishu/document-comment.cjs +2 -1
  16. package/dist/feishu/document-comment.mjs +2 -1
  17. package/dist/feishu/host-business-state.cjs +7 -2
  18. package/dist/feishu/host-business-state.mjs +7 -2
  19. package/dist/feishu/host-shell.cjs +138 -32
  20. package/dist/feishu/host-shell.mjs +138 -32
  21. package/dist/runtime/pi-compaction-recovery.cjs +623 -0
  22. package/dist/runtime/pi-compaction-recovery.mjs +602 -0
  23. package/dist/runtime/pi-provider-config.cjs +2 -2
  24. package/dist/runtime/pi-provider-config.mjs +2 -2
  25. package/dist/runtime/pi-rpc-client.cjs +39 -22
  26. package/dist/runtime/pi-rpc-client.mjs +39 -22
  27. package/dist/runtime/provider-error-classifier.cjs +39 -0
  28. package/dist/runtime/provider-error-classifier.mjs +34 -0
  29. package/dist/runtime/runtime-adapters.mjs +82 -19
  30. package/dist/runtime/runtime-host.cjs +663 -12
  31. package/dist/runtime/runtime-host.mjs +661 -11
  32. package/package.json +1 -1
@@ -2,10 +2,13 @@ import crypto from "node:crypto";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { agentCliPromptCapabilities } from "../agent/agent-cli-capabilities.mjs";
5
+ import { isCanonicalInboxTarget, targetKeyOfInboxEnvelope } from "../agent/inbox-projection.mjs";
5
6
  import { SpanKind } from "@opentelemetry/api";
7
+ import { buildStrictProviderErrorInput, classifyStrictProviderError } from "./provider-error-classifier.mjs";
6
8
  import { classifyRuntimePrerequisite, providerAuthenticationFailureReadiness, RuntimePrerequisiteError, } from "./runtime-readiness.mjs";
7
9
  import { resolveOfficialLarkCli } from "../app/official-lark-cli.mjs";
8
10
  import { assertAgentWorkspaceBound, managedLarkCliEnv } from "../app/agent-lark-cli-workspace.mjs";
11
+ import { PiCompactionBreaker, PiCompactionRecoveryMachine } from "./pi-compaction-recovery.mjs";
9
12
  export class RuntimeSessionResetError extends Error {
10
13
  code;
11
14
  pendingCount;
@@ -16,9 +19,33 @@ export class RuntimeSessionResetError extends Error {
16
19
  this.name = "RuntimeSessionResetError";
17
20
  }
18
21
  }
22
+ export class RuntimeSessionRecoveryError extends Error {
23
+ code;
24
+ pendingCount;
25
+ constructor(code, message, pendingCount = 0) {
26
+ super(message);
27
+ this.code = code;
28
+ this.pendingCount = pendingCount;
29
+ this.name = "RuntimeSessionRecoveryError";
30
+ }
31
+ }
19
32
  const MAX_DELIVERIES = 2048;
20
33
  const now = () => new Date().toISOString();
21
34
  const isActiveDelivery = (status) => ["pending", "submitting", "accepted"].includes(status);
35
+ const REPLAY_FAILURE_CODES = new Set([
36
+ "canonical_inbox_row_missing", "canonical_inbox_malformed", "duplicate_message_id",
37
+ "inbox_state_conflict", "delivery_target_conflict", "structured_target_invalid",
38
+ "structured_target_missing", "wake_reason_conflict",
39
+ ]);
40
+ function replayFailureReason(code) {
41
+ return `Runtime delivery quarantined (${code}): no safe canonical Inbox target is available; the Inbox/ledger remain durable for operator recovery`;
42
+ }
43
+ function replayFailureCodeOf(record) {
44
+ if (record.status !== "error" || typeof record.reason !== "string")
45
+ return null;
46
+ const code = /^Runtime delivery quarantined \(([^)]+)\):/.exec(record.reason)?.[1];
47
+ return code && REPLAY_FAILURE_CODES.has(code) ? code : null;
48
+ }
22
49
  function deliveryFile(agent) {
23
50
  return agent.config.stateDir ? path.join(agent.config.stateDir, "runtime-deliveries.json") : null;
24
51
  }
@@ -91,6 +118,9 @@ export function createRuntimeHost(options) {
91
118
  const listeners = new Set();
92
119
  const log = options.log ?? (() => { });
93
120
  const telemetry = options.telemetry;
121
+ const compactTimeoutMs = options.compactTimeoutMs ?? 120_000;
122
+ if (!Number.isFinite(compactTimeoutMs) || compactTimeoutMs <= 0)
123
+ throw new Error("compactTimeoutMs must be positive");
94
124
  const retryPolicy = {
95
125
  baseDelayMs: options.retryPolicy?.baseDelayMs ?? 250,
96
126
  maxDelayMs: options.retryPolicy?.maxDelayMs ?? 10_000,
@@ -151,6 +181,82 @@ export function createRuntimeHost(options) {
151
181
  emitConsumed(agent, persist(agent));
152
182
  return agent.records.get(record.deliveryId) ?? record;
153
183
  };
184
+ const scrubQuarantinedInput = (record, reason) => {
185
+ const staleInput = record.input && typeof record.input === "object" ? record.input : null;
186
+ record.input = {
187
+ inputId: typeof staleInput?.inputId === "string" && staleInput.inputId ? staleInput.inputId : record.deliveryId,
188
+ deliveryId: record.deliveryId,
189
+ kind: "wake",
190
+ text: reason,
191
+ attempt: Number.isSafeInteger(staleInput?.attempt) && Number(staleInput?.attempt) >= 0 ? Number(staleInput.attempt) : 0,
192
+ };
193
+ };
194
+ /** Rebuild every submitted notice from canonical structured state, never persisted text. */
195
+ const prepareRecordInput = (agent, record, busy) => {
196
+ if (record.target !== undefined && (typeof record.target !== "string" || !isCanonicalInboxTarget(record.target))) {
197
+ return { status: "error", code: "structured_target_invalid", reason: replayFailureReason("structured_target_invalid") };
198
+ }
199
+ if (record.wakeReason !== undefined && typeof record.wakeReason !== "string") {
200
+ return { status: "error", code: "wake_reason_conflict", reason: replayFailureReason("wake_reason_conflict") };
201
+ }
202
+ let target = typeof record.target === "string" ? record.target : null;
203
+ let wakeReason = typeof record.wakeReason === "string" ? record.wakeReason : undefined;
204
+ if (agent.stateStore?.resolveInboxDeliverySource) {
205
+ let source;
206
+ try {
207
+ source = agent.stateStore.resolveInboxDeliverySource(record.messageId);
208
+ }
209
+ catch {
210
+ source = { status: "invalid", code: "canonical_inbox_malformed" };
211
+ }
212
+ if (source.status === "consumed")
213
+ return { status: "consumed" };
214
+ if (source.status === "missing" || source.status === "invalid") {
215
+ return { status: "error", code: source.code, reason: replayFailureReason(source.code) };
216
+ }
217
+ if (target && target !== source.target) {
218
+ return { status: "error", code: "delivery_target_conflict", reason: replayFailureReason("delivery_target_conflict") };
219
+ }
220
+ const canonicalWakeReason = typeof source.envelope.wake_reason === "string" ? source.envelope.wake_reason : undefined;
221
+ if (record.wakeReason !== undefined && wakeReason !== canonicalWakeReason) {
222
+ return { status: "error", code: "wake_reason_conflict", reason: replayFailureReason("wake_reason_conflict") };
223
+ }
224
+ target = source.target;
225
+ wakeReason = canonicalWakeReason;
226
+ }
227
+ if (!target)
228
+ return { status: "error", code: "structured_target_missing", reason: replayFailureReason("structured_target_missing") };
229
+ const staleInput = record.input && typeof record.input === "object" ? record.input : null;
230
+ const attempt = Number.isSafeInteger(staleInput?.attempt) && Number(staleInput?.attempt) >= 0 ? Number(staleInput.attempt) : 0;
231
+ const inputId = typeof staleInput?.inputId === "string" && staleInput.inputId ? staleInput.inputId : record.deliveryId;
232
+ record.target = target;
233
+ if (wakeReason)
234
+ record.wakeReason = wakeReason;
235
+ else
236
+ delete record.wakeReason;
237
+ record.input = {
238
+ inputId,
239
+ deliveryId: record.deliveryId,
240
+ kind: busy ? "inbox_update" : "wake",
241
+ text: options.promptBuilder.buildInboxNotice({ busy, count: 1, deliveryId: record.deliveryId, target,
242
+ ...(wakeReason ? { wakeReason } : {}) }),
243
+ attempt,
244
+ };
245
+ return { status: "ready", target };
246
+ };
247
+ const quarantineRecord = (agent, record, code) => {
248
+ const reason = replayFailureReason(code);
249
+ scrubQuarantinedInput(record, reason);
250
+ record.reason = reason;
251
+ record.retryable = false;
252
+ delete record.errorCategory;
253
+ const finalRecord = setRecord(agent, record, "error");
254
+ if (finalRecord.status === "consumed")
255
+ return { status: "accepted", deliveryId: record.deliveryId };
256
+ emit({ type: "delivery", agentId: agent.config.agentId, deliveryId: record.deliveryId,
257
+ messageId: record.messageId, status: "error", reason });
258
+ return { status: "error", deliveryId: record.deliveryId, reason, retryable: false };
259
+ };
154
260
  const reconcileExternalConsumption = (agent) => {
155
261
  if (agent.stateStore) {
156
262
  agent.stateStore.withInboxTransaction(() => reconcileFile(agent, agent.stateStore.readJson("runtimeDeliveries", { version: 1, records: [] })));
@@ -284,12 +390,14 @@ export function createRuntimeHost(options) {
284
390
  if (result.status === "accepted") {
285
391
  delete record.reason;
286
392
  delete record.retryable;
393
+ delete record.errorCategory;
287
394
  const finalRecord = setRecord(agent, record, "accepted");
288
395
  if (finalRecord.status !== "consumed")
289
396
  emit({ type: "delivery", agentId: agent.config.agentId,
290
397
  deliveryId: record.deliveryId, messageId: record.messageId, status: "accepted" });
291
398
  return { status: "accepted", deliveryId: record.deliveryId };
292
399
  }
400
+ delete record.errorCategory;
293
401
  const retryable = result.status === "deferred" || result.retryable;
294
402
  record.reason = result.reason;
295
403
  record.retryable = retryable;
@@ -307,6 +415,20 @@ export function createRuntimeHost(options) {
307
415
  if (!busy)
308
416
  agent.busy = true; // Reserve the turn before prompt() can yield.
309
417
  try {
418
+ const prepared = prepareRecordInput(agent, record, busy);
419
+ if (prepared.status === "consumed") {
420
+ if (!busy)
421
+ agent.busy = false;
422
+ const consumedRecord = setRecord(agent, record, "consumed");
423
+ emit({ type: "delivery", agentId: agent.config.agentId, deliveryId: consumedRecord.deliveryId,
424
+ messageId: consumedRecord.messageId, status: "consumed" });
425
+ return { status: "accepted", deliveryId: record.deliveryId };
426
+ }
427
+ if (prepared.status === "error") {
428
+ if (!busy)
429
+ agent.busy = false;
430
+ return quarantineRecord(agent, record, prepared.code);
431
+ }
310
432
  const session = await ensureSession(agent);
311
433
  const submittingRecord = setRecord(agent, record, "submitting");
312
434
  if (submittingRecord.status === "consumed") {
@@ -330,6 +452,7 @@ export function createRuntimeHost(options) {
330
452
  const reason = error instanceof Error ? error.message : String(error);
331
453
  record.reason = reason;
332
454
  record.retryable = true;
455
+ delete record.errorCategory;
333
456
  setRecord(agent, record, "pending");
334
457
  emit({ type: "delivery", agentId: agent.config.agentId, deliveryId: record.deliveryId, messageId: record.messageId, status: "deferred", reason });
335
458
  return { status: "deferred", deliveryId: record.deliveryId, reason };
@@ -456,6 +579,106 @@ export function createRuntimeHost(options) {
456
579
  })();
457
580
  agent.configurationRecovery = recovery;
458
581
  };
582
+ let recoverContextOverflowInternal;
583
+ const beginPiManualCompaction = (agent, session, record, nativeWillRetry = false) => {
584
+ if (agent.adapter.id !== "pi" || !session.compact || !agent.compactionBreaker)
585
+ return;
586
+ const existing = agent.compactionMachines.get(record.input.inputId);
587
+ if (existing) {
588
+ existing.agentEnd({ exactOverflow: true, willRetry: nativeWillRetry });
589
+ return;
590
+ }
591
+ const key = `${record.deliveryId}:${record.input.inputId}`;
592
+ const prior = agent.compactionBreaker.get(key);
593
+ if (prior && prior.manualAttempt >= 1 && ["manual_sent", "manual_ambiguous", "manual_failed", "native_failed"].includes(prior.state)) {
594
+ agent.busy = false;
595
+ agent.turnInProgress = false;
596
+ void recoverContextOverflowInternal(agent.config.agentId, key, "validated Pi compaction breaker state after restart", key)
597
+ .catch((error) => log("Pi restart fallback failed", String(error)));
598
+ return;
599
+ }
600
+ const machine = new PiCompactionRecoveryMachine({
601
+ breaker: agent.compactionBreaker, key, messageId: record.messageId, deliveryId: record.deliveryId,
602
+ inputId: record.input.inputId, sessionGeneration: agent.generation,
603
+ onAction: (action) => {
604
+ if (action === "retry_input") {
605
+ machine.retrySubmitted();
606
+ record.status = "pending";
607
+ record.retryable = true;
608
+ record.reason = "Pi manual compaction succeeded; retrying the same delivery input";
609
+ record.updatedAt = now();
610
+ persist(agent);
611
+ queueMicrotask(() => { if (!agent.stopped)
612
+ void submit(agent, record, false); });
613
+ }
614
+ else if (action === "fresh_session_fallback") {
615
+ agent.busy = false;
616
+ agent.turnInProgress = false;
617
+ if (record.status !== "error") {
618
+ record.reason = machine.recordSnapshot.fallbackReason || "Pi compaction recovery fallback";
619
+ record.retryable = false;
620
+ record.errorCategory = "context_window";
621
+ setRecord(agent, record, "error");
622
+ }
623
+ void recoverContextOverflowInternal(agent.config.agentId, key, machine.recordSnapshot.fallbackReason || "Pi compaction recovery fallback", key)
624
+ .catch((error) => log("Pi context fallback failed", String(error)));
625
+ }
626
+ },
627
+ });
628
+ agent.compactionMachines.set(record.input.inputId, machine);
629
+ machine.agentEnd({ exactOverflow: true, willRetry: nativeWillRetry });
630
+ if (nativeWillRetry)
631
+ return;
632
+ machine.agentSettled();
633
+ const deadline = new Date(Date.now() + compactTimeoutMs).toISOString();
634
+ machine.manualCompactSent(deadline);
635
+ let compactSettled = false;
636
+ const compactTimer = setTimeout(() => {
637
+ if (compactSettled)
638
+ return;
639
+ compactSettled = true;
640
+ machine.compactTimeout();
641
+ }, compactTimeoutMs);
642
+ compactTimer.unref?.();
643
+ void session.compact().then(() => {
644
+ if (compactSettled)
645
+ return;
646
+ compactSettled = true;
647
+ clearTimeout(compactTimer);
648
+ machine.compactResponse({ success: true });
649
+ }).catch((error) => {
650
+ if (compactSettled)
651
+ return;
652
+ compactSettled = true;
653
+ clearTimeout(compactTimer);
654
+ const message = error instanceof Error ? error.message : String(error);
655
+ machine.compactResponse({ success: false, ambiguous: /process exited|process failed|session replaced|protocol/i.test(message) });
656
+ });
657
+ };
658
+ const recoverStalePiCompaction = async (agent) => {
659
+ const breaker = agent.compactionBreaker;
660
+ if (!breaker)
661
+ return;
662
+ const stale = breaker.listNonTerminal().find((candidate) => candidate.state !== "native_succeeded");
663
+ if (!stale) {
664
+ const succeeded = breaker.listNonTerminal().find((candidate) => candidate.state === "native_succeeded");
665
+ if (succeeded)
666
+ breaker.transition(succeeded.key, {}, "closed");
667
+ return;
668
+ }
669
+ const record = [...agent.records.values()].find((candidate) => candidate.deliveryId === stale.deliveryId
670
+ || candidate.input.inputId === stale.inputId);
671
+ if (!record)
672
+ return;
673
+ record.reason = stale.fallbackReason || "Pi compaction recovery resumed after restart";
674
+ record.retryable = false;
675
+ record.errorCategory = "context_window";
676
+ setRecord(agent, record, "error");
677
+ breaker.forceFallback(stale.key, record.reason);
678
+ agent.busy = false;
679
+ agent.turnInProgress = false;
680
+ await recoverContextOverflowInternal(agent.config.agentId, stale.key, record.reason, stale.key);
681
+ };
459
682
  const observe = (agent, session, event) => {
460
683
  if (agent.session !== session)
461
684
  return; // Ignore late output from a replaced child.
@@ -464,6 +687,34 @@ export function createRuntimeHost(options) {
464
687
  if (event.type !== "turn-end")
465
688
  telemetry?.runtimeEvent(agent.config.agentId, event);
466
689
  emit({ type: "runtime", agentId: agent.config.agentId, event });
690
+ if (event.type === "runtime-observation") {
691
+ if (event.phase === "compaction_start" && event.reason === "overflow" && event.inputId) {
692
+ const overflowRecord = [...agent.records.values()].find((candidate) => candidate.input.inputId === event.inputId);
693
+ if (overflowRecord && !agent.compactionMachines.has(event.inputId))
694
+ beginPiManualCompaction(agent, session, overflowRecord, true);
695
+ }
696
+ const candidates = [...agent.compactionMachines.values()].filter((machine) => machine.recordSnapshot.sessionGeneration === agent.generation
697
+ && (!event.inputId || machine.recordSnapshot.inputId === event.inputId));
698
+ const machines = event.inputId ? candidates : candidates.length === 1 ? candidates : [];
699
+ for (const machine of machines) {
700
+ if (event.phase === "compaction_start" && event.reason === "overflow")
701
+ machine.compactionStart({ reason: "overflow" });
702
+ if (event.phase === "compaction_end" && event.reason === "manual") {
703
+ machine.compactionEnd({ reason: "manual", success: event.success === true, willRetry: event.willRetry === true });
704
+ }
705
+ if (event.phase === "compaction_end" && event.reason === "overflow") {
706
+ machine.compactionEnd({ reason: "overflow", success: event.success === true, willRetry: event.willRetry === true });
707
+ }
708
+ if (event.phase === "settled")
709
+ machine.agentSettled();
710
+ }
711
+ if (event.phase === "compaction_end" && event.reason === "overflow" && event.success !== true) {
712
+ for (const record of agent.records.values()) {
713
+ if (isActiveDelivery(record.status))
714
+ agent.piOverflowCompactionFailed.add(record.input.inputId);
715
+ }
716
+ }
717
+ }
467
718
  if (event.type === "session-init") {
468
719
  agent.config.sessionId = event.sessionId;
469
720
  emit({ type: "session", agentId: agent.config.agentId, runtime: agent.adapter.id, sessionId: event.sessionId, launchId: agent.launchId,
@@ -479,6 +730,13 @@ export function createRuntimeHost(options) {
479
730
  emit({ type: "activity", agentId: agent.config.agentId, activity: "working", activityKind: "working", detailKind: "turn_started" });
480
731
  }
481
732
  else if (event.type === "turn-end") {
733
+ for (const [inputId, machine] of agent.compactionMachines) {
734
+ if (machine.recordSnapshot.sessionGeneration !== agent.generation)
735
+ continue;
736
+ machine.close();
737
+ if (["closed", "fallback_committed"].includes(machine.state))
738
+ agent.compactionMachines.delete(inputId);
739
+ }
482
740
  const recoveredAuthentication = agent.authFailureActive && agent.turnInProgress
483
741
  && agent.turnHadAuthenticatedOutput && !agent.turnHadFailure;
484
742
  agent.turnInProgress = false;
@@ -511,6 +769,29 @@ export function createRuntimeHost(options) {
511
769
  if (!record || record.status === "consumed" || record.status === "error")
512
770
  return;
513
771
  agent.turnHadFailure = true;
772
+ if (agent.adapter.id === "pi" && event.errorCategory === "context_window" && event.willRetry === true && session.compact) {
773
+ beginPiManualCompaction(agent, session, record, true);
774
+ return;
775
+ }
776
+ if (agent.adapter.id === "pi" && event.errorCategory === "context_window" && event.willRetry !== true && session.compact) {
777
+ record.reason = event.message;
778
+ record.retryable = false;
779
+ record.errorCategory = "context_window";
780
+ setRecord(agent, record, "error");
781
+ if (agent.piOverflowCompactionFailed.has(record.input.inputId)) {
782
+ agent.busy = false;
783
+ agent.turnInProgress = false;
784
+ void recoverContextOverflowInternal(agent.config.agentId, `${record.deliveryId}:${record.input.inputId}`, "native Pi overflow compaction failed", `${record.deliveryId}:${record.input.inputId}`)
785
+ .catch((error) => log("Pi native compaction fallback failed", String(error)));
786
+ return;
787
+ }
788
+ // Pi emits this terminal error only after agent_settled; release the
789
+ // normal turn reservation before the one authorized recovery action.
790
+ agent.busy = false;
791
+ agent.turnInProgress = false;
792
+ beginPiManualCompaction(agent, session, record);
793
+ return;
794
+ }
514
795
  if (event.willRetry) {
515
796
  emit({ type: "delivery", agentId: agent.config.agentId, deliveryId: record.deliveryId,
516
797
  messageId: record.messageId, status: "deferred", reason: event.message });
@@ -521,6 +802,13 @@ export function createRuntimeHost(options) {
521
802
  agent.busy = false;
522
803
  record.reason = event.message;
523
804
  record.retryable = event.retryable;
805
+ const classifiedCategory = event.errorCategory ?? classifyStrictProviderError(buildStrictProviderErrorInput({
806
+ message: event.message, errorCategory: event.errorCategory, upstream: event.upstream,
807
+ }));
808
+ if (classifiedCategory)
809
+ record.errorCategory = classifiedCategory;
810
+ else
811
+ delete record.errorCategory;
524
812
  const finalRecord = setRecord(agent, record, event.retryable ? "pending" : "error");
525
813
  if (finalRecord.status !== "consumed")
526
814
  emit({ type: "delivery", agentId: agent.config.agentId,
@@ -607,6 +895,129 @@ export function createRuntimeHost(options) {
607
895
  });
608
896
  return agent.starting;
609
897
  };
898
+ recoverContextOverflowInternal = async (agentId, deliveryKey, reason, breakerKey) => {
899
+ const agent = managed.get(agentId);
900
+ if (!agent || agent.stopped)
901
+ throw new RuntimeSessionRecoveryError("unknown_agent", `unknown runtime Agent: ${agentId}`);
902
+ if (agent.adapter.id !== "pi")
903
+ throw new RuntimeSessionRecoveryError("recovery_unavailable", "Pi context fallback is unavailable for this runtime");
904
+ const oldSession = agent.session;
905
+ if (!oldSession || agent.busy || agent.submitting || agent.starting) {
906
+ throw new RuntimeSessionRecoveryError("agent_busy", `Agent ${agentId} is not idle for Pi context fallback`);
907
+ }
908
+ const record = [...agent.records.values()].find((candidate) => candidate.deliveryId === deliveryKey || candidate.input.inputId === deliveryKey || `${candidate.deliveryId}:${candidate.input.inputId}` === deliveryKey);
909
+ if (!record)
910
+ throw new RuntimeSessionRecoveryError("recovery_refused", "Pi context fallback delivery identity is unavailable");
911
+ const probeEnv = runtimeEnv(agent.config, `${agent.launchId}:pi-context-fallback`);
912
+ await assertOfficialCliReady(agent.config, probeEnv);
913
+ const readiness = agent.adapter.probe ? await agent.adapter.probe({ agentId: agent.config.agentId,
914
+ workspaceDir: agent.config.workspaceDir, stateDir: agent.config.stateDir, env: probeEnv }) : { runtime: "pi", state: "ready" };
915
+ if (readiness.state !== "ready")
916
+ throw new RuntimeSessionRecoveryError("recovery_staged_not_committed", "Pi context fallback readiness failed");
917
+ const standingPrompt = options.promptBuilder.build({
918
+ agentId: agent.config.agentId, name: agent.config.displayName || agent.config.name,
919
+ description: agent.config.description || "", runtime: agent.adapter.id, cli: agentCliPromptCapabilities("larkin"),
920
+ });
921
+ let fresh;
922
+ try {
923
+ fresh = await agent.adapter.createSession({ agentId: agent.config.agentId, model: agent.config.model,
924
+ reasoningEffort: agent.config.effort || null, workspaceDir: agent.config.workspaceDir, stateDir: agent.config.stateDir,
925
+ resumeSessionId: null, standingPrompt, env: probeEnv });
926
+ }
927
+ catch {
928
+ throw new RuntimeSessionRecoveryError("recovery_staged_not_committed", "Pi fresh fallback session could not be staged");
929
+ }
930
+ if (agent.session !== oldSession || agent.busy || agent.submitting || agent.starting || agent.stopped) {
931
+ await fresh.close("Pi context fallback commit precondition changed").catch(() => { });
932
+ throw new RuntimeSessionRecoveryError("agent_busy", `Agent ${agentId} changed during Pi context fallback`);
933
+ }
934
+ let unsubscribeFresh = null;
935
+ try {
936
+ unsubscribeFresh = fresh.subscribe((next) => observe(agent, fresh, next));
937
+ }
938
+ catch {
939
+ await fresh.close("Pi context fallback subscription failed").catch(() => { });
940
+ throw new RuntimeSessionRecoveryError("recovery_staged_not_committed", "Pi context fallback subscription failed");
941
+ }
942
+ const oldSessionId = oldSession.sessionId;
943
+ const oldGeneration = agent.generation;
944
+ const oldLaunchId = agent.launchId;
945
+ const oldConfigSessionId = agent.config.sessionId;
946
+ const oldReadiness = agent.readiness;
947
+ const oldDisabledReason = agent.disabledReason;
948
+ const oldRecord = { ...record, input: { ...record.input } };
949
+ let durableRollback = null;
950
+ let rearmedCount = 1;
951
+ let remainingPendingCount = 1;
952
+ const commitSwap = (messageIds, rollback) => {
953
+ if (agent.session !== oldSession || agent.busy || agent.submitting || agent.starting || agent.stopped) {
954
+ throw new RuntimeSessionRecoveryError("agent_busy", `Agent ${agentId} changed during Pi context fallback`);
955
+ }
956
+ durableRollback = rollback ?? null;
957
+ if (breakerKey && agent.compactionBreaker)
958
+ agent.compactionBreaker.forceFallbackInTransaction(breakerKey, reason);
959
+ agent.generation += 1;
960
+ agent.launchId = crypto.randomUUID();
961
+ agent.config.sessionId = null;
962
+ agent.session = fresh;
963
+ agent.readiness = readiness;
964
+ agent.disabledReason = null;
965
+ for (const candidate of agent.records.values()) {
966
+ if (!messageIds.includes(candidate.messageId))
967
+ continue;
968
+ candidate.status = "pending";
969
+ candidate.retryable = true;
970
+ candidate.reason = "context-window recovery rearmed the retained Inbox delivery";
971
+ candidate.updatedAt = now();
972
+ }
973
+ };
974
+ try {
975
+ if (agent.stateStore?.rearmContextOverflow) {
976
+ const rearmed = agent.stateStore.rearmContextOverflow((messageIds, rollback) => commitSwap(messageIds, rollback), {
977
+ messageId: record.messageId, deliveryId: record.deliveryId, inputId: record.input.inputId,
978
+ });
979
+ rearmedCount = rearmed.rearmedCount;
980
+ remainingPendingCount = rearmed.remainingPendingCount;
981
+ }
982
+ else {
983
+ commitSwap([record.messageId]);
984
+ persist(agent);
985
+ }
986
+ }
987
+ catch (error) {
988
+ try {
989
+ const rollback = durableRollback;
990
+ if (rollback)
991
+ rollback();
992
+ }
993
+ catch { /* preserve primary error */ }
994
+ agent.generation = oldGeneration;
995
+ agent.launchId = oldLaunchId;
996
+ agent.config.sessionId = oldConfigSessionId;
997
+ agent.session = oldSession;
998
+ agent.readiness = oldReadiness;
999
+ agent.disabledReason = oldDisabledReason;
1000
+ Object.assign(record, oldRecord);
1001
+ try {
1002
+ unsubscribeFresh?.();
1003
+ }
1004
+ catch { /* preserve primary failure */ }
1005
+ await fresh.close("Pi context fallback rolled back").catch(() => { });
1006
+ throw new RuntimeSessionRecoveryError("recovery_staged_not_committed", error instanceof Error ? error.message : "Pi context fallback commit failed");
1007
+ }
1008
+ if (fresh.sessionId) {
1009
+ agent.config.sessionId = fresh.sessionId;
1010
+ emit({ type: "session", agentId, runtime: agent.adapter.id, sessionId: fresh.sessionId, launchId: agent.launchId,
1011
+ ...(fresh.effectiveModel ? { model: fresh.effectiveModel } : {}),
1012
+ ...(fresh.effectiveReasoningEffort ? { reasoningEffort: fresh.effectiveReasoningEffort } : {}) });
1013
+ }
1014
+ emit({ type: "agent-status", agentId, status: "active", readiness });
1015
+ await oldSession.close("Pi context fallback committed").catch((error) => log("previous Pi session close after fallback failed", String(error)));
1016
+ void retryPending(agent);
1017
+ return { generationChanged: true, sessionChanged: oldSessionId !== fresh.sessionId, turns: 0,
1018
+ runtimeReady: true, pendingCount: remainingPendingCount, rearmedCount,
1019
+ replayStatus: remainingPendingCount > 0 ? "pending" : "consumed", sessionId: fresh.sessionId };
1020
+ };
610
1021
  return {
611
1022
  async probe(config) {
612
1023
  const adapter = options.adapterFor(config.runtime);
@@ -781,6 +1192,180 @@ export function createRuntimeHost(options) {
781
1192
  return { generationChanged: true, sessionChanged: oldSessionId !== fresh.sessionId, turns: 0,
782
1193
  runtimeReady: true, pendingCount: 0, sessionId: fresh.sessionId };
783
1194
  },
1195
+ async recoverSession(agentId, reason) {
1196
+ if (reason !== "context-overflow")
1197
+ throw new RuntimeSessionRecoveryError("recovery_refused", "unsupported recovery reason");
1198
+ const agent = managed.get(agentId);
1199
+ if (agent && agent.adapter.id !== "pi")
1200
+ throw new RuntimeSessionRecoveryError("recovery_unavailable", "context-overflow recovery is available only for Pi Runtime");
1201
+ if (!agent || agent.stopped)
1202
+ throw new RuntimeSessionRecoveryError("unknown_agent", `unknown runtime Agent: ${agentId}`);
1203
+ if (!agent.stateStore?.rearmContextOverflow) {
1204
+ throw new RuntimeSessionRecoveryError("recovery_unavailable", "canonical Inbox recovery is unavailable");
1205
+ }
1206
+ if (agent.busy || agent.turnInProgress || agent.submitting || agent.starting) {
1207
+ throw new RuntimeSessionRecoveryError("agent_busy", `Agent ${agentId} is not idle`);
1208
+ }
1209
+ const oldSession = agent.session;
1210
+ if (!oldSession)
1211
+ throw new RuntimeSessionRecoveryError("agent_busy", `Agent ${agentId} Runtime session is unavailable`);
1212
+ const oldSessionId = oldSession.sessionId;
1213
+ if (agent.stateStore.readNdjson) {
1214
+ const pendingRows = agent.stateStore.withInboxTransaction(() => agent.stateStore.readNdjson("inbox"));
1215
+ if (pendingRows.length === 0) {
1216
+ throw new RuntimeSessionRecoveryError("recovery_refused", "context-window recovery has no retained Inbox backlog");
1217
+ }
1218
+ const messageIds = new Set(pendingRows.flatMap((row) => {
1219
+ const messageId = row && typeof row === "object" && !Array.isArray(row)
1220
+ ? row.message_id : undefined;
1221
+ return typeof messageId === "string" && messageId ? [messageId] : [];
1222
+ }));
1223
+ const persisted = agent.stateStore.readJson("runtimeDeliveries", { version: 1, records: [] });
1224
+ if (Array.isArray(persisted.records) && persisted.records.some((record) => messageIds.has(record.messageId) && record.status !== "error")) {
1225
+ throw new RuntimeSessionRecoveryError("recovery_refused", "context-window recovery is already staged or completed");
1226
+ }
1227
+ }
1228
+ const probeEnv = runtimeEnv(agent.config, `${agent.launchId}:context-overflow-recovery`);
1229
+ let readiness;
1230
+ try {
1231
+ await assertOfficialCliReady(agent.config, probeEnv);
1232
+ readiness = agent.adapter.probe ? await agent.adapter.probe({ agentId: agent.config.agentId,
1233
+ workspaceDir: agent.config.workspaceDir, stateDir: agent.config.stateDir,
1234
+ env: { LARKIN_PI_COMMAND: process.env.LARKIN_PI_COMMAND, LARKIN_CODEX_COMMAND: process.env.LARKIN_CODEX_COMMAND,
1235
+ LARKIN_CLAUDE_COMMAND: process.env.LARKIN_CLAUDE_COMMAND, ...probeEnv } })
1236
+ : { runtime: agent.adapter.id, state: "ready" };
1237
+ }
1238
+ catch (error) {
1239
+ if (error instanceof RuntimePrerequisiteError)
1240
+ throw error;
1241
+ throw new RuntimeSessionRecoveryError("recovery_staged_not_committed", "Runtime readiness probe failed");
1242
+ }
1243
+ if (readiness.state !== "ready")
1244
+ throw new RuntimePrerequisiteError(readiness);
1245
+ const standingPrompt = options.promptBuilder.build({
1246
+ agentId: agent.config.agentId, name: agent.config.displayName || agent.config.name,
1247
+ description: agent.config.description || "", runtime: agent.adapter.id,
1248
+ cli: agentCliPromptCapabilities("larkin"),
1249
+ });
1250
+ let fresh;
1251
+ try {
1252
+ fresh = await agent.adapter.createSession({
1253
+ agentId: agent.config.agentId, model: agent.config.model, reasoningEffort: agent.config.effort || null,
1254
+ workspaceDir: agent.config.workspaceDir, stateDir: agent.config.stateDir,
1255
+ resumeSessionId: null, standingPrompt, env: probeEnv,
1256
+ });
1257
+ }
1258
+ catch {
1259
+ throw new RuntimeSessionRecoveryError("recovery_staged_not_committed", "fresh Runtime session could not be staged");
1260
+ }
1261
+ let unsubscribeFresh = null;
1262
+ try {
1263
+ unsubscribeFresh = fresh.subscribe((event) => observe(agent, fresh, event));
1264
+ }
1265
+ catch {
1266
+ await fresh.close("context-window recovery subscription failed").catch(() => { });
1267
+ throw new RuntimeSessionRecoveryError("recovery_staged_not_committed", "fresh Runtime session subscription failed");
1268
+ }
1269
+ const oldGeneration = agent.generation;
1270
+ const oldLaunchId = agent.launchId;
1271
+ const oldConfigSessionId = agent.config.sessionId;
1272
+ const oldReadiness = agent.readiness;
1273
+ const oldDisabledReason = agent.disabledReason;
1274
+ const oldRecords = new Map([...agent.records.entries()].map(([id, record]) => [id, { ...record, input: { ...record.input } }]));
1275
+ const cleanupFresh = async (reason) => {
1276
+ const unsubscribe = unsubscribeFresh;
1277
+ unsubscribeFresh = null;
1278
+ try {
1279
+ unsubscribe?.();
1280
+ }
1281
+ catch { /* close still owns staged-session cleanup */ }
1282
+ await fresh.close(reason).catch(() => { });
1283
+ };
1284
+ const restoreRuntime = () => {
1285
+ agent.session = oldSession;
1286
+ agent.generation = oldGeneration;
1287
+ agent.launchId = oldLaunchId;
1288
+ agent.config.sessionId = oldConfigSessionId;
1289
+ agent.readiness = oldReadiness;
1290
+ agent.disabledReason = oldDisabledReason;
1291
+ agent.records = new Map(oldRecords);
1292
+ agent.byMessage = new Map([...oldRecords.values()].map((record) => [record.messageId, record.deliveryId]));
1293
+ };
1294
+ let recoveryProjectionEmitted = false;
1295
+ const compensateProjection = () => {
1296
+ if (!recoveryProjectionEmitted)
1297
+ return;
1298
+ try {
1299
+ emit({ type: "session", agentId, runtime: agent.adapter.id, sessionId: oldSessionId, launchId: oldLaunchId });
1300
+ }
1301
+ catch { /* preserve the original listener failure */ }
1302
+ try {
1303
+ emit({ type: "agent-status", agentId, status: "active", ...(oldReadiness ? { readiness: oldReadiness } : {}) });
1304
+ }
1305
+ catch { /* preserve the original listener failure */ }
1306
+ };
1307
+ let durableRollback = null;
1308
+ try {
1309
+ const rearmed = agent.stateStore.rearmContextOverflow((messageIds, rollback) => {
1310
+ // This callback runs under the canonical Inbox lock. All potentially
1311
+ // racing checks above are synchronous from this process, while the
1312
+ // state-store method rechecks every row and delivery on disk.
1313
+ if (agent.session !== oldSession || agent.busy || agent.turnInProgress || agent.submitting || agent.starting || agent.stopped) {
1314
+ throw new RuntimeSessionRecoveryError("agent_busy", `Agent ${agentId} changed during recovery`);
1315
+ }
1316
+ durableRollback = rollback;
1317
+ agent.generation += 1;
1318
+ agent.launchId = crypto.randomUUID();
1319
+ agent.config.sessionId = null;
1320
+ agent.session = fresh;
1321
+ agent.readiness = readiness;
1322
+ agent.disabledReason = null;
1323
+ for (const record of agent.records.values()) {
1324
+ if (!messageIds.includes(record.messageId))
1325
+ continue;
1326
+ record.status = "pending";
1327
+ record.retryable = true;
1328
+ record.reason = "context-window recovery rearmed the retained Inbox delivery";
1329
+ record.updatedAt = now();
1330
+ }
1331
+ });
1332
+ recoveryProjectionEmitted = true;
1333
+ if (fresh.sessionId) {
1334
+ agent.config.sessionId = fresh.sessionId;
1335
+ emit({ type: "session", agentId, runtime: agent.adapter.id, sessionId: fresh.sessionId, launchId: agent.launchId,
1336
+ ...(fresh.effectiveModel ? { model: fresh.effectiveModel } : {}),
1337
+ ...(fresh.effectiveReasoningEffort ? { reasoningEffort: fresh.effectiveReasoningEffort } : {}) });
1338
+ }
1339
+ emit({ type: "agent-status", agentId, status: "active", readiness });
1340
+ durableRollback = null;
1341
+ await oldSession.close("context-window recovery committed")
1342
+ .catch((error) => log("previous runtime close after context-window recovery failed", String(error)));
1343
+ void retryPending(agent);
1344
+ return { generationChanged: true, sessionChanged: oldSessionId !== fresh.sessionId, turns: 0,
1345
+ runtimeReady: true, pendingCount: rearmed.remainingPendingCount, rearmedCount: rearmed.rearmedCount,
1346
+ replayStatus: rearmed.remainingPendingCount > 0 ? "pending" : "consumed", sessionId: fresh.sessionId };
1347
+ }
1348
+ catch (error) {
1349
+ try {
1350
+ const rollback = durableRollback;
1351
+ if (rollback)
1352
+ rollback();
1353
+ }
1354
+ catch { /* preserve the original failure */ }
1355
+ restoreRuntime();
1356
+ compensateProjection();
1357
+ await cleanupFresh("context-window recovery not committed");
1358
+ if (error instanceof RuntimeSessionRecoveryError)
1359
+ throw error;
1360
+ if (typeof error.code === "string") {
1361
+ throw new RuntimeSessionRecoveryError("recovery_refused", error instanceof Error ? error.message : "context-window recovery was refused");
1362
+ }
1363
+ throw new RuntimeSessionRecoveryError("recovery_staged_not_committed", "context-window recovery was not committed");
1364
+ }
1365
+ },
1366
+ async recoverContextOverflow(agentId, deliveryKey, reason) {
1367
+ return recoverContextOverflowInternal(agentId, deliveryKey, reason);
1368
+ },
784
1369
  async start(configs) {
785
1370
  let activeCount = 0;
786
1371
  let recoveringCount = 0;
@@ -799,7 +1384,14 @@ export function createRuntimeHost(options) {
799
1384
  const deliveryFile = stateStore.readJson("runtimeDeliveries", { version: 1, records: [] });
800
1385
  if (!stateStore.readNdjson)
801
1386
  return deliveryFile;
802
- const inboxIds = new Set(stateStore.readNdjson("inbox").flatMap((row) => typeof row?.message_id === "string" ? [row.message_id] : []));
1387
+ let inboxRows;
1388
+ try {
1389
+ inboxRows = stateStore.readNdjson("inbox");
1390
+ }
1391
+ catch {
1392
+ return deliveryFile;
1393
+ }
1394
+ const inboxIds = new Set(inboxRows.flatMap((row) => typeof row?.message_id === "string" ? [row.message_id] : []));
803
1395
  let changed = false;
804
1396
  const records = deliveryFile.records.map((record) => {
805
1397
  const synthetic = /^(?:redeliver_|rem_|interaction_)/.test(record.messageId);
@@ -820,15 +1412,44 @@ export function createRuntimeHost(options) {
820
1412
  byMessage: new Map(persisted.records.map((record) => [record.messageId, record.deliveryId])),
821
1413
  generation: 0, poller: null, retryTimer: null, recreateAttempts: 0,
822
1414
  stabilityTimer: null, recreateReason: null, stopped: false, disabledReason: null,
823
- configurationRecovery: null, stateStore, readiness: null,
1415
+ configurationRecovery: null, stateStore,
1416
+ compactionBreaker: config.runtime === "pi"
1417
+ ? new PiCompactionBreaker(config.stateDir ?? path.join(config.workspaceDir, ".larkin", "agents", config.agentId), {
1418
+ withLock: stateStore ? (operation) => stateStore.withInboxTransaction(operation) : undefined,
1419
+ }) : undefined,
1420
+ compactionMachines: new Map(), piOverflowCompactionFailed: new Set(), readiness: null,
824
1421
  turnInProgress: false, turnHadFailure: false, turnHadAuthenticatedOutput: false, authFailureActive: false };
825
- for (const record of agent.records.values())
826
- if (isActiveDelivery(record.status))
827
- record.status = "pending";
828
- emitConsumed(agent, persist(agent));
1422
+ const startupConsumed = [];
1423
+ const startupQuarantined = [];
1424
+ for (const record of agent.records.values()) {
1425
+ const existingQuarantine = replayFailureCodeOf(record);
1426
+ if (existingQuarantine) {
1427
+ startupQuarantined.push({ record, code: existingQuarantine });
1428
+ continue;
1429
+ }
1430
+ if (!isActiveDelivery(record.status))
1431
+ continue;
1432
+ record.status = "pending";
1433
+ const prepared = prepareRecordInput(agent, record, false);
1434
+ if (prepared.status === "consumed") {
1435
+ record.status = "consumed";
1436
+ record.updatedAt = now();
1437
+ startupConsumed.push(record);
1438
+ }
1439
+ else if (prepared.status === "error") {
1440
+ record.status = "error";
1441
+ record.updatedAt = now();
1442
+ scrubQuarantinedInput(record, prepared.reason);
1443
+ record.reason = prepared.reason;
1444
+ record.retryable = false;
1445
+ startupQuarantined.push({ record, code: prepared.code });
1446
+ }
1447
+ }
829
1448
  managed.set(config.agentId, agent);
1449
+ emitConsumed(agent, [...startupConsumed, ...persist(agent)]);
830
1450
  try {
831
1451
  await ensureSession(agent);
1452
+ await recoverStalePiCompaction(agent);
832
1453
  activeCount += 1;
833
1454
  await retryPending(agent);
834
1455
  }
@@ -844,6 +1465,13 @@ export function createRuntimeHost(options) {
844
1465
  emit({ type: "agent-status", agentId: config.agentId, status: "error", error: reason,
845
1466
  ...(error instanceof RuntimePrerequisiteError ? { readiness: error.readiness } : {}) });
846
1467
  }
1468
+ const visibleQuarantines = startupQuarantined.filter(({ record }) => agent.records.get(record.deliveryId)?.status === "error");
1469
+ for (const { record, code } of visibleQuarantines)
1470
+ emit({ type: "delivery", agentId: config.agentId,
1471
+ deliveryId: record.deliveryId, messageId: record.messageId, status: "error", reason: replayFailureReason(code) });
1472
+ if (visibleQuarantines.length)
1473
+ emit({ type: "agent-status", agentId: config.agentId, status: "error",
1474
+ error: `${visibleQuarantines.length} Runtime delivery record(s) quarantined; canonical Inbox recovery is required` });
847
1475
  agent.poller = setInterval(() => reconcileExternalConsumption(agent), 250);
848
1476
  agent.poller.unref?.();
849
1477
  });
@@ -859,12 +1487,33 @@ export function createRuntimeHost(options) {
859
1487
  telemetry?.delivery(agentId, messageId, "error");
860
1488
  throw new Error(`unknown runtime Agent: ${agentId}`);
861
1489
  }
1490
+ let target;
1491
+ try {
1492
+ target = targetKeyOfInboxEnvelope(envelope);
1493
+ }
1494
+ catch (error) {
1495
+ const reason = `Inbox target derivation failed: ${error instanceof Error ? error.message : String(error)}`;
1496
+ const deliveryId = `invalid-target-${crypto.createHash("sha256").update(`${agentId}\0${messageId}`).digest("hex").slice(0, 24)}`;
1497
+ telemetry?.delivery(agentId, messageId, "error");
1498
+ emit({ type: "delivery", agentId, deliveryId, messageId, status: "error", reason });
1499
+ return { status: "error", deliveryId, reason, retryable: false };
1500
+ }
862
1501
  reconcileExternalConsumption(agent);
863
1502
  const existingId = agent.byMessage.get(messageId);
864
1503
  if (existingId) {
865
1504
  const existing = agent.records.get(existingId);
866
1505
  if (existing?.status === "error") {
867
- existing.input.attempt += 1;
1506
+ if (existing.target && existing.target !== target)
1507
+ return quarantineRecord(agent, existing, "delivery_target_conflict");
1508
+ existing.target = target;
1509
+ if (typeof envelope.wake_reason === "string" && envelope.wake_reason)
1510
+ existing.wakeReason = envelope.wake_reason;
1511
+ else
1512
+ delete existing.wakeReason;
1513
+ const priorAttempt = existing.input && Number.isSafeInteger(existing.input.attempt) ? existing.input.attempt : 0;
1514
+ existing.input = existing.input && typeof existing.input === "object"
1515
+ ? { ...existing.input, attempt: priorAttempt + 1 }
1516
+ : { inputId: existing.deliveryId, deliveryId: existing.deliveryId, kind: "wake", text: "", attempt: priorAttempt + 1 };
868
1517
  delete existing.reason;
869
1518
  delete existing.retryable;
870
1519
  setRecord(agent, existing, "pending");
@@ -885,11 +1534,12 @@ export function createRuntimeHost(options) {
885
1534
  }
886
1535
  const deliveryId = crypto.randomUUID();
887
1536
  const busy = agent.busy || agent.submitting;
1537
+ const wakeReason = typeof envelope.wake_reason === "string" ? envelope.wake_reason : undefined;
888
1538
  const input = { inputId: deliveryId, deliveryId, kind: busy ? "inbox_update" : "wake",
889
- text: options.promptBuilder.buildInboxNotice({ busy, count: 1, deliveryId,
890
- ...(typeof envelope.target === "string" ? { target: envelope.target } : {}),
891
- ...(typeof envelope.wake_reason === "string" ? { wakeReason: envelope.wake_reason } : {}) }), attempt: 0 };
892
- const record = { deliveryId, messageId, status: "pending", input, updatedAt: now() };
1539
+ text: options.promptBuilder.buildInboxNotice({ busy, count: 1, deliveryId, target,
1540
+ ...(wakeReason ? { wakeReason } : {}) }), attempt: 0 };
1541
+ const record = { deliveryId, messageId, status: "pending", input, target,
1542
+ ...(wakeReason ? { wakeReason } : {}), updatedAt: now() };
893
1543
  agent.records.set(deliveryId, record);
894
1544
  agent.byMessage.set(messageId, deliveryId);
895
1545
  persist(agent);