@gmickel/gno 1.20.0 → 1.22.0
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 +29 -5
- package/assets/skill/SKILL.md +46 -15
- package/package.json +2 -1
- package/spec/cli.md +144 -0
- package/spec/db/schema.sql +170 -0
- package/spec/evals-agentic.md +48 -0
- package/spec/mcp.md +22 -0
- package/spec/output-schemas/capsule-reverified-event.schema.json +47 -0
- package/spec/output-schemas/changes.schema.json +280 -0
- package/spec/output-schemas/document-diff.schema.json +185 -0
- package/spec/output-schemas/impact.schema.json +122 -0
- package/spec/output-schemas/publish-artifact.schema.json +284 -0
- package/spec/output-schemas/saved-capsule-list.schema.json +16 -0
- package/spec/output-schemas/saved-capsule-registration.schema.json +172 -0
- package/spec/output-schemas/saved-capsule-reverification.schema.json +59 -0
- package/spec/output-schemas/saved-capsule-unwatch.schema.json +16 -0
- package/spec/output-schemas/saved-capsule-watch.schema.json +17 -0
- package/src/cli/commands/changes.ts +160 -0
- package/src/cli/commands/context-saved.ts +189 -0
- package/src/cli/options.ts +8 -0
- package/src/cli/program.ts +195 -0
- package/src/core/capsule-registry.ts +279 -0
- package/src/core/capsule-reverification-scheduler.ts +218 -0
- package/src/core/capsule-reverification.ts +289 -0
- package/src/core/change-diff.ts +182 -0
- package/src/core/change-journal.ts +228 -0
- package/src/core/knowledge-delta.ts +395 -0
- package/src/core/knowledge-impact.ts +202 -0
- package/src/ingestion/sync.ts +214 -165
- package/src/mcp/tools/changes.ts +80 -0
- package/src/mcp/tools/index.ts +29 -0
- package/src/publish/artifact-validation.ts +259 -0
- package/src/publish/artifact.ts +234 -118
- package/src/publish/export-service.ts +5 -9
- package/src/publish/metadata.ts +195 -0
- package/src/sdk/client.ts +42 -0
- package/src/sdk/index.ts +7 -0
- package/src/sdk/types.ts +22 -0
- package/src/serve/doc-events.ts +12 -1
- package/src/serve/resident-runtime.ts +22 -0
- package/src/serve/routes/api.ts +13 -0
- package/src/serve/routes/changes.ts +102 -0
- package/src/serve/server.ts +34 -0
- package/src/serve/watch-service.ts +9 -0
- package/src/store/index.ts +21 -0
- package/src/store/migrations/015-document-change-journal.ts +85 -0
- package/src/store/migrations/016-saved-capsules.ts +131 -0
- package/src/store/migrations/017-document-change-retention-counters.ts +33 -0
- package/src/store/migrations/018-saved-capsule-registration-epoch.ts +24 -0
- package/src/store/migrations/019-saved-capsule-registration-generation.ts +53 -0
- package/src/store/migrations/index.ts +10 -0
- package/src/store/sqlite/adapter.ts +291 -7
- package/src/store/sqlite/capsule-registry-store.ts +534 -0
- package/src/store/sqlite/change-journal-store.ts +473 -0
- package/src/store/types.ts +262 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/** Metadata-only registry for explicitly saved Context Capsule files. */
|
|
2
|
+
|
|
3
|
+
// node:path resolve has no Bun path utility equivalent.
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
SavedCapsuleNotificationPreference,
|
|
8
|
+
SavedCapsuleRegistrationRecord,
|
|
9
|
+
StorePort,
|
|
10
|
+
StoreResult,
|
|
11
|
+
} from "../store/types";
|
|
12
|
+
import type { ContextCapsuleV1 } from "./context-capsule";
|
|
13
|
+
|
|
14
|
+
import { DEFAULT_INDEX_NAME, stripUriIndex } from "../app/constants";
|
|
15
|
+
import { canonicalizeIndexName } from "../app/index-name";
|
|
16
|
+
import { decodeDocumentChangeCursor } from "./change-journal";
|
|
17
|
+
import { sha256Text } from "./context-capsule-validation";
|
|
18
|
+
import { parseCanonicalContextCapsuleForVerification } from "./context-verifier";
|
|
19
|
+
import { canonicalVerifierJson } from "./context-verifier-canonical";
|
|
20
|
+
|
|
21
|
+
const MAX_CAPSULE_BYTES = 16 * 1024 * 1024;
|
|
22
|
+
const MAX_EVIDENCE_REFERENCES = 10_000;
|
|
23
|
+
const MAX_QUESTION_BYTES = 8192;
|
|
24
|
+
const MAX_LABEL_BYTES = 512;
|
|
25
|
+
const UTF8_ENCODER = new TextEncoder();
|
|
26
|
+
const UTF8_DECODER = new TextDecoder("utf-8", { fatal: true });
|
|
27
|
+
|
|
28
|
+
type RegistryStore = Pick<
|
|
29
|
+
StorePort,
|
|
30
|
+
| "deleteSavedCapsuleRegistration"
|
|
31
|
+
| "getSavedCapsuleRegistration"
|
|
32
|
+
| "listDocumentChanges"
|
|
33
|
+
| "listSavedCapsuleRegistrations"
|
|
34
|
+
| "upsertSavedCapsuleRegistration"
|
|
35
|
+
>;
|
|
36
|
+
|
|
37
|
+
export type SavedCapsuleRegistryErrorCode =
|
|
38
|
+
| "capsule_file_changed"
|
|
39
|
+
| "capsule_file_missing"
|
|
40
|
+
| "capsule_file_too_large"
|
|
41
|
+
| "capsule_read_failed"
|
|
42
|
+
| "invalid_filter"
|
|
43
|
+
| "invalid_metadata"
|
|
44
|
+
| "registration_not_found"
|
|
45
|
+
| "store_failed";
|
|
46
|
+
|
|
47
|
+
export class SavedCapsuleRegistryError extends Error {
|
|
48
|
+
readonly code: SavedCapsuleRegistryErrorCode;
|
|
49
|
+
|
|
50
|
+
constructor(
|
|
51
|
+
code: SavedCapsuleRegistryErrorCode,
|
|
52
|
+
message: string,
|
|
53
|
+
cause?: unknown
|
|
54
|
+
) {
|
|
55
|
+
super(message, cause === undefined ? undefined : { cause });
|
|
56
|
+
this.name = "SavedCapsuleRegistryError";
|
|
57
|
+
this.code = code;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface RegisterSavedCapsuleInput {
|
|
62
|
+
filePath: string;
|
|
63
|
+
question?: string;
|
|
64
|
+
label?: string;
|
|
65
|
+
notificationPreference?: SavedCapsuleNotificationPreference;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface LoadedSavedCapsule {
|
|
69
|
+
capsule: ContextCapsuleV1;
|
|
70
|
+
fileHash: string;
|
|
71
|
+
filePath: string;
|
|
72
|
+
raw: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const unwrapStore = <T>(result: StoreResult<T>, operation: string): T => {
|
|
76
|
+
if (result.ok) return result.value;
|
|
77
|
+
throw new SavedCapsuleRegistryError(
|
|
78
|
+
"store_failed",
|
|
79
|
+
`${operation}: ${result.error.message}`,
|
|
80
|
+
result.error.cause
|
|
81
|
+
);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const boundedOptionalText = (
|
|
85
|
+
value: string | undefined,
|
|
86
|
+
field: "question" | "label",
|
|
87
|
+
maxBytes: number
|
|
88
|
+
): string | null => {
|
|
89
|
+
if (value === undefined) return null;
|
|
90
|
+
const normalized = value.trim().normalize("NFC");
|
|
91
|
+
if (
|
|
92
|
+
normalized.length === 0 ||
|
|
93
|
+
UTF8_ENCODER.encode(normalized).byteLength > maxBytes
|
|
94
|
+
) {
|
|
95
|
+
throw new SavedCapsuleRegistryError(
|
|
96
|
+
"invalid_metadata",
|
|
97
|
+
`${field} must be non-empty and at most ${maxBytes} UTF-8 bytes`
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
return normalized;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export const loadSavedCapsuleFile = async (
|
|
104
|
+
filePath: string,
|
|
105
|
+
expectedFileHash?: string
|
|
106
|
+
): Promise<LoadedSavedCapsule> => {
|
|
107
|
+
const canonicalPath = resolve(filePath);
|
|
108
|
+
const file = Bun.file(canonicalPath);
|
|
109
|
+
if (!(await file.exists())) {
|
|
110
|
+
throw new SavedCapsuleRegistryError(
|
|
111
|
+
"capsule_file_missing",
|
|
112
|
+
"Saved Context Capsule file is missing"
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
if (file.size < 1 || file.size > MAX_CAPSULE_BYTES) {
|
|
116
|
+
throw new SavedCapsuleRegistryError(
|
|
117
|
+
"capsule_file_too_large",
|
|
118
|
+
`Saved Context Capsule must be between 1 and ${MAX_CAPSULE_BYTES} bytes`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
const raw = UTF8_DECODER.decode(await file.arrayBuffer());
|
|
123
|
+
const fileHash = sha256Text(raw);
|
|
124
|
+
if (expectedFileHash !== undefined && fileHash !== expectedFileHash) {
|
|
125
|
+
throw new SavedCapsuleRegistryError(
|
|
126
|
+
"capsule_file_changed",
|
|
127
|
+
"Saved Context Capsule file changed after registration"
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
const capsule = parseCanonicalContextCapsuleForVerification(
|
|
131
|
+
JSON.parse(raw) as unknown
|
|
132
|
+
);
|
|
133
|
+
if (capsule.evidence.length > MAX_EVIDENCE_REFERENCES) {
|
|
134
|
+
throw new SavedCapsuleRegistryError(
|
|
135
|
+
"capsule_file_too_large",
|
|
136
|
+
`Saved Context Capsule exceeds ${MAX_EVIDENCE_REFERENCES} evidence references`
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
capsule,
|
|
141
|
+
fileHash,
|
|
142
|
+
filePath: canonicalPath,
|
|
143
|
+
raw,
|
|
144
|
+
};
|
|
145
|
+
} catch (cause) {
|
|
146
|
+
if (cause instanceof SavedCapsuleRegistryError) throw cause;
|
|
147
|
+
throw new SavedCapsuleRegistryError(
|
|
148
|
+
"capsule_read_failed",
|
|
149
|
+
cause instanceof Error
|
|
150
|
+
? `Saved Context Capsule is invalid: ${cause.message}`
|
|
151
|
+
: "Saved Context Capsule is invalid",
|
|
152
|
+
cause
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const assertRuntimeIndex = (
|
|
158
|
+
capsule: ContextCapsuleV1,
|
|
159
|
+
runtimeIndexName: string
|
|
160
|
+
): string => {
|
|
161
|
+
const effective = canonicalizeIndexName(
|
|
162
|
+
runtimeIndexName || DEFAULT_INDEX_NAME
|
|
163
|
+
);
|
|
164
|
+
if (effective !== capsule.scope.indexName) {
|
|
165
|
+
throw new SavedCapsuleRegistryError(
|
|
166
|
+
"invalid_filter",
|
|
167
|
+
`Context Capsule index ${capsule.scope.indexName} does not match runtime index ${effective}`
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
return effective;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const latestSequence = async (store: RegistryStore): Promise<number> => {
|
|
174
|
+
const page = unwrapStore(
|
|
175
|
+
await store.listDocumentChanges({ limit: 1 }),
|
|
176
|
+
"Failed to read the document change journal"
|
|
177
|
+
);
|
|
178
|
+
return decodeDocumentChangeCursor(page.latestCursor);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
/** Register an explicit file without persisting or rewriting its body. */
|
|
182
|
+
export const registerSavedCapsule = async (
|
|
183
|
+
store: RegistryStore,
|
|
184
|
+
runtimeIndexName: string,
|
|
185
|
+
input: RegisterSavedCapsuleInput,
|
|
186
|
+
nowMs: number = Date.now()
|
|
187
|
+
): Promise<SavedCapsuleRegistrationRecord> => {
|
|
188
|
+
// Capture the conservative high-water mark before reading the caller-owned
|
|
189
|
+
// file. Any journal change concurrent with file loading then remains newer
|
|
190
|
+
// than the registration and cannot be skipped by the resident scheduler.
|
|
191
|
+
const sequence = await latestSequence(store);
|
|
192
|
+
const loaded = await loadSavedCapsuleFile(input.filePath);
|
|
193
|
+
const indexName = assertRuntimeIndex(loaded.capsule, runtimeIndexName);
|
|
194
|
+
const registrationId = `capsule-${sha256Text(loaded.filePath).slice(0, 40)}`;
|
|
195
|
+
const existing = unwrapStore(
|
|
196
|
+
await store.getSavedCapsuleRegistration(registrationId),
|
|
197
|
+
"Failed to read saved Context Capsule registration"
|
|
198
|
+
);
|
|
199
|
+
return unwrapStore(
|
|
200
|
+
await store.upsertSavedCapsuleRegistration({
|
|
201
|
+
registrationId,
|
|
202
|
+
filePath: loaded.filePath,
|
|
203
|
+
fileHash: loaded.fileHash,
|
|
204
|
+
capsuleId: loaded.capsule.capsuleId,
|
|
205
|
+
indexName,
|
|
206
|
+
question: boundedOptionalText(
|
|
207
|
+
input.question,
|
|
208
|
+
"question",
|
|
209
|
+
MAX_QUESTION_BYTES
|
|
210
|
+
),
|
|
211
|
+
label: boundedOptionalText(input.label, "label", MAX_LABEL_BYTES),
|
|
212
|
+
notificationPreference: input.notificationPreference ?? "none",
|
|
213
|
+
registeredAtMs: existing?.registeredAtMs ?? nowMs,
|
|
214
|
+
updatedAtMs: nowMs,
|
|
215
|
+
lastAttemptedSequence: sequence,
|
|
216
|
+
evidence: loaded.capsule.evidence
|
|
217
|
+
.map((evidence) => ({
|
|
218
|
+
evidenceId: evidence.evidenceId,
|
|
219
|
+
canonicalUri: stripUriIndex(evidence.uri),
|
|
220
|
+
collection: evidence.collection,
|
|
221
|
+
sourceHash: evidence.sourceHash,
|
|
222
|
+
mirrorHash: evidence.mirrorHash,
|
|
223
|
+
passageHash: evidence.passageHash,
|
|
224
|
+
}))
|
|
225
|
+
.sort((left, right) =>
|
|
226
|
+
left.evidenceId < right.evidenceId
|
|
227
|
+
? -1
|
|
228
|
+
: left.evidenceId > right.evidenceId
|
|
229
|
+
? 1
|
|
230
|
+
: 0
|
|
231
|
+
),
|
|
232
|
+
}),
|
|
233
|
+
"Failed to register saved Context Capsule"
|
|
234
|
+
);
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
export const listSavedCapsules = async (
|
|
238
|
+
store: RegistryStore
|
|
239
|
+
): Promise<SavedCapsuleRegistrationRecord[]> =>
|
|
240
|
+
unwrapStore(
|
|
241
|
+
await store.listSavedCapsuleRegistrations(),
|
|
242
|
+
"Failed to list saved Context Capsules"
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
export const getSavedCapsule = async (
|
|
246
|
+
store: RegistryStore,
|
|
247
|
+
registrationId: string
|
|
248
|
+
): Promise<SavedCapsuleRegistrationRecord> => {
|
|
249
|
+
const registration = unwrapStore(
|
|
250
|
+
await store.getSavedCapsuleRegistration(registrationId),
|
|
251
|
+
"Failed to read saved Context Capsule"
|
|
252
|
+
);
|
|
253
|
+
if (!registration) {
|
|
254
|
+
throw new SavedCapsuleRegistryError(
|
|
255
|
+
"registration_not_found",
|
|
256
|
+
"Saved Context Capsule registration not found"
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
return registration;
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
export const unregisterSavedCapsule = async (
|
|
263
|
+
store: RegistryStore,
|
|
264
|
+
registrationId: string
|
|
265
|
+
): Promise<void> => {
|
|
266
|
+
const deleted = unwrapStore(
|
|
267
|
+
await store.deleteSavedCapsuleRegistration(registrationId),
|
|
268
|
+
"Failed to remove saved Context Capsule"
|
|
269
|
+
);
|
|
270
|
+
if (!deleted) {
|
|
271
|
+
throw new SavedCapsuleRegistryError(
|
|
272
|
+
"registration_not_found",
|
|
273
|
+
"Saved Context Capsule registration not found"
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
export const canonicalSavedCapsuleRegistryJson = (value: unknown): string =>
|
|
279
|
+
canonicalVerifierJson(value);
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/** Bounded, coalescing resident scheduler for evidence-triggered reverification. */
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
SavedCapsuleRegistrationRecord,
|
|
5
|
+
StorePort,
|
|
6
|
+
StoreResult,
|
|
7
|
+
} from "../store/types";
|
|
8
|
+
import type {
|
|
9
|
+
SavedCapsuleReverificationDeps,
|
|
10
|
+
SavedCapsuleReverificationOutcome,
|
|
11
|
+
} from "./capsule-reverification";
|
|
12
|
+
|
|
13
|
+
import { SavedCapsuleRegistryError } from "./capsule-registry";
|
|
14
|
+
import { reverifySavedCapsule } from "./capsule-reverification";
|
|
15
|
+
import {
|
|
16
|
+
decodeDocumentChangeCursor,
|
|
17
|
+
encodeDocumentChangeCursor,
|
|
18
|
+
} from "./change-journal";
|
|
19
|
+
|
|
20
|
+
const MAX_REGISTRATIONS_PER_DRAIN = 10_000;
|
|
21
|
+
|
|
22
|
+
type SchedulerStore = StorePort &
|
|
23
|
+
Pick<
|
|
24
|
+
StorePort,
|
|
25
|
+
| "getSavedCapsuleReverificationState"
|
|
26
|
+
| "listDocumentChanges"
|
|
27
|
+
| "listSavedCapsuleIdsAffectedByChanges"
|
|
28
|
+
| "listSavedCapsuleRegistrations"
|
|
29
|
+
| "setSavedCapsuleReverificationSequence"
|
|
30
|
+
>;
|
|
31
|
+
|
|
32
|
+
export interface SavedCapsuleReverificationDrain {
|
|
33
|
+
fromSequence: number;
|
|
34
|
+
throughSequence: number;
|
|
35
|
+
cursorExpired: boolean;
|
|
36
|
+
affected: number;
|
|
37
|
+
completed: number;
|
|
38
|
+
failed: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface SavedCapsuleReverificationSchedulerOptions {
|
|
42
|
+
deps: Omit<SavedCapsuleReverificationDeps, "store"> & {
|
|
43
|
+
store: SchedulerStore & SavedCapsuleReverificationDeps["store"];
|
|
44
|
+
};
|
|
45
|
+
startBackgroundWork: (
|
|
46
|
+
operation: (signal: AbortSignal) => Promise<void>
|
|
47
|
+
) => boolean;
|
|
48
|
+
onDrain?: (result: SavedCapsuleReverificationDrain) => void;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const unwrapStore = <T>(result: StoreResult<T>, operation: string): T => {
|
|
52
|
+
if (result.ok) return result.value;
|
|
53
|
+
throw new SavedCapsuleRegistryError(
|
|
54
|
+
"store_failed",
|
|
55
|
+
`${operation}: ${result.error.message}`,
|
|
56
|
+
result.error.cause
|
|
57
|
+
);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export class SavedCapsuleReverificationScheduler {
|
|
61
|
+
readonly #options: SavedCapsuleReverificationSchedulerOptions;
|
|
62
|
+
#pending = false;
|
|
63
|
+
#running = false;
|
|
64
|
+
#disposed = false;
|
|
65
|
+
|
|
66
|
+
constructor(options: SavedCapsuleReverificationSchedulerOptions) {
|
|
67
|
+
this.#options = options;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
notifySyncSettled(): void {
|
|
71
|
+
if (this.#disposed) return;
|
|
72
|
+
this.#pending = true;
|
|
73
|
+
if (this.#running) return;
|
|
74
|
+
this.#running = true;
|
|
75
|
+
const started = this.#options.startBackgroundWork(async (signal) => {
|
|
76
|
+
await this.#run(signal);
|
|
77
|
+
});
|
|
78
|
+
if (!started) {
|
|
79
|
+
this.#running = false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async triggerNow(signal: AbortSignal = new AbortController().signal) {
|
|
84
|
+
if (this.#disposed) return [];
|
|
85
|
+
const results: SavedCapsuleReverificationDrain[] = [];
|
|
86
|
+
do {
|
|
87
|
+
this.#pending = false;
|
|
88
|
+
results.push(await this.#drain(signal));
|
|
89
|
+
} while (this.#pending && !signal.aborted && !this.#disposed);
|
|
90
|
+
return results;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async dispose(): Promise<void> {
|
|
94
|
+
this.#disposed = true;
|
|
95
|
+
await Promise.resolve();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async #run(signal: AbortSignal): Promise<void> {
|
|
99
|
+
const operation = (async () => {
|
|
100
|
+
try {
|
|
101
|
+
while (this.#pending && !signal.aborted && !this.#disposed) {
|
|
102
|
+
this.#pending = false;
|
|
103
|
+
const result = await this.#drain(signal);
|
|
104
|
+
this.#options.onDrain?.(result);
|
|
105
|
+
}
|
|
106
|
+
} finally {
|
|
107
|
+
this.#running = false;
|
|
108
|
+
if (this.#pending && !this.#disposed) {
|
|
109
|
+
this.notifySyncSettled();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
})();
|
|
113
|
+
await operation;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async #drain(signal: AbortSignal): Promise<SavedCapsuleReverificationDrain> {
|
|
117
|
+
const store = this.#options.deps.store;
|
|
118
|
+
const schedulerState = unwrapStore(
|
|
119
|
+
await store.getSavedCapsuleReverificationState(),
|
|
120
|
+
"Failed to read saved Capsule scheduler state"
|
|
121
|
+
);
|
|
122
|
+
const fromSequence = schedulerState.lastProcessedSequence;
|
|
123
|
+
const journal = unwrapStore(
|
|
124
|
+
await store.listDocumentChanges({
|
|
125
|
+
cursor: encodeDocumentChangeCursor(fromSequence),
|
|
126
|
+
limit: 1,
|
|
127
|
+
}),
|
|
128
|
+
"Failed to read document change journal"
|
|
129
|
+
);
|
|
130
|
+
const throughSequence = decodeDocumentChangeCursor(journal.latestCursor);
|
|
131
|
+
if (throughSequence <= fromSequence) {
|
|
132
|
+
return {
|
|
133
|
+
fromSequence,
|
|
134
|
+
throughSequence,
|
|
135
|
+
cursorExpired: journal.cursorExpired,
|
|
136
|
+
affected: 0,
|
|
137
|
+
completed: 0,
|
|
138
|
+
failed: 0,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let registrations: SavedCapsuleRegistrationRecord[];
|
|
143
|
+
if (journal.cursorExpired) {
|
|
144
|
+
registrations = unwrapStore(
|
|
145
|
+
await store.listSavedCapsuleRegistrations(),
|
|
146
|
+
"Failed to list saved Context Capsules"
|
|
147
|
+
);
|
|
148
|
+
if (registrations.length > MAX_REGISTRATIONS_PER_DRAIN) {
|
|
149
|
+
throw new SavedCapsuleRegistryError(
|
|
150
|
+
"store_failed",
|
|
151
|
+
"Saved Capsule scheduler registration bound exceeded"
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
const affected = unwrapStore(
|
|
156
|
+
await store.listSavedCapsuleIdsAffectedByChanges(
|
|
157
|
+
fromSequence,
|
|
158
|
+
throughSequence,
|
|
159
|
+
MAX_REGISTRATIONS_PER_DRAIN
|
|
160
|
+
),
|
|
161
|
+
"Failed to resolve changed saved Capsule evidence"
|
|
162
|
+
);
|
|
163
|
+
if (affected.truncated) {
|
|
164
|
+
throw new SavedCapsuleRegistryError(
|
|
165
|
+
"store_failed",
|
|
166
|
+
"Saved Capsule scheduler registration bound exceeded"
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
const all = unwrapStore(
|
|
170
|
+
await store.listSavedCapsuleRegistrations(),
|
|
171
|
+
"Failed to list saved Context Capsules"
|
|
172
|
+
);
|
|
173
|
+
const ids = new Set(affected.registrationIds);
|
|
174
|
+
registrations = all.filter((registration) =>
|
|
175
|
+
ids.has(registration.registrationId)
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const outcomes: SavedCapsuleReverificationOutcome[] = [];
|
|
180
|
+
for (const registration of registrations) {
|
|
181
|
+
if (signal.aborted) break;
|
|
182
|
+
if (registration.lastAttemptedSequence >= throughSequence) continue;
|
|
183
|
+
outcomes.push(
|
|
184
|
+
await reverifySavedCapsule(
|
|
185
|
+
registration.registrationId,
|
|
186
|
+
{
|
|
187
|
+
kind: "journal",
|
|
188
|
+
fromSequence,
|
|
189
|
+
throughSequence,
|
|
190
|
+
},
|
|
191
|
+
this.#options.deps
|
|
192
|
+
)
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
if (!signal.aborted) {
|
|
196
|
+
const advanced = unwrapStore(
|
|
197
|
+
await store.setSavedCapsuleReverificationSequence(
|
|
198
|
+
throughSequence,
|
|
199
|
+
schedulerState.registrationEpoch
|
|
200
|
+
),
|
|
201
|
+
"Failed to advance saved Capsule scheduler state"
|
|
202
|
+
);
|
|
203
|
+
if (!advanced) this.#pending = true;
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
fromSequence,
|
|
207
|
+
throughSequence,
|
|
208
|
+
cursorExpired: journal.cursorExpired,
|
|
209
|
+
affected: outcomes.length,
|
|
210
|
+
completed: outcomes.filter(
|
|
211
|
+
(outcome) => outcome.verification.operationStatus === "completed"
|
|
212
|
+
).length,
|
|
213
|
+
failed: outcomes.filter(
|
|
214
|
+
(outcome) => outcome.verification.operationStatus === "failed"
|
|
215
|
+
).length,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
}
|