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
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.AgentStateStore = void 0;
36
+ exports.AgentStateStore = exports.ContextOverflowRecoveryError = void 0;
37
37
  exports.createAgentStateStore = createAgentStateStore;
38
38
  const fs = __importStar(require("node:fs"));
39
39
  const crypto = __importStar(require("node:crypto"));
@@ -42,6 +42,7 @@ const root_layout_js_1 = require("../platform/root-layout.cjs");
42
42
  const process_state_js_1 = require("../platform/process-state.cjs");
43
43
  const secure_metadata_js_1 = require("../platform/secure-metadata.cjs");
44
44
  const inbox_projection_js_1 = require("./inbox-projection.cjs");
45
+ const provider_error_classifier_js_1 = require("../runtime/provider-error-classifier.cjs");
45
46
  const JSON_KEYS = new Set([
46
47
  "agentState", "status", "map", "replyctx", "botIdentity", "senderProfiles", "readReceipts", "pendingReact", "runtimeDeliveries", "inboxState", "freshnessState", "documentComments", "reminders", "interactions",
47
48
  ]);
@@ -140,7 +141,9 @@ function normalizeInboxState(value) {
140
141
  continue;
141
142
  const row = candidate;
142
143
  if (typeof row.target === "string" && row.target && validSequence(row.seq))
143
- state.messages[messageId] = { target: row.target, seq: row.seq };
144
+ state.messages[messageId] = {
145
+ target: row.target, seq: row.seq, ...(typeof row.kind === "string" && row.kind ? { kind: row.kind } : {}),
146
+ };
144
147
  }
145
148
  }
146
149
  if (raw.drafts && typeof raw.drafts === "object" && !Array.isArray(raw.drafts)) {
@@ -195,6 +198,15 @@ function validInboxLockOwner(value) {
195
198
  && typeof owner.processStartToken === "string" && !!owner.processStartToken
196
199
  && typeof owner.nonce === "string" && /^[0-9a-f-]{36}$/i.test(owner.nonce);
197
200
  }
201
+ class ContextOverflowRecoveryError extends Error {
202
+ code;
203
+ constructor(code, message) {
204
+ super(message);
205
+ this.code = code;
206
+ this.name = "ContextOverflowRecoveryError";
207
+ }
208
+ }
209
+ exports.ContextOverflowRecoveryError = ContextOverflowRecoveryError;
198
210
  class AgentStateStore {
199
211
  paths;
200
212
  boundary;
@@ -461,13 +473,14 @@ class AgentStateStore {
461
473
  if (!value || typeof value !== "object" || Array.isArray(value))
462
474
  throw new Error("Inbox envelope must be an object");
463
475
  const input = value;
464
- const target = typeof input.target === "string" && input.target ? input.target : (0, inbox_projection_js_1.targetKeyOfInboxEnvelope)(input);
476
+ const target = (0, inbox_projection_js_1.targetKeyOfInboxEnvelope)(input);
465
477
  const current = state.targets[target] ?? { latest_received_seq: 0, model_seen_seq: 0 };
466
478
  const requested = validSequence(input.target_seq) ? input.target_seq : 0;
467
479
  const targetSeq = requested > current.latest_received_seq ? requested : current.latest_received_seq + 1;
468
480
  state.targets[target] = { ...current, latest_received_seq: targetSeq };
469
481
  if (typeof input.message_id === "string" && input.message_id) {
470
- state.messages[input.message_id] = { target, seq: targetSeq };
482
+ state.messages[input.message_id] = { target, seq: targetSeq,
483
+ ...(typeof input.kind === "string" && input.kind ? { kind: input.kind } : {}) };
471
484
  const messageIds = Object.keys(state.messages);
472
485
  for (const stale of messageIds.slice(0, Math.max(0, messageIds.length - 2_048)))
473
486
  delete state.messages[stale];
@@ -475,8 +488,9 @@ class AgentStateStore {
475
488
  return { ...input, envelope_version: 2, target, target_seq: targetSeq };
476
489
  }
477
490
  reconcileInboxRows(rows, state) {
478
- return rows.map((row) => {
479
- const target = typeof row.target === "string" && row.target ? row.target : (0, inbox_projection_js_1.targetKeyOfInboxEnvelope)(row);
491
+ const targets = rows.map((row) => (0, inbox_projection_js_1.targetKeyOfInboxEnvelope)(row));
492
+ return rows.map((row, index) => {
493
+ const target = targets[index];
480
494
  const existing = typeof row.message_id === "string" ? state.messages[row.message_id] : undefined;
481
495
  const current = state.targets[target] ?? { latest_received_seq: 0, model_seen_seq: 0 };
482
496
  const targetSeq = validSequence(row.target_seq) ? row.target_seq
@@ -484,7 +498,8 @@ class AgentStateStore {
484
498
  current.latest_received_seq = Math.max(current.latest_received_seq, targetSeq);
485
499
  state.targets[target] = current;
486
500
  if (typeof row.message_id === "string" && row.message_id)
487
- state.messages[row.message_id] = { target, seq: targetSeq };
501
+ state.messages[row.message_id] = { target, seq: targetSeq,
502
+ ...(typeof row.kind === "string" && row.kind ? { kind: row.kind } : {}) };
488
503
  return { ...row, envelope_version: 2, target, target_seq: targetSeq };
489
504
  });
490
505
  }
@@ -610,23 +625,302 @@ class AgentStateStore {
610
625
  else
611
626
  append();
612
627
  }
613
- /** Append a canonical Inbox envelope once by stable message_id under the shared cross-process lock. */
614
- appendInboxOnce(value) {
628
+ /**
629
+ * Append one canonical Inbox envelope and return the exact normalized object
630
+ * serialized to disk. Stable message_id dedupe and locator coherence share the
631
+ * same lock as poll, so HostShell can deliver that very object without a
632
+ * persistence/delivery split.
633
+ */
634
+ appendCanonicalInboxOnce(value) {
615
635
  if (!value || typeof value !== "object" || Array.isArray(value))
616
636
  throw new Error("Inbox envelope must be an object");
617
- const messageId = value.message_id;
637
+ const input = value;
638
+ const messageId = input.message_id;
618
639
  if (typeof messageId !== "string" || !messageId)
619
640
  throw new Error("Inbox envelope requires message_id");
641
+ const incomingTarget = (0, inbox_projection_js_1.targetKeyOfInboxEnvelope)(input);
620
642
  const file = this.file("inbox");
621
643
  return this.withInboxLock(file, () => {
622
- if (this.inboxState().messages[messageId])
623
- return false;
624
- if (this.readNdjson("inbox").some((row) => row.message_id === messageId))
625
- return false;
626
- this.appendInboxUnlocked(value);
627
- return true;
644
+ const state = this.inboxState();
645
+ const matching = this.readNdjson("inbox").filter((row) => row.message_id === messageId);
646
+ if (matching.length > 1)
647
+ throw new Error("Inbox duplicate message_id has multiple pending canonical rows");
648
+ if (matching.length === 1) {
649
+ const existing = matching[0];
650
+ if ((0, inbox_projection_js_1.targetKeyOfInboxEnvelope)(existing) !== incomingTarget) {
651
+ throw new Error("Inbox duplicate message_id conflicts with its canonical target");
652
+ }
653
+ if (existing.target_seq !== undefined && !validSequence(existing.target_seq)) {
654
+ throw new Error("Inbox duplicate message_id has a malformed canonical sequence");
655
+ }
656
+ const known = state.messages[messageId];
657
+ if (known) {
658
+ if (known.target !== incomingTarget)
659
+ throw new Error("Inbox message state conflicts with its canonical target");
660
+ if (validSequence(existing.target_seq) && existing.target_seq !== known.seq) {
661
+ throw new Error("Inbox message state conflicts with its canonical sequence");
662
+ }
663
+ const targetState = state.targets[known.target];
664
+ if (targetState && targetState.model_seen_seq >= known.seq) {
665
+ throw new Error("Inbox pending row conflicts with consumed model-seen state");
666
+ }
667
+ }
668
+ return { status: "duplicate_pending", envelope: existing };
669
+ }
670
+ const known = state.messages[messageId];
671
+ if (known) {
672
+ if (known.target !== incomingTarget)
673
+ throw new Error("Inbox duplicate message_id conflicts with consumed canonical target");
674
+ const targetState = state.targets[known.target];
675
+ if (!targetState || targetState.model_seen_seq < known.seq) {
676
+ throw new Error("Inbox state references a missing unconsumed canonical row");
677
+ }
678
+ return { status: "duplicate_consumed", envelope: null };
679
+ }
680
+ return { status: "appended", envelope: this.appendInboxUnlocked(input) };
681
+ });
682
+ }
683
+ /** Append a canonical Inbox envelope once by stable message_id under the shared cross-process lock. */
684
+ appendInboxOnce(value) {
685
+ return this.appendCanonicalInboxOnce(value).status === "appended";
686
+ }
687
+ /**
688
+ * Resolve one Runtime delivery solely from canonical Inbox row/state. This is
689
+ * the single replay resolver used for startup migration and every retry; it
690
+ * never derives a DM/generic fallback from stale RuntimeInput text.
691
+ */
692
+ resolveInboxDeliverySource(messageId) {
693
+ if (!messageId)
694
+ return { status: "missing", code: "canonical_inbox_row_missing" };
695
+ return this.withInboxLock(this.file("inbox"), () => {
696
+ let rows;
697
+ try {
698
+ rows = this.readNdjson("inbox");
699
+ }
700
+ catch {
701
+ return { status: "invalid", code: "canonical_inbox_malformed" };
702
+ }
703
+ const matching = rows.filter((row) => row?.message_id === messageId);
704
+ if (matching.length > 1)
705
+ return { status: "invalid", code: "duplicate_message_id" };
706
+ const state = this.inboxState();
707
+ const known = state.messages[messageId];
708
+ if (matching.length === 1) {
709
+ const envelope = matching[0];
710
+ let target;
711
+ try {
712
+ target = (0, inbox_projection_js_1.targetKeyOfInboxEnvelope)(envelope);
713
+ }
714
+ catch {
715
+ return { status: "invalid", code: "canonical_inbox_malformed" };
716
+ }
717
+ if (envelope.target_seq !== undefined && !validSequence(envelope.target_seq)) {
718
+ return { status: "invalid", code: "canonical_inbox_malformed" };
719
+ }
720
+ if (known) {
721
+ const targetState = state.targets[known.target];
722
+ if (known.target !== target || !validSequence(known.seq)
723
+ || (validSequence(envelope.target_seq) && envelope.target_seq !== known.seq)
724
+ || Boolean(targetState && targetState.model_seen_seq >= known.seq)) {
725
+ return { status: "invalid", code: "inbox_state_conflict" };
726
+ }
727
+ }
728
+ return { status: "pending", target, envelope };
729
+ }
730
+ if (!known)
731
+ return { status: "missing", code: "canonical_inbox_row_missing" };
732
+ if (!(0, inbox_projection_js_1.isCanonicalInboxTarget)(known.target) || !validSequence(known.seq)) {
733
+ return { status: "invalid", code: "inbox_state_conflict" };
734
+ }
735
+ const targetState = state.targets[known.target];
736
+ if (targetState && targetState.model_seen_seq >= known.seq)
737
+ return { status: "consumed", target: known.target };
738
+ return { status: "missing", code: "canonical_inbox_row_missing" };
628
739
  });
629
740
  }
741
+ /**
742
+ * Re-arm only the durable deliveries proven to be the context-window incident.
743
+ * The Inbox bytes are intentionally untouched. An optional synchronous commit
744
+ * hook lets RuntimeHost swap its in-memory generation while this same lock is
745
+ * still held. If the hook fails, the ledger is restored before the lock is
746
+ * released. The callback also receives a guarded rollback for post-callback
747
+ * commit failures before retry scheduling begins.
748
+ */
749
+ rearmContextOverflow(onCommit, expected) {
750
+ let lockActive = true;
751
+ try {
752
+ return this.withInboxTransaction(() => {
753
+ let rows;
754
+ try {
755
+ rows = this.readNdjson("inbox");
756
+ }
757
+ catch {
758
+ throw new ContextOverflowRecoveryError("canonical_inbox_malformed", "canonical Inbox is malformed");
759
+ }
760
+ if (rows.length === 0)
761
+ throw new ContextOverflowRecoveryError("inbox_empty", "canonical Inbox backlog is empty");
762
+ const messageIds = new Set();
763
+ for (const row of rows) {
764
+ try {
765
+ (0, inbox_projection_js_1.targetKeyOfInboxEnvelope)(row);
766
+ }
767
+ catch {
768
+ throw new ContextOverflowRecoveryError("canonical_inbox_malformed", "canonical Inbox contains a malformed row");
769
+ }
770
+ if (typeof row.message_id !== "string" || !row.message_id) {
771
+ throw new ContextOverflowRecoveryError("canonical_inbox_malformed", "canonical Inbox row has no message identity");
772
+ }
773
+ if (messageIds.has(row.message_id))
774
+ throw new ContextOverflowRecoveryError("duplicate_message_id", "canonical Inbox contains duplicate message identities");
775
+ messageIds.add(row.message_id);
776
+ }
777
+ const ledger = this.readJson("runtimeDeliveries", { version: 1, records: [] });
778
+ if (!Array.isArray(ledger.records))
779
+ throw new ContextOverflowRecoveryError("canonical_inbox_malformed", "Runtime delivery ledger is malformed");
780
+ const ledgerRecords = ledger.records;
781
+ const matches = new Map();
782
+ const deliveryIds = new Set();
783
+ const inputIds = new Set();
784
+ for (const candidate of ledger.records) {
785
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
786
+ continue;
787
+ const record = candidate;
788
+ if (typeof record.messageId !== "string" || !messageIds.has(record.messageId))
789
+ continue;
790
+ if (typeof record.deliveryId !== "string" || !record.deliveryId)
791
+ throw new ContextOverflowRecoveryError("delivery_missing", "a matching Runtime delivery record has no delivery identity");
792
+ if (!record.input || typeof record.input !== "object" || Array.isArray(record.input)) {
793
+ throw new ContextOverflowRecoveryError("delivery_identity_invalid", "a matching Runtime delivery record has no Runtime input identity");
794
+ }
795
+ const input = record.input;
796
+ if (typeof input.inputId !== "string" || !input.inputId.trim()
797
+ || typeof input.deliveryId !== "string" || input.deliveryId !== record.deliveryId) {
798
+ throw new ContextOverflowRecoveryError("delivery_identity_invalid", "a matching Runtime delivery record has malformed input identity");
799
+ }
800
+ if (matches.has(record.messageId))
801
+ throw new ContextOverflowRecoveryError("delivery_duplicate", "a canonical Inbox row maps to multiple delivery records");
802
+ if (deliveryIds.has(record.deliveryId))
803
+ throw new ContextOverflowRecoveryError("delivery_duplicate", "matching Runtime delivery records share a delivery identity");
804
+ if (inputIds.has(input.inputId))
805
+ throw new ContextOverflowRecoveryError("duplicate_input_id", "matching Runtime delivery records share an input identity");
806
+ deliveryIds.add(record.deliveryId);
807
+ inputIds.add(input.inputId);
808
+ matches.set(record.messageId, record);
809
+ }
810
+ if (expected?.messageId && !messageIds.has(expected.messageId)) {
811
+ throw new ContextOverflowRecoveryError("delivery_missing", "the expected context-overflow message is not in canonical Inbox");
812
+ }
813
+ if (expected?.messageId) {
814
+ const expectedRecord = matches.get(expected.messageId);
815
+ if (!expectedRecord || (expected.deliveryId && expectedRecord.deliveryId !== expected.deliveryId)
816
+ || (expected.inputId && expectedRecord.input.inputId !== expected.inputId)) {
817
+ throw new ContextOverflowRecoveryError("delivery_identity_invalid", "the expected context-overflow delivery identity changed");
818
+ }
819
+ }
820
+ for (const messageId of messageIds) {
821
+ const record = matches.get(messageId);
822
+ if (!record)
823
+ throw new ContextOverflowRecoveryError("delivery_missing", "a canonical Inbox row has no Runtime delivery record");
824
+ if (record.status !== "error" || record.retryable === true) {
825
+ throw new ContextOverflowRecoveryError("delivery_not_terminal", "a canonical Inbox row is not backed by a non-retryable terminal Runtime error");
826
+ }
827
+ const reasonCategory = (0, provider_error_classifier_js_1.classifyStrictProviderError)((0, provider_error_classifier_js_1.buildStrictProviderErrorInput)({
828
+ reason: record.reason, errorCategory: record.errorCategory,
829
+ }));
830
+ if (record.retryable !== false || reasonCategory !== "context_window"
831
+ || (record.errorCategory !== undefined && record.errorCategory !== "context_window")) {
832
+ throw new ContextOverflowRecoveryError("delivery_not_context_window", "a Runtime delivery error is not classified as context_window");
833
+ }
834
+ }
835
+ for (const candidate of ledgerRecords) {
836
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
837
+ continue;
838
+ const record = candidate;
839
+ if (typeof record.deliveryId === "string" && deliveryIds.has(record.deliveryId)
840
+ && (typeof record.messageId !== "string" || !messageIds.has(record.messageId))) {
841
+ throw new ContextOverflowRecoveryError("delivery_duplicate", "a matching delivery identity is also used by an unrelated Runtime delivery record");
842
+ }
843
+ }
844
+ const updatedAt = new Date().toISOString();
845
+ const records = ledgerRecords.map((candidate) => {
846
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
847
+ return candidate;
848
+ const record = candidate;
849
+ if (typeof record.messageId !== "string" || !messageIds.has(record.messageId))
850
+ return candidate;
851
+ return { ...record, status: "pending", retryable: true, updatedAt,
852
+ reason: "context-window recovery rearmed the retained Inbox delivery" };
853
+ });
854
+ const committedLedger = { ...ledger, records };
855
+ let restored = false;
856
+ const restoreUnlocked = () => {
857
+ if (restored)
858
+ return;
859
+ const current = this.readJson("runtimeDeliveries", { version: 1, records: [] });
860
+ if (!Array.isArray(current.records))
861
+ throw new Error("Runtime delivery rollback found a malformed ledger");
862
+ const originalMatching = ledgerRecords.filter((candidate) => {
863
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
864
+ return false;
865
+ const record = candidate;
866
+ return typeof record.messageId === "string" && messageIds.has(record.messageId);
867
+ });
868
+ const originalDeliveryIds = new Set(originalMatching.flatMap((candidate) => {
869
+ const deliveryId = candidate.deliveryId;
870
+ return typeof deliveryId === "string" ? [deliveryId] : [];
871
+ }));
872
+ const currentMatching = current.records.filter((candidate) => {
873
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
874
+ return false;
875
+ const record = candidate;
876
+ return (typeof record.deliveryId === "string" && originalDeliveryIds.has(record.deliveryId))
877
+ || (typeof record.messageId === "string" && messageIds.has(record.messageId));
878
+ });
879
+ const matchingRestored = currentMatching.length === originalMatching.length
880
+ && currentMatching.every((candidate, index) => JSON.stringify(candidate) === JSON.stringify(originalMatching[index]));
881
+ if (!matchingRestored) {
882
+ const unrelated = current.records.filter((candidate) => {
883
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
884
+ return true;
885
+ const record = candidate;
886
+ return !((typeof record.deliveryId === "string" && originalDeliveryIds.has(record.deliveryId))
887
+ || (typeof record.messageId === "string" && messageIds.has(record.messageId)));
888
+ });
889
+ const restoredRecords = [...unrelated];
890
+ for (const candidate of originalMatching) {
891
+ const originalIndex = ledgerRecords.indexOf(candidate);
892
+ restoredRecords.splice(Math.min(originalIndex, restoredRecords.length), 0, candidate);
893
+ }
894
+ this.writeJson("runtimeDeliveries", { ...current, records: restoredRecords });
895
+ }
896
+ restored = true;
897
+ };
898
+ const rollback = () => {
899
+ if (lockActive)
900
+ restoreUnlocked();
901
+ else
902
+ this.withInboxTransaction(restoreUnlocked);
903
+ };
904
+ try {
905
+ this.writeJson("runtimeDeliveries", committedLedger);
906
+ onCommit?.([...messageIds], rollback);
907
+ }
908
+ catch (error) {
909
+ try {
910
+ rollback();
911
+ }
912
+ catch {
913
+ throw new Error("context-window recovery ledger rollback failed");
914
+ }
915
+ throw error;
916
+ }
917
+ return { rearmedCount: messageIds.size, remainingPendingCount: rows.length };
918
+ });
919
+ }
920
+ finally {
921
+ lockActive = false;
922
+ }
923
+ }
630
924
  /**
631
925
  * Recoverably bridge an outbox wake into the canonical Inbox. A durable
632
926
  * Runtime ledger is authoritative after the Inbox has been drained, so a
@@ -638,6 +932,7 @@ class AgentStateStore {
638
932
  const messageId = value.message_id;
639
933
  if (typeof messageId !== "string" || !messageId)
640
934
  throw new Error("Inbox envelope requires message_id");
935
+ (0, inbox_projection_js_1.targetKeyOfInboxEnvelope)(value);
641
936
  const file = this.file("inbox");
642
937
  return this.withInboxLock(file, () => {
643
938
  const ledger = this.readJson("runtimeDeliveries", { version: 1, records: [] });
@@ -686,6 +981,9 @@ class AgentStateStore {
686
981
  * this transaction is active waits and becomes part of the next batch.
687
982
  */
688
983
  pollInbox(options = {}) {
984
+ if (options.target && !(0, inbox_projection_js_1.isCanonicalInboxTarget)(options.target)) {
985
+ throw new Error(`Invalid canonical Inbox poll target ${JSON.stringify(options.target)}`);
986
+ }
689
987
  const file = this.file("inbox");
690
988
  return this.withInboxLock(file, () => {
691
989
  let rawRows;
@@ -704,7 +1002,7 @@ class AgentStateStore {
704
1002
  const envelopes = [];
705
1003
  const remaining = [];
706
1004
  for (const row of rows) {
707
- const selectedTarget = typeof row.target === "string" ? row.target : (0, inbox_projection_js_1.targetKeyOfInboxEnvelope)(row);
1005
+ const selectedTarget = (0, inbox_projection_js_1.targetKeyOfInboxEnvelope)(row);
708
1006
  if (envelopes.length < limit && (!options.target || options.target === selectedTarget))
709
1007
  envelopes.push(row);
710
1008
  else
@@ -713,7 +1011,7 @@ class AgentStateStore {
713
1011
  const pendingCount = remaining.reduce((count, row) => {
714
1012
  if (!options.target)
715
1013
  return count + 1;
716
- const rowTarget = typeof row.target === "string" ? row.target : (0, inbox_projection_js_1.targetKeyOfInboxEnvelope)(row);
1014
+ const rowTarget = (0, inbox_projection_js_1.targetKeyOfInboxEnvelope)(row);
717
1015
  return count + (rowTarget === options.target ? 1 : 0);
718
1016
  }, 0);
719
1017
  if (!envelopes.length)
@@ -747,7 +1045,7 @@ class AgentStateStore {
747
1045
  }
748
1046
  let seenThroughSeq = null;
749
1047
  for (const envelope of envelopes) {
750
- const target = String(envelope.target || (0, inbox_projection_js_1.targetKeyOfInboxEnvelope)(envelope));
1048
+ const target = (0, inbox_projection_js_1.targetKeyOfInboxEnvelope)(envelope);
751
1049
  const targetSeq = Number(envelope.target_seq);
752
1050
  const targetState = state.targets[target] ?? { latest_received_seq: targetSeq, model_seen_seq: 0 };
753
1051
  if (validSequence(targetSeq)) {
@@ -768,11 +1066,12 @@ class AgentStateStore {
768
1066
  resolveInboxMessageTarget(messageId) {
769
1067
  return this.withInboxLock(this.file("inbox"), () => {
770
1068
  const state = this.inboxState();
771
- const known = state.messages[messageId]?.target;
1069
+ const known = state.messages[messageId];
772
1070
  if (known)
773
- return known;
1071
+ return (0, inbox_projection_js_1.targetKeyOfInboxEnvelope)({ message_id: messageId, target: known.target,
1072
+ ...(known.kind ? { kind: known.kind } : {}) });
774
1073
  const row = this.readNdjson("inbox").find((candidate) => candidate.message_id === messageId);
775
- return row ? (typeof row.target === "string" ? row.target : (0, inbox_projection_js_1.targetKeyOfInboxEnvelope)(row)) : null;
1074
+ return row ? (0, inbox_projection_js_1.targetKeyOfInboxEnvelope)(row) : null;
776
1075
  });
777
1076
  }
778
1077
  listInboxDrafts() {