






























1+// Persists runtime tool-schema quarantines in the shared SQLite-backed core
2+// plugin-state store so health surfaces can see failures from any live
3+// runtime process.
4+import {
5+createRuntimeHealthRecordEnvelope,
6+createRuntimeHealthStore,
7+type RuntimeHealthRecordEnvelope,
8+} from "../plugin-state/runtime-health-store.js";
9+10+export type RuntimeToolSchemaQuarantine = {
11+toolName: string;
12+owner?: string;
13+reason: string;
14+failedAt: Date;
15+};
16+17+type PersistedRuntimeToolSchemaQuarantineRecord = RuntimeHealthRecordEnvelope & {
18+toolName: string;
19+owner?: string;
20+reason: string;
21+};
22+23+function isNonEmptyString(value: unknown): value is string {
24+return typeof value === "string" && value.trim().length > 0;
25+}
26+27+const quarantineStore = createRuntimeHealthStore<PersistedRuntimeToolSchemaQuarantineRecord>({
28+ownerId: "core:runtime-tool-quarantine-health",
29+namespace: "schema-quarantines",
30+maxEntries: 128,
31+// Failing runs re-register their quarantine and refresh this TTL, so it only
32+// expires records that stop recurring (e.g. a schema fixed without restart).
33+ttlMs: 24 * 60 * 60 * 1_000,
34+normalizeRecord: (value) => {
35+if (!isNonEmptyString(value.toolName) || !isNonEmptyString(value.reason)) {
36+return undefined;
37+}
38+return {
39+toolName: value.toolName,
40+reason: value.reason,
41+failedAtMs: value.failedAtMs,
42+processId: value.processId,
43+processToken: value.processToken,
44+processStartTime: value.processStartTime,
45+ ...(isNonEmptyString(value.owner) ? { owner: value.owner } : {}),
46+};
47+},
48+displayKey: (record) => JSON.stringify([record.owner ?? "", record.toolName]),
49+// Latest wins: the most recent violation message is the actionable one.
50+pick: "latest",
51+});
52+53+function recordKey(
54+record: Pick<PersistedRuntimeToolSchemaQuarantineRecord, "owner" | "toolName" | "processId">,
55+): string {
56+return JSON.stringify([record.owner ?? "", record.toolName, record.processId]);
57+}
58+59+export type RuntimeToolSchemaQuarantineIdentity = {
60+toolName: string;
61+owner?: string;
62+};
63+64+function identityKey(identity: RuntimeToolSchemaQuarantineIdentity): string {
65+return JSON.stringify([identity.owner ?? "", identity.toolName]);
66+}
67+68+// Keys this process has persisted. Recovery clearing checks this set first so
69+// the per-run path does zero store IO unless this process actually recorded a
70+// quarantine that may have recovered.
71+const locallyPersistedKeys = new Set<string>();
72+73+export function recordPersistedRuntimeToolSchemaQuarantine(
74+quarantine: RuntimeToolSchemaQuarantine,
75+): void {
76+const record: PersistedRuntimeToolSchemaQuarantineRecord = {
77+toolName: quarantine.toolName,
78+reason: quarantine.reason,
79+ ...createRuntimeHealthRecordEnvelope(quarantine.failedAt),
80+ ...(quarantine.owner ? { owner: quarantine.owner } : {}),
81+};
82+quarantineStore.register(recordKey(record), record);
83+locallyPersistedKeys.add(identityKey(record));
84+}
85+86+/**
87+ * Removes this process's persisted quarantines for tools that now validate
88+ * cleanly. `listHealthyTools` is only invoked when this process has persisted
89+ * quarantines, keeping the common per-run path free of work.
90+ */
91+export function clearRecoveredPersistedRuntimeToolSchemaQuarantines(
92+listHealthyTools: () => readonly RuntimeToolSchemaQuarantineIdentity[],
93+): void {
94+if (locallyPersistedKeys.size === 0) {
95+return;
96+}
97+const recoveredKeys = new Set(
98+listHealthyTools()
99+.map(identityKey)
100+.filter((key) => locallyPersistedKeys.has(key)),
101+);
102+if (recoveredKeys.size === 0) {
103+return;
104+}
105+quarantineStore.clearForProcess(process.pid, (record) => recoveredKeys.has(identityKey(record)));
106+for (const key of recoveredKeys) {
107+locallyPersistedKeys.delete(key);
108+}
109+}
110+111+export function listPersistedRuntimeToolSchemaQuarantines(): RuntimeToolSchemaQuarantine[] {
112+return quarantineStore.list().map((record) => {
113+const quarantine: RuntimeToolSchemaQuarantine = {
114+toolName: record.toolName,
115+reason: record.reason,
116+failedAt: new Date(record.failedAtMs),
117+};
118+if (record.owner) {
119+quarantine.owner = record.owner;
120+}
121+return quarantine;
122+});
123+}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。