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.
- package/README.md +72 -33
- package/assets/readme/larkin-hero.svg +111 -0
- package/dist/agent/agent-state-store.cjs +321 -22
- package/dist/agent/agent-state-store.mjs +320 -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/app/cli.mjs +3 -1
- package/dist/app/local-control.mjs +108 -2
- package/dist/app/runtime-process.mjs +4 -0
- package/dist/app/session-cli.mjs +55 -20
- 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 +138 -32
- package/dist/feishu/host-shell.mjs +138 -32
- package/dist/runtime/pi-compaction-recovery.cjs +623 -0
- package/dist/runtime/pi-compaction-recovery.mjs +602 -0
- package/dist/runtime/pi-provider-config.cjs +2 -2
- package/dist/runtime/pi-provider-config.mjs +2 -2
- package/dist/runtime/pi-rpc-client.cjs +39 -22
- package/dist/runtime/pi-rpc-client.mjs +39 -22
- package/dist/runtime/provider-error-classifier.cjs +39 -0
- package/dist/runtime/provider-error-classifier.mjs +34 -0
- package/dist/runtime/runtime-adapters.mjs +82 -19
- package/dist/runtime/runtime-host.cjs +663 -12
- package/dist/runtime/runtime-host.mjs +661 -11
- 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] = {
|
|
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 =
|
|
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
|
-
|
|
479
|
-
|
|
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
|
-
/**
|
|
614
|
-
|
|
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
|
|
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
|
-
|
|
623
|
-
|
|
624
|
-
if (
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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]
|
|
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 ? (
|
|
1074
|
+
return row ? (0, inbox_projection_js_1.targetKeyOfInboxEnvelope)(row) : null;
|
|
776
1075
|
});
|
|
777
1076
|
}
|
|
778
1077
|
listInboxDrafts() {
|