larkin 0.4.1 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +76 -33
  2. package/artifacts/release/THIRD_PARTY_NOTICES.txt +218 -228
  3. package/assets/readme/larkin-github-cover.png +0 -0
  4. package/assets/readme/larkin-hero.svg +111 -0
  5. package/dist/agent/agent-state-store.cjs +193 -1
  6. package/dist/agent/agent-state-store.mjs +191 -0
  7. package/dist/app/agent-config.mjs +73 -6
  8. package/dist/app/agent-readiness.cjs +22 -2
  9. package/dist/app/agent-readiness.mjs +20 -2
  10. package/dist/app/binary-entry.mjs +6 -0
  11. package/dist/app/cli.mjs +7 -1
  12. package/dist/app/local-control.mjs +108 -2
  13. package/dist/app/run.mjs +8 -0
  14. package/dist/app/runtime-process.mjs +17 -1
  15. package/dist/app/session-cli.mjs +55 -20
  16. package/dist/dashboard/dashboard-view-model.mjs +12 -3
  17. package/dist/feishu/host-shell.cjs +104 -9
  18. package/dist/feishu/host-shell.mjs +105 -10
  19. package/dist/platform/config.cjs +406 -33
  20. package/dist/platform/config.mjs +405 -33
  21. package/dist/platform/process-boundary-trace.cjs +108 -0
  22. package/dist/platform/process-boundary-trace.mjs +102 -0
  23. package/dist/runtime/pi-artifact-provenance.cjs +147 -0
  24. package/dist/runtime/pi-artifact-provenance.mjs +139 -0
  25. package/dist/runtime/pi-compaction-recovery.cjs +703 -0
  26. package/dist/runtime/pi-compaction-recovery.mjs +679 -0
  27. package/dist/runtime/pi-model-catalog.cjs +2 -2
  28. package/dist/runtime/pi-model-catalog.mjs +2 -2
  29. package/dist/runtime/pi-profile-migration.cjs +556 -0
  30. package/dist/runtime/pi-profile-migration.mjs +515 -0
  31. package/dist/runtime/pi-provider-config.cjs +3 -3
  32. package/dist/runtime/pi-provider-config.mjs +3 -3
  33. package/dist/runtime/pi-rpc-client.cjs +39 -22
  34. package/dist/runtime/pi-rpc-client.mjs +39 -22
  35. package/dist/runtime/provider-error-classifier.cjs +39 -0
  36. package/dist/runtime/provider-error-classifier.mjs +34 -0
  37. package/dist/runtime/runtime-adapters.mjs +176 -27
  38. package/dist/runtime/runtime-host.cjs +630 -19
  39. package/dist/runtime/runtime-host.mjs +628 -18
  40. package/dist/runtime/runtime-readiness.cjs +6 -4
  41. package/dist/runtime/runtime-readiness.mjs +6 -4
  42. package/package.json +3 -3
@@ -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
  ]);
@@ -197,6 +198,15 @@ function validInboxLockOwner(value) {
197
198
  && typeof owner.processStartToken === "string" && !!owner.processStartToken
198
199
  && typeof owner.nonce === "string" && /^[0-9a-f-]{36}$/i.test(owner.nonce);
199
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;
200
210
  class AgentStateStore {
201
211
  paths;
202
212
  boundary;
@@ -728,6 +738,188 @@ class AgentStateStore {
728
738
  return { status: "missing", code: "canonical_inbox_row_missing" };
729
739
  });
730
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
+ });
853
+ const committedLedger = { ...ledger, records };
854
+ let restored = false;
855
+ const restoreUnlocked = () => {
856
+ if (restored)
857
+ return;
858
+ const current = this.readJson("runtimeDeliveries", { version: 1, records: [] });
859
+ if (!Array.isArray(current.records))
860
+ throw new Error("Runtime delivery rollback found a malformed ledger");
861
+ const originalMatching = ledgerRecords.filter((candidate) => {
862
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
863
+ return false;
864
+ const record = candidate;
865
+ return typeof record.messageId === "string" && messageIds.has(record.messageId);
866
+ });
867
+ const originalDeliveryIds = new Set(originalMatching.flatMap((candidate) => {
868
+ const deliveryId = candidate.deliveryId;
869
+ return typeof deliveryId === "string" ? [deliveryId] : [];
870
+ }));
871
+ const currentMatching = current.records.filter((candidate) => {
872
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
873
+ return false;
874
+ const record = candidate;
875
+ return (typeof record.deliveryId === "string" && originalDeliveryIds.has(record.deliveryId))
876
+ || (typeof record.messageId === "string" && messageIds.has(record.messageId));
877
+ });
878
+ const matchingRestored = currentMatching.length === originalMatching.length
879
+ && currentMatching.every((candidate, index) => JSON.stringify(candidate) === JSON.stringify(originalMatching[index]));
880
+ if (!matchingRestored) {
881
+ const unrelated = current.records.filter((candidate) => {
882
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
883
+ return true;
884
+ const record = candidate;
885
+ return !((typeof record.deliveryId === "string" && originalDeliveryIds.has(record.deliveryId))
886
+ || (typeof record.messageId === "string" && messageIds.has(record.messageId)));
887
+ });
888
+ const restoredRecords = [...unrelated];
889
+ for (const candidate of originalMatching) {
890
+ const originalIndex = ledgerRecords.indexOf(candidate);
891
+ restoredRecords.splice(Math.min(originalIndex, restoredRecords.length), 0, candidate);
892
+ }
893
+ this.writeJson("runtimeDeliveries", { ...current, records: restoredRecords });
894
+ }
895
+ restored = true;
896
+ };
897
+ const rollback = () => {
898
+ if (lockActive)
899
+ restoreUnlocked();
900
+ else
901
+ this.withInboxTransaction(restoreUnlocked);
902
+ };
903
+ try {
904
+ this.writeJson("runtimeDeliveries", committedLedger);
905
+ onCommit?.([...messageIds], rollback);
906
+ }
907
+ catch (error) {
908
+ try {
909
+ rollback();
910
+ }
911
+ catch {
912
+ throw new Error("context-window recovery ledger rollback failed");
913
+ }
914
+ throw error;
915
+ }
916
+ return { rearmedCount: messageIds.size, remainingPendingCount: rows.length };
917
+ });
918
+ }
919
+ finally {
920
+ lockActive = false;
921
+ }
922
+ }
731
923
  /**
732
924
  * Recoverably bridge an outbox wake into the canonical Inbox. A durable
733
925
  * Runtime ledger is authoritative after the Inbox has been drained, so a
@@ -5,6 +5,7 @@ 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
7
  import { isCanonicalInboxTarget, targetKeyOfInboxEnvelope } from "./inbox-projection.mjs";
8
+ import { buildStrictProviderErrorInput, classifyStrictProviderError } from "../runtime/provider-error-classifier.mjs";
8
9
  const JSON_KEYS = new Set([
9
10
  "agentState", "status", "map", "replyctx", "botIdentity", "senderProfiles", "readReceipts", "pendingReact", "runtimeDeliveries", "inboxState", "freshnessState", "documentComments", "reminders", "interactions",
10
11
  ]);
@@ -160,6 +161,14 @@ function validInboxLockOwner(value) {
160
161
  && typeof owner.processStartToken === "string" && !!owner.processStartToken
161
162
  && typeof owner.nonce === "string" && /^[0-9a-f-]{36}$/i.test(owner.nonce);
162
163
  }
164
+ export class ContextOverflowRecoveryError extends Error {
165
+ code;
166
+ constructor(code, message) {
167
+ super(message);
168
+ this.code = code;
169
+ this.name = "ContextOverflowRecoveryError";
170
+ }
171
+ }
163
172
  export class AgentStateStore {
164
173
  paths;
165
174
  boundary;
@@ -691,6 +700,188 @@ export class AgentStateStore {
691
700
  return { status: "missing", code: "canonical_inbox_row_missing" };
692
701
  });
693
702
  }
703
+ /**
704
+ * Re-arm only the durable deliveries proven to be the context-window incident.
705
+ * The Inbox bytes are intentionally untouched. An optional synchronous commit
706
+ * hook lets RuntimeHost swap its in-memory generation while this same lock is
707
+ * still held. If the hook fails, the ledger is restored before the lock is
708
+ * released. The callback also receives a guarded rollback for post-callback
709
+ * commit failures before retry scheduling begins.
710
+ */
711
+ rearmContextOverflow(onCommit, expected) {
712
+ let lockActive = true;
713
+ try {
714
+ return this.withInboxTransaction(() => {
715
+ let rows;
716
+ try {
717
+ rows = this.readNdjson("inbox");
718
+ }
719
+ catch {
720
+ throw new ContextOverflowRecoveryError("canonical_inbox_malformed", "canonical Inbox is malformed");
721
+ }
722
+ if (rows.length === 0)
723
+ throw new ContextOverflowRecoveryError("inbox_empty", "canonical Inbox backlog is empty");
724
+ const messageIds = new Set();
725
+ for (const row of rows) {
726
+ try {
727
+ targetKeyOfInboxEnvelope(row);
728
+ }
729
+ catch {
730
+ throw new ContextOverflowRecoveryError("canonical_inbox_malformed", "canonical Inbox contains a malformed row");
731
+ }
732
+ if (typeof row.message_id !== "string" || !row.message_id) {
733
+ throw new ContextOverflowRecoveryError("canonical_inbox_malformed", "canonical Inbox row has no message identity");
734
+ }
735
+ if (messageIds.has(row.message_id))
736
+ throw new ContextOverflowRecoveryError("duplicate_message_id", "canonical Inbox contains duplicate message identities");
737
+ messageIds.add(row.message_id);
738
+ }
739
+ const ledger = this.readJson("runtimeDeliveries", { version: 1, records: [] });
740
+ if (!Array.isArray(ledger.records))
741
+ throw new ContextOverflowRecoveryError("canonical_inbox_malformed", "Runtime delivery ledger is malformed");
742
+ const ledgerRecords = ledger.records;
743
+ const matches = new Map();
744
+ const deliveryIds = new Set();
745
+ const inputIds = new Set();
746
+ for (const candidate of ledger.records) {
747
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
748
+ continue;
749
+ const record = candidate;
750
+ if (typeof record.messageId !== "string" || !messageIds.has(record.messageId))
751
+ continue;
752
+ if (typeof record.deliveryId !== "string" || !record.deliveryId)
753
+ throw new ContextOverflowRecoveryError("delivery_missing", "a matching Runtime delivery record has no delivery identity");
754
+ if (!record.input || typeof record.input !== "object" || Array.isArray(record.input)) {
755
+ throw new ContextOverflowRecoveryError("delivery_identity_invalid", "a matching Runtime delivery record has no Runtime input identity");
756
+ }
757
+ const input = record.input;
758
+ if (typeof input.inputId !== "string" || !input.inputId.trim()
759
+ || typeof input.deliveryId !== "string" || input.deliveryId !== record.deliveryId) {
760
+ throw new ContextOverflowRecoveryError("delivery_identity_invalid", "a matching Runtime delivery record has malformed input identity");
761
+ }
762
+ if (matches.has(record.messageId))
763
+ throw new ContextOverflowRecoveryError("delivery_duplicate", "a canonical Inbox row maps to multiple delivery records");
764
+ if (deliveryIds.has(record.deliveryId))
765
+ throw new ContextOverflowRecoveryError("delivery_duplicate", "matching Runtime delivery records share a delivery identity");
766
+ if (inputIds.has(input.inputId))
767
+ throw new ContextOverflowRecoveryError("duplicate_input_id", "matching Runtime delivery records share an input identity");
768
+ deliveryIds.add(record.deliveryId);
769
+ inputIds.add(input.inputId);
770
+ matches.set(record.messageId, record);
771
+ }
772
+ if (expected?.messageId && !messageIds.has(expected.messageId)) {
773
+ throw new ContextOverflowRecoveryError("delivery_missing", "the expected context-overflow message is not in canonical Inbox");
774
+ }
775
+ if (expected?.messageId) {
776
+ const expectedRecord = matches.get(expected.messageId);
777
+ if (!expectedRecord || (expected.deliveryId && expectedRecord.deliveryId !== expected.deliveryId)
778
+ || (expected.inputId && expectedRecord.input.inputId !== expected.inputId)) {
779
+ throw new ContextOverflowRecoveryError("delivery_identity_invalid", "the expected context-overflow delivery identity changed");
780
+ }
781
+ }
782
+ for (const messageId of messageIds) {
783
+ const record = matches.get(messageId);
784
+ if (!record)
785
+ throw new ContextOverflowRecoveryError("delivery_missing", "a canonical Inbox row has no Runtime delivery record");
786
+ if (record.status !== "error" || record.retryable === true) {
787
+ throw new ContextOverflowRecoveryError("delivery_not_terminal", "a canonical Inbox row is not backed by a non-retryable terminal Runtime error");
788
+ }
789
+ const reasonCategory = classifyStrictProviderError(buildStrictProviderErrorInput({
790
+ reason: record.reason, errorCategory: record.errorCategory,
791
+ }));
792
+ if (record.retryable !== false || reasonCategory !== "context_window"
793
+ || (record.errorCategory !== undefined && record.errorCategory !== "context_window")) {
794
+ throw new ContextOverflowRecoveryError("delivery_not_context_window", "a Runtime delivery error is not classified as context_window");
795
+ }
796
+ }
797
+ for (const candidate of ledgerRecords) {
798
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
799
+ continue;
800
+ const record = candidate;
801
+ if (typeof record.deliveryId === "string" && deliveryIds.has(record.deliveryId)
802
+ && (typeof record.messageId !== "string" || !messageIds.has(record.messageId))) {
803
+ throw new ContextOverflowRecoveryError("delivery_duplicate", "a matching delivery identity is also used by an unrelated Runtime delivery record");
804
+ }
805
+ }
806
+ const updatedAt = new Date().toISOString();
807
+ const records = ledgerRecords.map((candidate) => {
808
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
809
+ return candidate;
810
+ const record = candidate;
811
+ if (typeof record.messageId !== "string" || !messageIds.has(record.messageId))
812
+ return candidate;
813
+ return { ...record, status: "pending", retryable: true, updatedAt };
814
+ });
815
+ const committedLedger = { ...ledger, records };
816
+ let restored = false;
817
+ const restoreUnlocked = () => {
818
+ if (restored)
819
+ return;
820
+ const current = this.readJson("runtimeDeliveries", { version: 1, records: [] });
821
+ if (!Array.isArray(current.records))
822
+ throw new Error("Runtime delivery rollback found a malformed ledger");
823
+ const originalMatching = ledgerRecords.filter((candidate) => {
824
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
825
+ return false;
826
+ const record = candidate;
827
+ return typeof record.messageId === "string" && messageIds.has(record.messageId);
828
+ });
829
+ const originalDeliveryIds = new Set(originalMatching.flatMap((candidate) => {
830
+ const deliveryId = candidate.deliveryId;
831
+ return typeof deliveryId === "string" ? [deliveryId] : [];
832
+ }));
833
+ const currentMatching = current.records.filter((candidate) => {
834
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
835
+ return false;
836
+ const record = candidate;
837
+ return (typeof record.deliveryId === "string" && originalDeliveryIds.has(record.deliveryId))
838
+ || (typeof record.messageId === "string" && messageIds.has(record.messageId));
839
+ });
840
+ const matchingRestored = currentMatching.length === originalMatching.length
841
+ && currentMatching.every((candidate, index) => JSON.stringify(candidate) === JSON.stringify(originalMatching[index]));
842
+ if (!matchingRestored) {
843
+ const unrelated = current.records.filter((candidate) => {
844
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
845
+ return true;
846
+ const record = candidate;
847
+ return !((typeof record.deliveryId === "string" && originalDeliveryIds.has(record.deliveryId))
848
+ || (typeof record.messageId === "string" && messageIds.has(record.messageId)));
849
+ });
850
+ const restoredRecords = [...unrelated];
851
+ for (const candidate of originalMatching) {
852
+ const originalIndex = ledgerRecords.indexOf(candidate);
853
+ restoredRecords.splice(Math.min(originalIndex, restoredRecords.length), 0, candidate);
854
+ }
855
+ this.writeJson("runtimeDeliveries", { ...current, records: restoredRecords });
856
+ }
857
+ restored = true;
858
+ };
859
+ const rollback = () => {
860
+ if (lockActive)
861
+ restoreUnlocked();
862
+ else
863
+ this.withInboxTransaction(restoreUnlocked);
864
+ };
865
+ try {
866
+ this.writeJson("runtimeDeliveries", committedLedger);
867
+ onCommit?.([...messageIds], rollback);
868
+ }
869
+ catch (error) {
870
+ try {
871
+ rollback();
872
+ }
873
+ catch {
874
+ throw new Error("context-window recovery ledger rollback failed");
875
+ }
876
+ throw error;
877
+ }
878
+ return { rearmedCount: messageIds.size, remainingPendingCount: rows.length };
879
+ });
880
+ }
881
+ finally {
882
+ lockActive = false;
883
+ }
884
+ }
694
885
  /**
695
886
  * Recoverably bridge an outbox wake into the canonical Inbox. A durable
696
887
  * Runtime ledger is authoritative after the Inbox has been drained, so a
@@ -11,6 +11,8 @@ import { isChannelReconnecting, projectAgentReadiness } from "./agent-readiness.
11
11
  import { requestAgentUpsert } from "./local-control.mjs";
12
12
  import * as larkinConfig from "../platform/config.mjs";
13
13
  import { managedOfficialLarkCli } from "./agent-lark-cli-workspace.mjs";
14
+ import { assertBuiltinPiAgentDirectory, piAgentDirectory } from "../runtime/pi-provider-config.mjs";
15
+ import { traceProcessBoundary } from "../platform/process-boundary-trace.mjs";
14
16
  function safeDocumentCommentCapabilities(configDir, profile) {
15
17
  try {
16
18
  return {
@@ -49,15 +51,15 @@ const die = (message) => {
49
51
  console.error(" 下一步: larkin config --help");
50
52
  process.exit(1);
51
53
  };
52
- if (!kind || !["agents", "model", "runtime", "effort", "chats", "config"].includes(kind)) {
53
- die("用法: larkin agents | larkin config <show|mention|apply> | larkin model [<id>] | larkin runtime [<id>] [--model <id>] | larkin effort [<level>|clear] | larkin chats [free|strict <oc_id>]");
54
+ if (!kind || !["agents", "model", "runtime", "effort", "pi-distribution", "chats", "config"].includes(kind)) {
55
+ die("用法: larkin agents | larkin config <show|mention|apply> | larkin model [<id>] | larkin runtime [<id>] [--model <id>] | larkin pi-distribution [show|builtin|external] | larkin effort [<level>|clear] | larkin chats [free|strict <oc_id>]");
54
56
  }
55
57
  const allowedValueFlags = {
56
58
  agents: new Set(), model: new Set(["--agent"]), runtime: new Set(["--agent", "--model"]),
57
- effort: new Set(["--agent"]), chats: new Set(["--agent"]), config: new Set(["--agent", "--chat"]),
59
+ effort: new Set(["--agent"]), "pi-distribution": new Set(["--agent", "--snapshot"]), chats: new Set(["--agent"]), config: new Set(["--agent", "--chat"]),
58
60
  };
59
61
  const allowedBooleanFlags = {
60
- agents: new Set(["--json"]), model: new Set(), runtime: new Set(), effort: new Set(), chats: new Set(), config: new Set(["--json"]),
62
+ agents: new Set(["--json"]), model: new Set(), runtime: new Set(), effort: new Set(), "pi-distribution": new Set(["--import-external-profile"]), chats: new Set(), config: new Set(["--json"]),
61
63
  };
62
64
  const parsedValues = new Map();
63
65
  const parsedBooleans = new Set();
@@ -91,6 +93,7 @@ const flagAgent = parsedValues.get("--agent");
91
93
  const flagModel = parsedValues.get("--model");
92
94
  const flagChat = parsedValues.get("--chat");
93
95
  const flagJson = parsedBooleans.has("--json");
96
+ const importExternalProfile = parsedBooleans.has("--import-external-profile");
94
97
  const value = positionals[0];
95
98
  function assertOnlyFlags(valueFlags, booleanFlags = []) {
96
99
  const allowedValues = new Set(valueFlags);
@@ -122,6 +125,11 @@ else if (kind === "effort") {
122
125
  if (positionals.length > 1)
123
126
  die("用法: larkin effort [<level>|clear|default] [--agent <App ID>]");
124
127
  }
128
+ else if (kind === "pi-distribution") {
129
+ assertOnlyFlags(["--agent", "--snapshot"], ["--import-external-profile"]);
130
+ if (positionals.length > 1)
131
+ die("用法: larkin pi-distribution [show|builtin|external] [--agent <App ID>] [--snapshot <private-file>]");
132
+ }
125
133
  else if (kind === "chats") {
126
134
  assertOnlyFlags(["--agent"]);
127
135
  if (![0, 2].includes(positionals.length))
@@ -169,6 +177,21 @@ const loaded = (() => {
169
177
  const { configDir, file, config } = loaded;
170
178
  if (!fs.existsSync(file))
171
179
  die("未找到 Larkin 配置,请运行 larkin setup");
180
+ const snapshotFile = parsedValues.get("--snapshot");
181
+ if (kind === "pi-distribution" && value === "rollback") {
182
+ if (importExternalProfile)
183
+ die("--import-external-profile 只允许与 builtin 一起使用");
184
+ if (!snapshotFile || flagAgent || positionals.length !== 1)
185
+ die("用法: larkin pi-distribution rollback --snapshot <private-file>");
186
+ try {
187
+ const result = larkinConfig.rollbackConfig(process.env, snapshotFile);
188
+ say(JSON.stringify({ ok: true, rolledBackAgent: result.agentId, revision: result.revision }));
189
+ }
190
+ catch (error) {
191
+ die(`回滚失败:${error instanceof Error ? error.message : String(error)}`);
192
+ }
193
+ process.exit(0);
194
+ }
172
195
  if (kind === "agents") {
173
196
  const list = Object.values(config.agents || {});
174
197
  const daemon = readProcessState(config.larkinHome).daemon;
@@ -266,12 +289,19 @@ if (kind === "agents") {
266
289
  && !!status?.inboundVerifiedAt
267
290
  && Date.parse(status.inboundVerifiedAt) >= Date.parse(String(daemon.startedAt || 0));
268
291
  const commentDiagnostic = projectDocumentCommentDiagnostic(commentCapability, status);
292
+ const projectedReadiness = projectAgentReadiness({ agentId: agent.agentId, daemon, status });
269
293
  say(` ${agent.name}${agent.name === config.activeAgent ? " [active]" : ""}`);
270
294
  const effectiveModel = status?.session?.runtime === agent.runtime && status.session.model ? status.session.model : agent.model;
271
295
  const effectiveEffort = status?.session?.runtime === agent.runtime && status.session.reasoningEffort ? status.session.reasoningEffort : agent.effort;
272
296
  say(` runtime=${agent.runtime} model=${effectiveModel}${effectiveModel !== agent.model ? ` stored=${agent.model}` : ""}${effectiveEffort ? ` effort=${effectiveEffort}` : ""}`);
273
- if (status?.runtimeReadiness)
274
- say(` runtime readiness=${status.runtimeReadiness.state || "incompatible"}${status.runtimeReadiness.reason ? `:${status.runtimeReadiness.reason}` : ""}${status.runtimeReadiness.nextAction ? `;下一步:${status.runtimeReadiness.nextAction}` : ""}`);
297
+ if (status?.runtimeReadiness) {
298
+ const current = projectedReadiness.readiness.runtime_ready;
299
+ const state = status.runtimeReadiness.state === "ready" && !current ? "unavailable" : status.runtimeReadiness.state || "incompatible";
300
+ const staleReason = status.runtimeReadiness.state === "ready" && !current
301
+ ? ":ready 证据不属于当前 owned daemon epoch 或当前 session,已拒绝展示为 ready"
302
+ : status.runtimeReadiness.reason ? `:${status.runtimeReadiness.reason}` : "";
303
+ say(` runtime readiness=${state}${staleReason}${status.runtimeReadiness.nextAction ? `;下一步:${status.runtimeReadiness.nextAction}` : ""}`);
304
+ }
275
305
  if (status?.runtimeReadiness?.executable)
276
306
  say(` runtime executable=${status.runtimeReadiness.executable}${status.runtimeReadiness.version ? ` version=${status.runtimeReadiness.version}` : ""}`);
277
307
  say(` bot=${bot ? `${bot.name}(${bot.open_id})` : "(未连接过,无身份缓存)"}`);
@@ -405,6 +435,43 @@ if (!key) {
405
435
  }
406
436
  const selectedKey = key;
407
437
  const agent = config.agents[selectedKey];
438
+ if (kind === "pi-distribution") {
439
+ traceProcessBoundary(process.env, "agent-config:pi-distribution-loaded", { configDir, agentId: selectedKey, targetDir: piAgentDirectory(configDir, selectedKey), requested: value || "show", importExternalProfile });
440
+ const requested = value || "show";
441
+ if (importExternalProfile && requested !== "builtin")
442
+ die("--import-external-profile 只允许与 builtin 一起使用");
443
+ if (requested === "show") {
444
+ say(JSON.stringify({ agentId: selectedKey, runtime: agent.runtime, piDistribution: agent.piDistribution ?? "external" }));
445
+ process.exit(0);
446
+ }
447
+ if (requested !== "builtin" && requested !== "external")
448
+ die("Pi distribution 只允许 show/builtin/external");
449
+ if (agent.runtime !== "pi")
450
+ die(`Agent ${selectedKey} 不是 Pi runtime`);
451
+ if (!snapshotFile)
452
+ die("修改 Pi distribution 必须提供 --snapshot <private-file>");
453
+ if (requested === "builtin" && !importExternalProfile) {
454
+ try {
455
+ assertBuiltinPiAgentDirectory(piAgentDirectory(configDir, selectedKey));
456
+ }
457
+ catch {
458
+ die("内置 Pi provider 尚未为该 Agent 配置;请先运行 larkin setup 完成内置 Pi provider 配置,或使用 --import-external-profile");
459
+ }
460
+ }
461
+ try {
462
+ const result = larkinConfig.mutateConfig(process.env, { kind: "set-agent-pi-distribution", agentId: selectedKey, distribution: requested }, { kind: "user" }, {
463
+ snapshotFile: snapshotFile, ...(importExternalProfile ? { importExternalProfile: true } : {}),
464
+ });
465
+ traceProcessBoundary(process.env, "agent-config:pi-distribution-persisted", { configDir, agentId: selectedKey, targetDir: piAgentDirectory(configDir, selectedKey), requested, applyState: result.applyState });
466
+ say(JSON.stringify({ ok: true, agentId: selectedKey, piDistribution: requested, revision: result.revision, applyState: result.applyState }));
467
+ }
468
+ catch (error) {
469
+ if (importExternalProfile)
470
+ die("Pi external profile import failed; no secret or private path was disclosed");
471
+ die(`Pi distribution 修改失败:${error instanceof Error ? error.message : String(error)}`);
472
+ }
473
+ process.exit(0);
474
+ }
408
475
  const catalog = larkinConfig.loadRuntimeModels();
409
476
  if ((["model", "effort"].includes(kind || "") && agent.runtime === "codex") || (kind === "runtime" && value === "codex")) {
410
477
  try {
@@ -1,5 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isRuntimeReadinessCurrent = isRuntimeReadinessCurrent;
4
+ exports.isCurrentOwnedDaemon = isCurrentOwnedDaemon;
3
5
  exports.isChannelReconnecting = isChannelReconnecting;
4
6
  exports.projectAgentReadiness = projectAgentReadiness;
5
7
  const process_state_js_1 = require("../platform/process-state.cjs");
@@ -7,6 +9,19 @@ function timestamp(value) {
7
9
  const parsed = Date.parse(String(value || ""));
8
10
  return Number.isFinite(parsed) ? parsed : null;
9
11
  }
12
+ function isRuntimeReadinessCurrent(readiness, daemonStartedAt) {
13
+ if (readiness?.state !== "ready")
14
+ return false;
15
+ const observedAt = timestamp(readiness.observedAt);
16
+ const epoch = timestamp(daemonStartedAt);
17
+ return observedAt !== null && epoch !== null && observedAt >= epoch;
18
+ }
19
+ function isCurrentOwnedDaemon(daemon) {
20
+ return daemon?.state === "owned"
21
+ && Number(daemon.pid) > 0
22
+ && typeof daemon.processStartToken === "string" && daemon.processStartToken.length > 0
23
+ && timestamp(daemon.startedAt) !== null;
24
+ }
10
25
  function isChannelReconnecting(status) {
11
26
  const reconnectingAt = timestamp(status?.reconnectingAt);
12
27
  const connectedAt = timestamp(status?.connectedAt);
@@ -16,9 +31,14 @@ function isChannelReconnecting(status) {
16
31
  }
17
32
  function projectAgentReadiness(input) {
18
33
  const status = input.status ?? null;
19
- const daemonOwned = (0, process_state_js_1.daemonHasAgent)(input.daemon, input.agentId);
34
+ const daemonOwned = isCurrentOwnedDaemon(input.daemon) && (0, process_state_js_1.daemonHasAgent)(input.daemon, input.agentId);
20
35
  const daemonStartedAt = timestamp(input.daemon.startedAt);
21
36
  const connectedAt = timestamp(status?.connectedAt);
37
+ // A resumed session deliberately retains its original creation time. Its
38
+ // current daemon-epoch proof is the fresh session observation, not that
39
+ // historic creation timestamp.
40
+ const sessionObservedAt = timestamp(status?.session?.lastSeenAt) ?? timestamp(status?.session?.startedAt);
41
+ const sessionCurrent = daemonStartedAt !== null && sessionObservedAt !== null && sessionObservedAt >= daemonStartedAt;
22
42
  const channelConnected = daemonOwned
23
43
  && status?.connectedVia === "channel"
24
44
  && daemonStartedAt !== null
@@ -26,7 +46,7 @@ function projectAgentReadiness(input) {
26
46
  && connectedAt >= daemonStartedAt;
27
47
  const readiness = {
28
48
  daemon_owned: daemonOwned,
29
- runtime_ready: status?.runtimeReadiness?.state === "ready",
49
+ runtime_ready: daemonOwned && sessionCurrent && isRuntimeReadinessCurrent(status?.runtimeReadiness, input.daemon.startedAt),
30
50
  channel_connected: channelConnected,
31
51
  channel_not_reconnecting: !isChannelReconnecting(status),
32
52
  };