@hivemagnus/ardent 1.1.7 → 1.1.10
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 +5 -0
- package/dist/context-maintenance.d.ts +21 -0
- package/dist/context-maintenance.js +225 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,6 +37,11 @@ checkpoints stay readable by the previous runtime; richer portable metadata is
|
|
|
37
37
|
stored in an additive `emberfold_checkpoint_metadata` sidecar table. The initial
|
|
38
38
|
integration retains existing budgets and uses the general continuity profile.
|
|
39
39
|
|
|
40
|
+
For existing installations that must retain their runtime extensions, the node
|
|
41
|
+
manager offers [context-only adoption](../../docs/ARDENT_CONTEXT_ADOPTION.md).
|
|
42
|
+
It stages the pinned adapter and engine without replacing the runtime, runs
|
|
43
|
+
isolated provider acceptance, and verifies an actual old-runtime rollback.
|
|
44
|
+
|
|
40
45
|
## Install
|
|
41
46
|
|
|
42
47
|
Ardent needs Node.js 24.22.1+ or 26.13.0+ (`better-sqlite3` is a native
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { type CompactionCall } from './context-checkpoints.js';
|
|
4
|
+
import { TaskStore } from './store.js';
|
|
5
|
+
import { loadModelPolicy } from './model-policy.js';
|
|
6
|
+
declare const snapshotSchema: z.ZodObject<{
|
|
7
|
+
tables: z.ZodArray<z.ZodObject<{
|
|
8
|
+
table: z.ZodString;
|
|
9
|
+
watermark: z.ZodNumber;
|
|
10
|
+
count: z.ZodNumber;
|
|
11
|
+
sha256: z.ZodString;
|
|
12
|
+
unknown_only: z.ZodBoolean;
|
|
13
|
+
}, z.core.$strict>>;
|
|
14
|
+
}, z.core.$strict>;
|
|
15
|
+
export type ContextSnapshot = z.infer<typeof snapshotSchema>;
|
|
16
|
+
export declare function snapshotContext(database: Database.Database): ContextSnapshot;
|
|
17
|
+
export declare function verifyContextSnapshot(database: Database.Database, input: unknown): void;
|
|
18
|
+
export declare function assertContextIdle(database: Database.Database): void;
|
|
19
|
+
export declare function exerciseContext(store: TaskStore, summarize: CompactionCall, loaded: NonNullable<Awaited<ReturnType<typeof loadModelPolicy>>>, previousModule: string, continueContext: (messages: unknown) => Promise<string>): Promise<Record<string, unknown>>;
|
|
20
|
+
export declare function contextMaintenance(input: unknown): Promise<Record<string, unknown>>;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { lstat, mkdir, open, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import Database from 'better-sqlite3';
|
|
6
|
+
import { parse } from 'dotenv';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import { ContextCheckpoints } from './context-checkpoints.js';
|
|
9
|
+
import { TaskStore } from './store.js';
|
|
10
|
+
import { loadConfig } from './config.js';
|
|
11
|
+
import { loadModelPolicy } from './model-policy.js';
|
|
12
|
+
import { compactWithCodex } from './codex.js';
|
|
13
|
+
import { resolveSources, validateCheckpoint } from '@hivemagnus/emberfold';
|
|
14
|
+
const digest = (value) => createHash('sha256').update(JSON.stringify(value)).digest('hex');
|
|
15
|
+
const tableName = z.string().regex(/^[a-zA-Z_][a-zA-Z0-9_]*$/);
|
|
16
|
+
const snapshotSchema = z.object({ tables: z.array(z.object({ table: tableName,
|
|
17
|
+
watermark: z.number().int().nonnegative(), count: z.number().int().nonnegative(), sha256: z.string().regex(/^[a-f0-9]{64}$/),
|
|
18
|
+
unknown_only: z.boolean() }).strict()).max(256) }).strict();
|
|
19
|
+
const previousReaderSchema = z.object({ ContextCheckpoints: z.custom(value => typeof value === 'function') });
|
|
20
|
+
function rowsDigest(database, table, watermark, unknownOnly) {
|
|
21
|
+
tableName.parse(table);
|
|
22
|
+
const hash = createHash('sha256');
|
|
23
|
+
let count = 0;
|
|
24
|
+
for (const row of database.prepare(`SELECT rowid AS __rowid__,* FROM "${table}" WHERE rowid<=?${unknownOnly ? " AND state='unknown'" : ''} ORDER BY rowid`).iterate(watermark)) {
|
|
25
|
+
hash.update(JSON.stringify(row) + '\n');
|
|
26
|
+
count++;
|
|
27
|
+
}
|
|
28
|
+
return { count, sha256: hash.digest('hex') };
|
|
29
|
+
}
|
|
30
|
+
export function snapshotContext(database) {
|
|
31
|
+
if (database.pragma('quick_check', { simple: true }) !== 'ok')
|
|
32
|
+
throw new Error('Profile database integrity check failed');
|
|
33
|
+
const tables = [];
|
|
34
|
+
const names = z.array(z.object({ name: tableName })).parse(database.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").all());
|
|
35
|
+
for (const { name } of names) {
|
|
36
|
+
const columns = z.array(z.object({ name: z.string() })).parse(database.prepare(`PRAGMA table_info("${name}")`).all());
|
|
37
|
+
const immutable = ['session_messages', 'events', 'context_checkpoints'].includes(name);
|
|
38
|
+
if (!immutable && !columns.some(column => column.name === 'state'))
|
|
39
|
+
continue;
|
|
40
|
+
const { watermark } = z.object({ watermark: z.number().int().nonnegative() })
|
|
41
|
+
.parse(database.prepare(`SELECT coalesce(max(rowid),0) AS watermark FROM "${name}"`).get());
|
|
42
|
+
tables.push({ table: name, watermark, unknown_only: !immutable, ...rowsDigest(database, name, watermark, !immutable) });
|
|
43
|
+
}
|
|
44
|
+
return { tables };
|
|
45
|
+
}
|
|
46
|
+
export function verifyContextSnapshot(database, input) {
|
|
47
|
+
for (const snapshot of snapshotSchema.parse(input).tables) {
|
|
48
|
+
const actual = rowsDigest(database, snapshot.table, snapshot.watermark, snapshot.unknown_only);
|
|
49
|
+
if (actual.count !== snapshot.count || actual.sha256 !== snapshot.sha256)
|
|
50
|
+
throw new Error('Original sources, journal events, or uncertain effects changed');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export function assertContextIdle(database) {
|
|
54
|
+
const { count } = z.object({ count: z.number().int() }).parse(database.prepare("SELECT count(*) AS count FROM tasks WHERE state IN ('queued','running','input_required')").get());
|
|
55
|
+
if (count)
|
|
56
|
+
throw new Error('Profile has unfinished tasks; no maintenance is permitted');
|
|
57
|
+
if (database.prepare('SELECT 1 FROM admission_fence LIMIT 1').get())
|
|
58
|
+
throw new Error('An existing one-way profile admission fence must remain intact');
|
|
59
|
+
const { count: deliveries } = z.object({ count: z.number().int() }).parse(database.prepare("SELECT count(*) AS count FROM channel_deliveries WHERE state='queued'").get());
|
|
60
|
+
if (deliveries)
|
|
61
|
+
throw new Error('Profile has unfinished deliveries; no maintenance is permitted');
|
|
62
|
+
}
|
|
63
|
+
async function ownedFile(path) {
|
|
64
|
+
const info = await lstat(path);
|
|
65
|
+
if (!info.isFile() || info.isSymbolicLink() || info.uid !== process.getuid?.() || (info.mode & 0o077) !== 0)
|
|
66
|
+
throw new Error('Maintenance requires owner-only profile state');
|
|
67
|
+
}
|
|
68
|
+
export async function exerciseContext(store, summarize, loaded, previousModule, continueContext) {
|
|
69
|
+
const session = store.session('context-acceptance', 'direct', 'synthetic-continuity');
|
|
70
|
+
if (store.db.prepare('SELECT 1 FROM session_messages LIMIT 1').get())
|
|
71
|
+
throw new Error('Acceptance database must be fresh');
|
|
72
|
+
for (const prompt of [
|
|
73
|
+
'Synthetic fixture: reference Ω-42. The report is weekly. Verify the original release source before completion. Delivery outcome is unknown.',
|
|
74
|
+
'Correction: the report is monthly, not weekly. Keep the exact reference. Original-source verification remains unfinished and delivery remains unconfirmed.',
|
|
75
|
+
'Retain the latest correction and unfinished verification. No external action has been performed.',
|
|
76
|
+
'Continue only from the recorded evidence; do not assume the delivery succeeded.'
|
|
77
|
+
]) {
|
|
78
|
+
const task = store.accept({ caller: session.caller, context: 'direct', sessionId: session.id, prompt,
|
|
79
|
+
backend: 'codex', deadlineMs: Date.now() + 120000 });
|
|
80
|
+
store.startQueued(task.id);
|
|
81
|
+
store.finishWithAssistant(task.id, 'completed', 'Acknowledged historical fixture: ' + prompt);
|
|
82
|
+
}
|
|
83
|
+
const originals = store.db.prepare('SELECT * FROM session_messages ORDER BY sequence').all();
|
|
84
|
+
const originalDigest = digest(originals);
|
|
85
|
+
const manager = new ContextCheckpoints(store, summarize);
|
|
86
|
+
const task = { id: 'preview:acceptance', session_id: session.id, caller: session.caller, context: 'direct', prompt: '' };
|
|
87
|
+
const compacted = await manager.prepare(task, 'Current application authority is external to history.', [], loaded, AbortSignal.timeout(180000), { force: true, apply: true, expectedHead: null });
|
|
88
|
+
if (!compacted.compacted)
|
|
89
|
+
throw new Error('Acceptance did not publish a real checkpoint');
|
|
90
|
+
const head = z.object({ active_checkpoint_id: z.string().min(1), valid: z.literal(true) }).parse(manager.status(session.id, session.caller, 'direct'));
|
|
91
|
+
const { metadata_json } = z.object({ metadata_json: z.string() }).parse(store.db.prepare('SELECT metadata_json FROM emberfold_checkpoint_metadata WHERE checkpoint_id=?').get(head.active_checkpoint_id));
|
|
92
|
+
const messages = z.array(z.object({ sequence: z.number(), role: z.enum(['user', 'assistant']), content: z.string() })).parse(originals)
|
|
93
|
+
.map(row => ({ id: String(row.sequence), role: row.role, content: row.content }));
|
|
94
|
+
const checkpoint = validateCheckpoint(JSON.parse(metadata_json), messages, JSON.stringify([session.caller, 'direct', session.id]));
|
|
95
|
+
const recovered = resolveSources(messages, checkpoint.source_message_ids);
|
|
96
|
+
if (!recovered.length || recovered.some(row => !messages.some(message => message.id === row.id && message.content === row.content)))
|
|
97
|
+
throw new Error('Source recovery failed');
|
|
98
|
+
const { ContextCheckpoints: Previous } = previousReaderSchema.parse(await import(pathToFileURL(previousModule).href));
|
|
99
|
+
const previous = new Previous(store, summarize);
|
|
100
|
+
if (previous.status(session.id, session.caller, 'direct').valid !== true)
|
|
101
|
+
throw new Error('Previous-reader rollback failed');
|
|
102
|
+
const prior = await previous.prepare(task, 'Current application authority is external to history.', [], loaded, AbortSignal.timeout(5000));
|
|
103
|
+
if (digest(prior.messages) !== digest(compacted.messages))
|
|
104
|
+
throw new Error('Previous-reader context projection differs');
|
|
105
|
+
const answer = JSON.parse(await continueContext(compacted.messages));
|
|
106
|
+
z.object({ reference: z.literal('Ω-42'), schedule: z.literal('monthly'),
|
|
107
|
+
verification: z.literal('pending'), delivery: z.literal('unknown') }).strict().parse(answer);
|
|
108
|
+
store.db.prepare('UPDATE context_checkpoints SET summary_json=? WHERE id=?').run('{invalid', head.active_checkpoint_id);
|
|
109
|
+
if (manager.status(session.id, session.caller, 'direct').valid !== false)
|
|
110
|
+
throw new Error('Corruption was not detected');
|
|
111
|
+
const rebuilt = await manager.prepare(task, 'Current application authority is external to history.', [], loaded, AbortSignal.timeout(180000), { force: true, apply: true, expectedHead: head.active_checkpoint_id });
|
|
112
|
+
if (!rebuilt.compacted || manager.status(session.id, session.caller, 'direct').valid !== true)
|
|
113
|
+
throw new Error('Original-source rebuild failed');
|
|
114
|
+
if (digest(store.db.prepare('SELECT * FROM session_messages ORDER BY sequence').all()) !== originalDigest)
|
|
115
|
+
throw new Error('Acceptance rewrote original messages');
|
|
116
|
+
return { compacted: true, continued: true, recovered_sources: recovered.length, rebuilt: true,
|
|
117
|
+
previous_reader: true, original_messages_unchanged: true, tools_disabled: true };
|
|
118
|
+
}
|
|
119
|
+
const requestSchema = z.object({ operation: z.enum(['inspect', 'backup', 'verify', 'accept', 'check']),
|
|
120
|
+
profile_dir: z.string().min(1), work_dir: z.string().min(1), previous_module: z.string().min(1),
|
|
121
|
+
expected_policy_sha256: z.string().regex(/^[a-f0-9]{64}$/), expected_config_sha256: z.string().regex(/^[a-f0-9]{64}$/), snapshot: snapshotSchema.optional() }).strict();
|
|
122
|
+
export async function contextMaintenance(input) {
|
|
123
|
+
const request = requestSchema.parse(input);
|
|
124
|
+
const databasePath = join(request.profile_dir, 'state/ardent.sqlite');
|
|
125
|
+
await ownedFile(databasePath);
|
|
126
|
+
const configBytes = await readFile(join(request.profile_dir, 'config.yaml'));
|
|
127
|
+
if (createHash('sha256').update(configBytes).digest('hex') !== request.expected_config_sha256)
|
|
128
|
+
throw new Error('Profile configuration does not match the adoption pin');
|
|
129
|
+
const policy = await loadModelPolicy(request.profile_dir);
|
|
130
|
+
if (!policy || policy.sha256 !== request.expected_policy_sha256 || !policy.policy.compaction.enabled)
|
|
131
|
+
throw new Error('Active model policy does not match the adoption pin');
|
|
132
|
+
const database = new Database(databasePath, { readonly: true, fileMustExist: true });
|
|
133
|
+
try {
|
|
134
|
+
if (request.operation === 'inspect') {
|
|
135
|
+
assertContextIdle(database);
|
|
136
|
+
return { idle: true, snapshot: snapshotContext(database), policy_sha256: policy.sha256 };
|
|
137
|
+
}
|
|
138
|
+
if (request.operation === 'check') {
|
|
139
|
+
assertContextIdle(database);
|
|
140
|
+
if (await lstat(join(request.profile_dir, 'state/gateway.lock')).then(() => true, (error) => {
|
|
141
|
+
if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT')
|
|
142
|
+
return false;
|
|
143
|
+
throw error;
|
|
144
|
+
}))
|
|
145
|
+
throw new Error('Gateway execution lock remains after stopping; inspect the exact owner');
|
|
146
|
+
const snapshot = snapshotSchema.parse(request.snapshot);
|
|
147
|
+
verifyContextSnapshot(database, snapshot);
|
|
148
|
+
for (const table of snapshot.tables.filter(table => table.unknown_only)) {
|
|
149
|
+
const actual = rowsDigest(database, table.table, Number.MAX_SAFE_INTEGER, true);
|
|
150
|
+
if (actual.count !== table.count)
|
|
151
|
+
throw new Error('Uncertain work appeared while stopping the profile');
|
|
152
|
+
}
|
|
153
|
+
return { idle: true, originals_preserved: true, uncertain_effects_preserved: true };
|
|
154
|
+
}
|
|
155
|
+
if (request.operation === 'backup') {
|
|
156
|
+
assertContextIdle(database);
|
|
157
|
+
const destination = join(request.work_dir, 'original.sqlite');
|
|
158
|
+
await writeFile(destination, '', { flag: 'wx', mode: 0o600 });
|
|
159
|
+
await database.backup(destination);
|
|
160
|
+
const snapshot = snapshotContext(database);
|
|
161
|
+
await writeFile(join(request.work_dir, 'original-snapshot.json'), JSON.stringify(snapshot), { flag: 'wx', mode: 0o600 });
|
|
162
|
+
for (const path of [destination, join(request.work_dir, 'original-snapshot.json'), request.work_dir]) {
|
|
163
|
+
const file = await open(path, 'r');
|
|
164
|
+
try {
|
|
165
|
+
await file.sync();
|
|
166
|
+
}
|
|
167
|
+
finally {
|
|
168
|
+
await file.close();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return { backed_up: true, snapshot };
|
|
172
|
+
}
|
|
173
|
+
if (request.operation === 'verify') {
|
|
174
|
+
verifyContextSnapshot(database, JSON.parse(await readFile(join(request.work_dir, 'original-snapshot.json'), 'utf8')));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
finally {
|
|
178
|
+
database.close();
|
|
179
|
+
}
|
|
180
|
+
if (request.operation === 'verify') {
|
|
181
|
+
const store = new TaskStore(databasePath);
|
|
182
|
+
try {
|
|
183
|
+
const forbidden = async () => { throw new Error('Production validation must not call the model'); };
|
|
184
|
+
const current = new ContextCheckpoints(store, forbidden);
|
|
185
|
+
const { ContextCheckpoints: Previous } = previousReaderSchema.parse(await import(pathToFileURL(request.previous_module).href));
|
|
186
|
+
const previous = new Previous(store, forbidden);
|
|
187
|
+
const sessions = z.array(z.object({ id: z.string(), caller: z.string(), context: z.enum(['direct', 'peer', 'scheduled']) })).max(10000)
|
|
188
|
+
.parse(store.db.prepare('SELECT s.id,s.caller,s.context FROM sessions s JOIN context_checkpoint_heads h ON h.session_id=s.id').all());
|
|
189
|
+
for (const session of sessions) {
|
|
190
|
+
const status = current.status(session.id, session.caller, session.context);
|
|
191
|
+
const prior = previous.status(session.id, session.caller, session.context);
|
|
192
|
+
if (status.valid !== true || prior.valid !== true || status.active_checkpoint_id !== prior.active_checkpoint_id)
|
|
193
|
+
throw new Error('A production checkpoint failed current or previous-reader validation');
|
|
194
|
+
}
|
|
195
|
+
return { originals_preserved: true, uncertain_effects_preserved: true, checkpoint_heads_verified: sessions.length };
|
|
196
|
+
}
|
|
197
|
+
finally {
|
|
198
|
+
store.close();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const loaded = await loadConfig(request.profile_dir);
|
|
202
|
+
if (loaded.revision !== request.expected_config_sha256)
|
|
203
|
+
throw new Error('Profile configuration changed before model preparation');
|
|
204
|
+
const env = { ...process.env, ...parse(await readFile(join(request.profile_dir, '.env'))) };
|
|
205
|
+
const summarize = (route, prompt, signal) => compactWithCodex(loaded.config.providers.codex.command, request.profile_dir, route.model, route.reasoning_effort, prompt, signal, env, loaded.config.providers.codex.auth_mode);
|
|
206
|
+
const directory = join(request.work_dir, 'acceptance');
|
|
207
|
+
await mkdir(directory, { mode: 0o700 });
|
|
208
|
+
const store = new TaskStore(join(directory, 'state.sqlite'));
|
|
209
|
+
try {
|
|
210
|
+
const route = { backend: 'codex', ...policy.policy.primary.codex };
|
|
211
|
+
return await exerciseContext(store, summarize, policy, request.previous_module, messages => summarize(route, JSON.stringify({ task: 'From this history only, return JSON fields reference, schedule, verification (pending or complete), delivery (unknown or confirmed). Preserve the latest correction and uncertainty.', messages }), AbortSignal.timeout(180000)));
|
|
212
|
+
}
|
|
213
|
+
finally {
|
|
214
|
+
store.close();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (process.argv[1] && pathToFileURL(resolve(process.argv[1])).href === import.meta.url) {
|
|
218
|
+
let bytes = '';
|
|
219
|
+
for await (const chunk of process.stdin) {
|
|
220
|
+
bytes += String(chunk);
|
|
221
|
+
if (Buffer.byteLength(bytes) > 1024 * 1024)
|
|
222
|
+
throw new Error('Maintenance input exceeds limit');
|
|
223
|
+
}
|
|
224
|
+
console.log(JSON.stringify(await contextMaintenance(JSON.parse(bytes))));
|
|
225
|
+
}
|