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.
- package/dist/agent/agent-state-store.cjs +127 -21
- package/dist/agent/agent-state-store.mjs +128 -22
- package/dist/agent/agent-transport.cjs +6 -4
- package/dist/agent/agent-transport.mjs +6 -4
- package/dist/agent/host-reminder-orchestrator.cjs +9 -3
- package/dist/agent/host-reminder-orchestrator.mjs +9 -3
- package/dist/agent/inbox-projection.cjs +114 -23
- package/dist/agent/inbox-projection.mjs +112 -23
- package/dist/feishu/document-comment.cjs +2 -1
- package/dist/feishu/document-comment.mjs +2 -1
- package/dist/feishu/host-business-state.cjs +7 -2
- package/dist/feishu/host-business-state.mjs +7 -2
- package/dist/feishu/host-shell.cjs +66 -31
- package/dist/feishu/host-shell.mjs +66 -31
- package/dist/runtime/runtime-host.cjs +173 -10
- package/dist/runtime/runtime-host.mjs +173 -10
- package/package.json +1 -1
|
@@ -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] = {
|
|
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 =
|
|
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
|
-
|
|
479
|
-
|
|
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
|
-
/**
|
|
614
|
-
|
|
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
|
|
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
|
-
|
|
623
|
-
|
|
624
|
-
if (
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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]
|
|
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 ? (
|
|
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] = {
|
|
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 =
|
|
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
|
-
|
|
442
|
-
|
|
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
|
-
/**
|
|
577
|
-
|
|
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
|
|
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
|
-
|
|
586
|
-
|
|
587
|
-
if (
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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]
|
|
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 ?
|
|
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);
|