larkin 0.3.3 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -140,7 +140,9 @@ function normalizeInboxState(value) {
140
140
  continue;
141
141
  const row = candidate;
142
142
  if (typeof row.target === "string" && row.target && validSequence(row.seq))
143
- state.messages[messageId] = { target: row.target, seq: row.seq };
143
+ state.messages[messageId] = {
144
+ target: row.target, seq: row.seq, ...(typeof row.kind === "string" && row.kind ? { kind: row.kind } : {}),
145
+ };
144
146
  }
145
147
  }
146
148
  if (raw.drafts && typeof raw.drafts === "object" && !Array.isArray(raw.drafts)) {
@@ -461,13 +463,14 @@ class AgentStateStore {
461
463
  if (!value || typeof value !== "object" || Array.isArray(value))
462
464
  throw new Error("Inbox envelope must be an object");
463
465
  const input = value;
464
- const target = typeof input.target === "string" && input.target ? input.target : (0, inbox_projection_js_1.targetKeyOfInboxEnvelope)(input);
466
+ const target = (0, inbox_projection_js_1.targetKeyOfInboxEnvelope)(input);
465
467
  const current = state.targets[target] ?? { latest_received_seq: 0, model_seen_seq: 0 };
466
468
  const requested = validSequence(input.target_seq) ? input.target_seq : 0;
467
469
  const targetSeq = requested > current.latest_received_seq ? requested : current.latest_received_seq + 1;
468
470
  state.targets[target] = { ...current, latest_received_seq: targetSeq };
469
471
  if (typeof input.message_id === "string" && input.message_id) {
470
- state.messages[input.message_id] = { target, seq: targetSeq };
472
+ state.messages[input.message_id] = { target, seq: targetSeq,
473
+ ...(typeof input.kind === "string" && input.kind ? { kind: input.kind } : {}) };
471
474
  const messageIds = Object.keys(state.messages);
472
475
  for (const stale of messageIds.slice(0, Math.max(0, messageIds.length - 2_048)))
473
476
  delete state.messages[stale];
@@ -475,8 +478,9 @@ class AgentStateStore {
475
478
  return { ...input, envelope_version: 2, target, target_seq: targetSeq };
476
479
  }
477
480
  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);
481
+ const targets = rows.map((row) => (0, inbox_projection_js_1.targetKeyOfInboxEnvelope)(row));
482
+ return rows.map((row, index) => {
483
+ const target = targets[index];
480
484
  const existing = typeof row.message_id === "string" ? state.messages[row.message_id] : undefined;
481
485
  const current = state.targets[target] ?? { latest_received_seq: 0, model_seen_seq: 0 };
482
486
  const targetSeq = validSequence(row.target_seq) ? row.target_seq
@@ -484,7 +488,8 @@ class AgentStateStore {
484
488
  current.latest_received_seq = Math.max(current.latest_received_seq, targetSeq);
485
489
  state.targets[target] = current;
486
490
  if (typeof row.message_id === "string" && row.message_id)
487
- state.messages[row.message_id] = { target, seq: targetSeq };
491
+ state.messages[row.message_id] = { target, seq: targetSeq,
492
+ ...(typeof row.kind === "string" && row.kind ? { kind: row.kind } : {}) };
488
493
  return { ...row, envelope_version: 2, target, target_seq: targetSeq };
489
494
  });
490
495
  }
@@ -610,21 +615,117 @@ class AgentStateStore {
610
615
  else
611
616
  append();
612
617
  }
613
- /** Append a canonical Inbox envelope once by stable message_id under the shared cross-process lock. */
614
- appendInboxOnce(value) {
618
+ /**
619
+ * Append one canonical Inbox envelope and return the exact normalized object
620
+ * serialized to disk. Stable message_id dedupe and locator coherence share the
621
+ * same lock as poll, so HostShell can deliver that very object without a
622
+ * persistence/delivery split.
623
+ */
624
+ appendCanonicalInboxOnce(value) {
615
625
  if (!value || typeof value !== "object" || Array.isArray(value))
616
626
  throw new Error("Inbox envelope must be an object");
617
- const messageId = value.message_id;
627
+ const input = value;
628
+ const messageId = input.message_id;
618
629
  if (typeof messageId !== "string" || !messageId)
619
630
  throw new Error("Inbox envelope requires message_id");
631
+ const incomingTarget = (0, inbox_projection_js_1.targetKeyOfInboxEnvelope)(input);
620
632
  const file = this.file("inbox");
621
633
  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;
634
+ const state = this.inboxState();
635
+ const matching = this.readNdjson("inbox").filter((row) => row.message_id === messageId);
636
+ if (matching.length > 1)
637
+ throw new Error("Inbox duplicate message_id has multiple pending canonical rows");
638
+ if (matching.length === 1) {
639
+ const existing = matching[0];
640
+ if ((0, inbox_projection_js_1.targetKeyOfInboxEnvelope)(existing) !== incomingTarget) {
641
+ throw new Error("Inbox duplicate message_id conflicts with its canonical target");
642
+ }
643
+ if (existing.target_seq !== undefined && !validSequence(existing.target_seq)) {
644
+ throw new Error("Inbox duplicate message_id has a malformed canonical sequence");
645
+ }
646
+ const known = state.messages[messageId];
647
+ if (known) {
648
+ if (known.target !== incomingTarget)
649
+ throw new Error("Inbox message state conflicts with its canonical target");
650
+ if (validSequence(existing.target_seq) && existing.target_seq !== known.seq) {
651
+ throw new Error("Inbox message state conflicts with its canonical sequence");
652
+ }
653
+ const targetState = state.targets[known.target];
654
+ if (targetState && targetState.model_seen_seq >= known.seq) {
655
+ throw new Error("Inbox pending row conflicts with consumed model-seen state");
656
+ }
657
+ }
658
+ return { status: "duplicate_pending", envelope: existing };
659
+ }
660
+ const known = state.messages[messageId];
661
+ if (known) {
662
+ if (known.target !== incomingTarget)
663
+ throw new Error("Inbox duplicate message_id conflicts with consumed canonical target");
664
+ const targetState = state.targets[known.target];
665
+ if (!targetState || targetState.model_seen_seq < known.seq) {
666
+ throw new Error("Inbox state references a missing unconsumed canonical row");
667
+ }
668
+ return { status: "duplicate_consumed", envelope: null };
669
+ }
670
+ return { status: "appended", envelope: this.appendInboxUnlocked(input) };
671
+ });
672
+ }
673
+ /** Append a canonical Inbox envelope once by stable message_id under the shared cross-process lock. */
674
+ appendInboxOnce(value) {
675
+ return this.appendCanonicalInboxOnce(value).status === "appended";
676
+ }
677
+ /**
678
+ * Resolve one Runtime delivery solely from canonical Inbox row/state. This is
679
+ * the single replay resolver used for startup migration and every retry; it
680
+ * never derives a DM/generic fallback from stale RuntimeInput text.
681
+ */
682
+ resolveInboxDeliverySource(messageId) {
683
+ if (!messageId)
684
+ return { status: "missing", code: "canonical_inbox_row_missing" };
685
+ return this.withInboxLock(this.file("inbox"), () => {
686
+ let rows;
687
+ try {
688
+ rows = this.readNdjson("inbox");
689
+ }
690
+ catch {
691
+ return { status: "invalid", code: "canonical_inbox_malformed" };
692
+ }
693
+ const matching = rows.filter((row) => row?.message_id === messageId);
694
+ if (matching.length > 1)
695
+ return { status: "invalid", code: "duplicate_message_id" };
696
+ const state = this.inboxState();
697
+ const known = state.messages[messageId];
698
+ if (matching.length === 1) {
699
+ const envelope = matching[0];
700
+ let target;
701
+ try {
702
+ target = (0, inbox_projection_js_1.targetKeyOfInboxEnvelope)(envelope);
703
+ }
704
+ catch {
705
+ return { status: "invalid", code: "canonical_inbox_malformed" };
706
+ }
707
+ if (envelope.target_seq !== undefined && !validSequence(envelope.target_seq)) {
708
+ return { status: "invalid", code: "canonical_inbox_malformed" };
709
+ }
710
+ if (known) {
711
+ const targetState = state.targets[known.target];
712
+ if (known.target !== target || !validSequence(known.seq)
713
+ || (validSequence(envelope.target_seq) && envelope.target_seq !== known.seq)
714
+ || Boolean(targetState && targetState.model_seen_seq >= known.seq)) {
715
+ return { status: "invalid", code: "inbox_state_conflict" };
716
+ }
717
+ }
718
+ return { status: "pending", target, envelope };
719
+ }
720
+ if (!known)
721
+ return { status: "missing", code: "canonical_inbox_row_missing" };
722
+ if (!(0, inbox_projection_js_1.isCanonicalInboxTarget)(known.target) || !validSequence(known.seq)) {
723
+ return { status: "invalid", code: "inbox_state_conflict" };
724
+ }
725
+ const targetState = state.targets[known.target];
726
+ if (targetState && targetState.model_seen_seq >= known.seq)
727
+ return { status: "consumed", target: known.target };
728
+ return { status: "missing", code: "canonical_inbox_row_missing" };
628
729
  });
629
730
  }
630
731
  /**
@@ -638,6 +739,7 @@ class AgentStateStore {
638
739
  const messageId = value.message_id;
639
740
  if (typeof messageId !== "string" || !messageId)
640
741
  throw new Error("Inbox envelope requires message_id");
742
+ (0, inbox_projection_js_1.targetKeyOfInboxEnvelope)(value);
641
743
  const file = this.file("inbox");
642
744
  return this.withInboxLock(file, () => {
643
745
  const ledger = this.readJson("runtimeDeliveries", { version: 1, records: [] });
@@ -686,6 +788,9 @@ class AgentStateStore {
686
788
  * this transaction is active waits and becomes part of the next batch.
687
789
  */
688
790
  pollInbox(options = {}) {
791
+ if (options.target && !(0, inbox_projection_js_1.isCanonicalInboxTarget)(options.target)) {
792
+ throw new Error(`Invalid canonical Inbox poll target ${JSON.stringify(options.target)}`);
793
+ }
689
794
  const file = this.file("inbox");
690
795
  return this.withInboxLock(file, () => {
691
796
  let rawRows;
@@ -704,7 +809,7 @@ class AgentStateStore {
704
809
  const envelopes = [];
705
810
  const remaining = [];
706
811
  for (const row of rows) {
707
- const selectedTarget = typeof row.target === "string" ? row.target : (0, inbox_projection_js_1.targetKeyOfInboxEnvelope)(row);
812
+ const selectedTarget = (0, inbox_projection_js_1.targetKeyOfInboxEnvelope)(row);
708
813
  if (envelopes.length < limit && (!options.target || options.target === selectedTarget))
709
814
  envelopes.push(row);
710
815
  else
@@ -713,7 +818,7 @@ class AgentStateStore {
713
818
  const pendingCount = remaining.reduce((count, row) => {
714
819
  if (!options.target)
715
820
  return count + 1;
716
- const rowTarget = typeof row.target === "string" ? row.target : (0, inbox_projection_js_1.targetKeyOfInboxEnvelope)(row);
821
+ const rowTarget = (0, inbox_projection_js_1.targetKeyOfInboxEnvelope)(row);
717
822
  return count + (rowTarget === options.target ? 1 : 0);
718
823
  }, 0);
719
824
  if (!envelopes.length)
@@ -747,7 +852,7 @@ class AgentStateStore {
747
852
  }
748
853
  let seenThroughSeq = null;
749
854
  for (const envelope of envelopes) {
750
- const target = String(envelope.target || (0, inbox_projection_js_1.targetKeyOfInboxEnvelope)(envelope));
855
+ const target = (0, inbox_projection_js_1.targetKeyOfInboxEnvelope)(envelope);
751
856
  const targetSeq = Number(envelope.target_seq);
752
857
  const targetState = state.targets[target] ?? { latest_received_seq: targetSeq, model_seen_seq: 0 };
753
858
  if (validSequence(targetSeq)) {
@@ -768,11 +873,12 @@ class AgentStateStore {
768
873
  resolveInboxMessageTarget(messageId) {
769
874
  return this.withInboxLock(this.file("inbox"), () => {
770
875
  const state = this.inboxState();
771
- const known = state.messages[messageId]?.target;
876
+ const known = state.messages[messageId];
772
877
  if (known)
773
- return known;
878
+ return (0, inbox_projection_js_1.targetKeyOfInboxEnvelope)({ message_id: messageId, target: known.target,
879
+ ...(known.kind ? { kind: known.kind } : {}) });
774
880
  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;
881
+ return row ? (0, inbox_projection_js_1.targetKeyOfInboxEnvelope)(row) : null;
776
882
  });
777
883
  }
778
884
  listInboxDrafts() {
@@ -4,7 +4,7 @@ import * as path from "node:path";
4
4
  import { TargetRootLayout } from "../platform/root-layout.mjs";
5
5
  import { acquireProcessLock, inspectProcess } from "../platform/process-state.mjs";
6
6
  import { isWindows } from "../platform/secure-metadata.mjs";
7
- import { targetKeyOfInboxEnvelope } from "./inbox-projection.mjs";
7
+ import { isCanonicalInboxTarget, targetKeyOfInboxEnvelope } from "./inbox-projection.mjs";
8
8
  const JSON_KEYS = new Set([
9
9
  "agentState", "status", "map", "replyctx", "botIdentity", "senderProfiles", "readReceipts", "pendingReact", "runtimeDeliveries", "inboxState", "freshnessState", "documentComments", "reminders", "interactions",
10
10
  ]);
@@ -103,7 +103,9 @@ function normalizeInboxState(value) {
103
103
  continue;
104
104
  const row = candidate;
105
105
  if (typeof row.target === "string" && row.target && validSequence(row.seq))
106
- state.messages[messageId] = { target: row.target, seq: row.seq };
106
+ state.messages[messageId] = {
107
+ target: row.target, seq: row.seq, ...(typeof row.kind === "string" && row.kind ? { kind: row.kind } : {}),
108
+ };
107
109
  }
108
110
  }
109
111
  if (raw.drafts && typeof raw.drafts === "object" && !Array.isArray(raw.drafts)) {
@@ -424,13 +426,14 @@ export class AgentStateStore {
424
426
  if (!value || typeof value !== "object" || Array.isArray(value))
425
427
  throw new Error("Inbox envelope must be an object");
426
428
  const input = value;
427
- const target = typeof input.target === "string" && input.target ? input.target : targetKeyOfInboxEnvelope(input);
429
+ const target = targetKeyOfInboxEnvelope(input);
428
430
  const current = state.targets[target] ?? { latest_received_seq: 0, model_seen_seq: 0 };
429
431
  const requested = validSequence(input.target_seq) ? input.target_seq : 0;
430
432
  const targetSeq = requested > current.latest_received_seq ? requested : current.latest_received_seq + 1;
431
433
  state.targets[target] = { ...current, latest_received_seq: targetSeq };
432
434
  if (typeof input.message_id === "string" && input.message_id) {
433
- state.messages[input.message_id] = { target, seq: targetSeq };
435
+ state.messages[input.message_id] = { target, seq: targetSeq,
436
+ ...(typeof input.kind === "string" && input.kind ? { kind: input.kind } : {}) };
434
437
  const messageIds = Object.keys(state.messages);
435
438
  for (const stale of messageIds.slice(0, Math.max(0, messageIds.length - 2_048)))
436
439
  delete state.messages[stale];
@@ -438,8 +441,9 @@ export class AgentStateStore {
438
441
  return { ...input, envelope_version: 2, target, target_seq: targetSeq };
439
442
  }
440
443
  reconcileInboxRows(rows, state) {
441
- return rows.map((row) => {
442
- const target = typeof row.target === "string" && row.target ? row.target : targetKeyOfInboxEnvelope(row);
444
+ const targets = rows.map((row) => targetKeyOfInboxEnvelope(row));
445
+ return rows.map((row, index) => {
446
+ const target = targets[index];
443
447
  const existing = typeof row.message_id === "string" ? state.messages[row.message_id] : undefined;
444
448
  const current = state.targets[target] ?? { latest_received_seq: 0, model_seen_seq: 0 };
445
449
  const targetSeq = validSequence(row.target_seq) ? row.target_seq
@@ -447,7 +451,8 @@ export class AgentStateStore {
447
451
  current.latest_received_seq = Math.max(current.latest_received_seq, targetSeq);
448
452
  state.targets[target] = current;
449
453
  if (typeof row.message_id === "string" && row.message_id)
450
- state.messages[row.message_id] = { target, seq: targetSeq };
454
+ state.messages[row.message_id] = { target, seq: targetSeq,
455
+ ...(typeof row.kind === "string" && row.kind ? { kind: row.kind } : {}) };
451
456
  return { ...row, envelope_version: 2, target, target_seq: targetSeq };
452
457
  });
453
458
  }
@@ -573,21 +578,117 @@ export class AgentStateStore {
573
578
  else
574
579
  append();
575
580
  }
576
- /** Append a canonical Inbox envelope once by stable message_id under the shared cross-process lock. */
577
- appendInboxOnce(value) {
581
+ /**
582
+ * Append one canonical Inbox envelope and return the exact normalized object
583
+ * serialized to disk. Stable message_id dedupe and locator coherence share the
584
+ * same lock as poll, so HostShell can deliver that very object without a
585
+ * persistence/delivery split.
586
+ */
587
+ appendCanonicalInboxOnce(value) {
578
588
  if (!value || typeof value !== "object" || Array.isArray(value))
579
589
  throw new Error("Inbox envelope must be an object");
580
- const messageId = value.message_id;
590
+ const input = value;
591
+ const messageId = input.message_id;
581
592
  if (typeof messageId !== "string" || !messageId)
582
593
  throw new Error("Inbox envelope requires message_id");
594
+ const incomingTarget = targetKeyOfInboxEnvelope(input);
583
595
  const file = this.file("inbox");
584
596
  return this.withInboxLock(file, () => {
585
- if (this.inboxState().messages[messageId])
586
- return false;
587
- if (this.readNdjson("inbox").some((row) => row.message_id === messageId))
588
- return false;
589
- this.appendInboxUnlocked(value);
590
- return true;
597
+ const state = this.inboxState();
598
+ const matching = this.readNdjson("inbox").filter((row) => row.message_id === messageId);
599
+ if (matching.length > 1)
600
+ throw new Error("Inbox duplicate message_id has multiple pending canonical rows");
601
+ if (matching.length === 1) {
602
+ const existing = matching[0];
603
+ if (targetKeyOfInboxEnvelope(existing) !== incomingTarget) {
604
+ throw new Error("Inbox duplicate message_id conflicts with its canonical target");
605
+ }
606
+ if (existing.target_seq !== undefined && !validSequence(existing.target_seq)) {
607
+ throw new Error("Inbox duplicate message_id has a malformed canonical sequence");
608
+ }
609
+ const known = state.messages[messageId];
610
+ if (known) {
611
+ if (known.target !== incomingTarget)
612
+ throw new Error("Inbox message state conflicts with its canonical target");
613
+ if (validSequence(existing.target_seq) && existing.target_seq !== known.seq) {
614
+ throw new Error("Inbox message state conflicts with its canonical sequence");
615
+ }
616
+ const targetState = state.targets[known.target];
617
+ if (targetState && targetState.model_seen_seq >= known.seq) {
618
+ throw new Error("Inbox pending row conflicts with consumed model-seen state");
619
+ }
620
+ }
621
+ return { status: "duplicate_pending", envelope: existing };
622
+ }
623
+ const known = state.messages[messageId];
624
+ if (known) {
625
+ if (known.target !== incomingTarget)
626
+ throw new Error("Inbox duplicate message_id conflicts with consumed canonical target");
627
+ const targetState = state.targets[known.target];
628
+ if (!targetState || targetState.model_seen_seq < known.seq) {
629
+ throw new Error("Inbox state references a missing unconsumed canonical row");
630
+ }
631
+ return { status: "duplicate_consumed", envelope: null };
632
+ }
633
+ return { status: "appended", envelope: this.appendInboxUnlocked(input) };
634
+ });
635
+ }
636
+ /** Append a canonical Inbox envelope once by stable message_id under the shared cross-process lock. */
637
+ appendInboxOnce(value) {
638
+ return this.appendCanonicalInboxOnce(value).status === "appended";
639
+ }
640
+ /**
641
+ * Resolve one Runtime delivery solely from canonical Inbox row/state. This is
642
+ * the single replay resolver used for startup migration and every retry; it
643
+ * never derives a DM/generic fallback from stale RuntimeInput text.
644
+ */
645
+ resolveInboxDeliverySource(messageId) {
646
+ if (!messageId)
647
+ return { status: "missing", code: "canonical_inbox_row_missing" };
648
+ return this.withInboxLock(this.file("inbox"), () => {
649
+ let rows;
650
+ try {
651
+ rows = this.readNdjson("inbox");
652
+ }
653
+ catch {
654
+ return { status: "invalid", code: "canonical_inbox_malformed" };
655
+ }
656
+ const matching = rows.filter((row) => row?.message_id === messageId);
657
+ if (matching.length > 1)
658
+ return { status: "invalid", code: "duplicate_message_id" };
659
+ const state = this.inboxState();
660
+ const known = state.messages[messageId];
661
+ if (matching.length === 1) {
662
+ const envelope = matching[0];
663
+ let target;
664
+ try {
665
+ target = targetKeyOfInboxEnvelope(envelope);
666
+ }
667
+ catch {
668
+ return { status: "invalid", code: "canonical_inbox_malformed" };
669
+ }
670
+ if (envelope.target_seq !== undefined && !validSequence(envelope.target_seq)) {
671
+ return { status: "invalid", code: "canonical_inbox_malformed" };
672
+ }
673
+ if (known) {
674
+ const targetState = state.targets[known.target];
675
+ if (known.target !== target || !validSequence(known.seq)
676
+ || (validSequence(envelope.target_seq) && envelope.target_seq !== known.seq)
677
+ || Boolean(targetState && targetState.model_seen_seq >= known.seq)) {
678
+ return { status: "invalid", code: "inbox_state_conflict" };
679
+ }
680
+ }
681
+ return { status: "pending", target, envelope };
682
+ }
683
+ if (!known)
684
+ return { status: "missing", code: "canonical_inbox_row_missing" };
685
+ if (!isCanonicalInboxTarget(known.target) || !validSequence(known.seq)) {
686
+ return { status: "invalid", code: "inbox_state_conflict" };
687
+ }
688
+ const targetState = state.targets[known.target];
689
+ if (targetState && targetState.model_seen_seq >= known.seq)
690
+ return { status: "consumed", target: known.target };
691
+ return { status: "missing", code: "canonical_inbox_row_missing" };
591
692
  });
592
693
  }
593
694
  /**
@@ -601,6 +702,7 @@ export class AgentStateStore {
601
702
  const messageId = value.message_id;
602
703
  if (typeof messageId !== "string" || !messageId)
603
704
  throw new Error("Inbox envelope requires message_id");
705
+ targetKeyOfInboxEnvelope(value);
604
706
  const file = this.file("inbox");
605
707
  return this.withInboxLock(file, () => {
606
708
  const ledger = this.readJson("runtimeDeliveries", { version: 1, records: [] });
@@ -649,6 +751,9 @@ export class AgentStateStore {
649
751
  * this transaction is active waits and becomes part of the next batch.
650
752
  */
651
753
  pollInbox(options = {}) {
754
+ if (options.target && !isCanonicalInboxTarget(options.target)) {
755
+ throw new Error(`Invalid canonical Inbox poll target ${JSON.stringify(options.target)}`);
756
+ }
652
757
  const file = this.file("inbox");
653
758
  return this.withInboxLock(file, () => {
654
759
  let rawRows;
@@ -667,7 +772,7 @@ export class AgentStateStore {
667
772
  const envelopes = [];
668
773
  const remaining = [];
669
774
  for (const row of rows) {
670
- const selectedTarget = typeof row.target === "string" ? row.target : targetKeyOfInboxEnvelope(row);
775
+ const selectedTarget = targetKeyOfInboxEnvelope(row);
671
776
  if (envelopes.length < limit && (!options.target || options.target === selectedTarget))
672
777
  envelopes.push(row);
673
778
  else
@@ -676,7 +781,7 @@ export class AgentStateStore {
676
781
  const pendingCount = remaining.reduce((count, row) => {
677
782
  if (!options.target)
678
783
  return count + 1;
679
- const rowTarget = typeof row.target === "string" ? row.target : targetKeyOfInboxEnvelope(row);
784
+ const rowTarget = targetKeyOfInboxEnvelope(row);
680
785
  return count + (rowTarget === options.target ? 1 : 0);
681
786
  }, 0);
682
787
  if (!envelopes.length)
@@ -710,7 +815,7 @@ export class AgentStateStore {
710
815
  }
711
816
  let seenThroughSeq = null;
712
817
  for (const envelope of envelopes) {
713
- const target = String(envelope.target || targetKeyOfInboxEnvelope(envelope));
818
+ const target = targetKeyOfInboxEnvelope(envelope);
714
819
  const targetSeq = Number(envelope.target_seq);
715
820
  const targetState = state.targets[target] ?? { latest_received_seq: targetSeq, model_seen_seq: 0 };
716
821
  if (validSequence(targetSeq)) {
@@ -731,11 +836,12 @@ export class AgentStateStore {
731
836
  resolveInboxMessageTarget(messageId) {
732
837
  return this.withInboxLock(this.file("inbox"), () => {
733
838
  const state = this.inboxState();
734
- const known = state.messages[messageId]?.target;
839
+ const known = state.messages[messageId];
735
840
  if (known)
736
- return known;
841
+ return targetKeyOfInboxEnvelope({ message_id: messageId, target: known.target,
842
+ ...(known.kind ? { kind: known.kind } : {}) });
737
843
  const row = this.readNdjson("inbox").find((candidate) => candidate.message_id === messageId);
738
- return row ? (typeof row.target === "string" ? row.target : targetKeyOfInboxEnvelope(row)) : null;
844
+ return row ? targetKeyOfInboxEnvelope(row) : null;
739
845
  });
740
846
  }
741
847
  listInboxDrafts() {
@@ -87,12 +87,14 @@ function createAgentTransport(env = process.env) {
87
87
  // —— legacy events/inbox characterization:从 host 写的 inbox 文件读入站正文(读后清空=已读)——
88
88
  // 必须严格对齐 agentApiEventsResponseSchema,否则 CLI 报 INVALID_JSON_RESPONSE,Claude 读不到消息。
89
89
  if (p.includes("/events") || p.includes("/inbox")) {
90
- let envelopes = [];
91
90
  try {
92
- envelopes = stateStore.pollInbox().envelopes;
91
+ const envelopes = stateStore.pollInbox().envelopes;
92
+ return { ok: true, status: 200, data: projectInboxEvents(envelopes) };
93
+ }
94
+ catch (error) {
95
+ return { ok: false, status: 409,
96
+ error: `Inbox consumption rejected; persisted rows were left unchanged: ${error instanceof Error ? error.message : String(error)}` };
93
97
  }
94
- catch { /* preserve legacy: malformed/unreadable inbox returns empty and is not cleared */ }
95
- return { ok: true, status: 200, data: projectInboxEvents(envelopes) };
96
98
  }
97
99
  // —— legacy history characterization:接飞书群历史 im +chat-messages-list ——
98
100
  // 需 bot 有 im:message:readonly;缺 scope 则优雅降级为空(不让 Claude 崩)。
@@ -80,12 +80,14 @@ export function createAgentTransport(env = process.env) {
80
80
  // —— legacy events/inbox characterization:从 host 写的 inbox 文件读入站正文(读后清空=已读)——
81
81
  // 必须严格对齐 agentApiEventsResponseSchema,否则 CLI 报 INVALID_JSON_RESPONSE,Claude 读不到消息。
82
82
  if (p.includes("/events") || p.includes("/inbox")) {
83
- let envelopes = [];
84
83
  try {
85
- envelopes = stateStore.pollInbox().envelopes;
84
+ const envelopes = stateStore.pollInbox().envelopes;
85
+ return { ok: true, status: 200, data: projectInboxEvents(envelopes) };
86
+ }
87
+ catch (error) {
88
+ return { ok: false, status: 409,
89
+ error: `Inbox consumption rejected; persisted rows were left unchanged: ${error instanceof Error ? error.message : String(error)}` };
86
90
  }
87
- catch { /* preserve legacy: malformed/unreadable inbox returns empty and is not cleared */ }
88
- return { ok: true, status: 200, data: projectInboxEvents(envelopes) };
89
91
  }
90
92
  // —— legacy history characterization:接飞书群历史 im +chat-messages-list ——
91
93
  // 需 bot 有 im:message:readonly;缺 scope 则优雅降级为空(不让 Claude 崩)。
@@ -37,6 +37,12 @@ exports.HostReminderOrchestrator = void 0;
37
37
  const fs = __importStar(require("node:fs"));
38
38
  const host_business_state_js_1 = require("../feishu/host-business-state.cjs");
39
39
  const defaultReminderStore = __importStar(require("./reminder-store.cjs"));
40
+ const inbox_projection_js_1 = require("./inbox-projection.cjs");
41
+ function withCanonicalTarget(envelope) {
42
+ const input = envelope;
43
+ const target = (0, inbox_projection_js_1.targetKeyOfInboxEnvelope)(input);
44
+ return input.target === target ? envelope : { ...envelope, target };
45
+ }
40
46
  class HostReminderOrchestrator {
41
47
  options;
42
48
  lastSnapshot = new Map();
@@ -99,14 +105,14 @@ class HostReminderOrchestrator {
99
105
  }
100
106
  deliver(agent, reminder, overdueMs) {
101
107
  const recurrence = reminder.repeat ? this.store.parseRepeat(reminder.repeat, reminder.tz) : null;
102
- const envelope = this.options.envelopeProjector.createReminderEnvelope(agent.agentId, {
108
+ const envelope = withCanonicalTarget(this.options.envelopeProjector.createReminderEnvelope(agent.agentId, {
103
109
  reminderId: reminder.reminderId,
104
110
  title: reminder.title,
105
111
  repeat: reminder.repeat ? String(reminder.repeat) : null,
106
112
  fireAt: reminder.fireAt,
107
113
  msgRef: reminder.msgRef,
108
114
  channel: reminder.channel ? String(reminder.channel) : null,
109
- }, overdueMs, reminder.repeat && recurrence && !("error" in recurrence) ? (recurrence.description || null) : null);
115
+ }, overdueMs, reminder.repeat && recurrence && !("error" in recurrence) ? (recurrence.description || null) : null));
110
116
  try {
111
117
  this.options.stateStore(agent).appendNdjson("inbox", envelope);
112
118
  }
@@ -268,7 +274,7 @@ class HostReminderOrchestrator {
268
274
  this.redelivered.add(agent.agentId);
269
275
  return;
270
276
  }
271
- const envelope = existing ?? this.options.envelopeProjector.createRedeliveryEnvelope(agent.agentId, wakeCount);
277
+ const envelope = withCanonicalTarget(existing ?? this.options.envelopeProjector.createRedeliveryEnvelope(agent.agentId, wakeCount));
272
278
  if (!existing) {
273
279
  try {
274
280
  this.options.stateStore(agent).appendNdjson("inbox", envelope);
@@ -1,6 +1,12 @@
1
1
  import * as fs from "node:fs";
2
2
  import { countWakeEnvelopes } from "../feishu/host-business-state.mjs";
3
3
  import * as defaultReminderStore from "./reminder-store.mjs";
4
+ import { targetKeyOfInboxEnvelope } from "./inbox-projection.mjs";
5
+ function withCanonicalTarget(envelope) {
6
+ const input = envelope;
7
+ const target = targetKeyOfInboxEnvelope(input);
8
+ return input.target === target ? envelope : { ...envelope, target };
9
+ }
4
10
  export class HostReminderOrchestrator {
5
11
  options;
6
12
  lastSnapshot = new Map();
@@ -63,14 +69,14 @@ export class HostReminderOrchestrator {
63
69
  }
64
70
  deliver(agent, reminder, overdueMs) {
65
71
  const recurrence = reminder.repeat ? this.store.parseRepeat(reminder.repeat, reminder.tz) : null;
66
- const envelope = this.options.envelopeProjector.createReminderEnvelope(agent.agentId, {
72
+ const envelope = withCanonicalTarget(this.options.envelopeProjector.createReminderEnvelope(agent.agentId, {
67
73
  reminderId: reminder.reminderId,
68
74
  title: reminder.title,
69
75
  repeat: reminder.repeat ? String(reminder.repeat) : null,
70
76
  fireAt: reminder.fireAt,
71
77
  msgRef: reminder.msgRef,
72
78
  channel: reminder.channel ? String(reminder.channel) : null,
73
- }, overdueMs, reminder.repeat && recurrence && !("error" in recurrence) ? (recurrence.description || null) : null);
79
+ }, overdueMs, reminder.repeat && recurrence && !("error" in recurrence) ? (recurrence.description || null) : null));
74
80
  try {
75
81
  this.options.stateStore(agent).appendNdjson("inbox", envelope);
76
82
  }
@@ -232,7 +238,7 @@ export class HostReminderOrchestrator {
232
238
  this.redelivered.add(agent.agentId);
233
239
  return;
234
240
  }
235
- const envelope = existing ?? this.options.envelopeProjector.createRedeliveryEnvelope(agent.agentId, wakeCount);
241
+ const envelope = withCanonicalTarget(existing ?? this.options.envelopeProjector.createRedeliveryEnvelope(agent.agentId, wakeCount));
236
242
  if (!existing) {
237
243
  try {
238
244
  this.options.stateStore(agent).appendNdjson("inbox", envelope);