@prodero/curator 0.2.1 → 0.3.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/ARCHITECTURE.md +12 -0
- package/CHANGELOG.md +20 -0
- package/README.md +60 -6
- package/dist/chat-text.js +139 -0
- package/dist/cli.js +9 -2
- package/dist/contracts.js +2 -0
- package/dist/conversation-presentation.js +101 -142
- package/dist/core/agent-instructions.js +7 -4
- package/dist/core/context.js +4 -2
- package/dist/core/curation-graphical.js +1 -1
- package/dist/core/curation-review.js +61 -17
- package/dist/core/curation-revision.js +2 -0
- package/dist/core/curation-semantic-jobs.js +2 -2
- package/dist/core/json-schema.js +6 -0
- package/dist/core/memory-index.js +1 -1
- package/dist/core/memory-pressure.js +16 -5
- package/dist/core/memory-sync.js +16 -9
- package/dist/core/memory-transformation-proposal.js +2 -2
- package/dist/core/project.js +18 -4
- package/dist/core/session-index.js +2 -2
- package/dist/core/session-reconciliation.js +320 -0
- package/dist/curation-review-adapter.js +1 -1
- package/dist/hosts/install-command.js +130 -0
- package/dist/hosts/manager.js +1 -1
- package/dist/memory-presentation.js +5 -175
- package/dist/presentation-server.js +12 -6
- package/dist/presentation-trace.js +3 -2
- package/dist/presentation.js +57 -136
- package/dist/providers/mcp-sampling.js +6 -6
- package/dist/server.js +63 -29
- package/dist/status-presentation.js +4 -2
- package/dist/text-layout.js +16 -24
- package/dist-ui/index.html +140 -128
- package/docs/COMPATIBILITY.md +14 -4
- package/docs/INSTALL-WITH-AGENT.md +64 -0
- package/docs/RELEASING.md +2 -2
- package/docs/V0.3.0-CHAT-TEXT.md +75 -0
- package/docs/V0.3.0-MAINTENANCE.md +23 -0
- package/docs/hosts.md +18 -4
- package/instructions/AGENTS.md +6 -4
- package/package.json +8 -5
- package/plugins/migrate_ai_studio/schema.cjs +1 -2
- package/plugins/migrate_ai_studio/ui/dist/index.html +75 -42
- package/setup.md +151 -0
|
@@ -25,7 +25,7 @@ export class CurationReviewError extends Error {
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
const fail = (code, message) => { throw new CurationReviewError(code, message); };
|
|
28
|
-
const decisionSchema = z.record(z.enum(['current', 'suggested', 'deferred']));
|
|
28
|
+
const decisionSchema = z.record(z.string(), z.enum(['current', 'suggested', 'deferred']));
|
|
29
29
|
const legacySchema = z.object({ revision: z.number().int().nonnegative(), snapshotHash: z.string(), decisions: decisionSchema }).strict();
|
|
30
30
|
const eventSchema = z.object({
|
|
31
31
|
revision: z.number().int().positive(), at: z.string().datetime(),
|
|
@@ -34,7 +34,7 @@ const eventSchema = z.object({
|
|
|
34
34
|
}).strict();
|
|
35
35
|
const journalSchema = z.object({
|
|
36
36
|
version: z.literal(2), projectIdentity: z.string(), snapshotHash: z.string(),
|
|
37
|
-
base: z.object({ revision: z.number().int().nonnegative(), decisions: decisionSchema, paused: z.boolean(), origin: z.enum(['new', 'legacy_v1', 'inherited']), inheritedFrom: z.array(z.string()), pauseUpdatedAt: z.string().datetime().optional(), analysis: z.enum(['complete', 'pending', 'failed']).optional(), clarifications: z.record(z.object({ id: z.string(), basisHash: z.string(), sourceIds: z.array(z.string()), state: clarificationStateSchema }).strict()).optional() }).strict(),
|
|
37
|
+
base: z.object({ revision: z.number().int().nonnegative(), decisions: decisionSchema, paused: z.boolean(), origin: z.enum(['new', 'legacy_v1', 'inherited']), inheritedFrom: z.array(z.string()), pauseUpdatedAt: z.string().datetime().optional(), analysis: z.enum(['complete', 'pending', 'failed']).optional(), clarifications: z.record(z.string(), z.object({ id: z.string(), basisHash: z.string(), sourceIds: z.array(z.string()), state: clarificationStateSchema }).strict()).optional() }).strict(),
|
|
38
38
|
events: z.array(eventSchema).max(REVIEW_RETENTION.maxEvents),
|
|
39
39
|
}).strict();
|
|
40
40
|
export function reviewUnits(plan) {
|
|
@@ -176,6 +176,20 @@ async function names(root) {
|
|
|
176
176
|
return []; throw e; });
|
|
177
177
|
return files.filter(n => /^review_[a-zA-Z0-9_]+\.json$/.test(n)).sort();
|
|
178
178
|
}
|
|
179
|
+
/** Explicit management diagnostics only. Never repairs, deletes or trusts old choices. */
|
|
180
|
+
export async function curationHistoryDiagnostics(root) {
|
|
181
|
+
const diagnostics = [];
|
|
182
|
+
for (const name of await names(root))
|
|
183
|
+
try {
|
|
184
|
+
await loadStored(root, name.slice(0, -5));
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
if (!(error instanceof CurationReviewError))
|
|
188
|
+
throw error;
|
|
189
|
+
diagnostics.push({ reviewId: name.slice(0, -5), code: error.code, message: error.message });
|
|
190
|
+
}
|
|
191
|
+
return diagnostics;
|
|
192
|
+
}
|
|
179
193
|
async function loadStored(root, reviewId) {
|
|
180
194
|
const snapshot = await readJson(await reviewPath(root, reviewId));
|
|
181
195
|
if (snapshot.version !== 1 && snapshot.version !== 2 && snapshot.version !== 3)
|
|
@@ -236,11 +250,23 @@ export async function loadCurationReview(root, reviewId, options = {}) {
|
|
|
236
250
|
const expired = (snapshot) => Date.now() - Date.parse(snapshot.createdAt) > REVIEW_RETENTION.maxAgeMs;
|
|
237
251
|
// Pin all decision-bearing history, even after expiry. Refuse at capacity instead
|
|
238
252
|
// of silently removing unresolved decisions. Empty abandoned previews may expire.
|
|
239
|
-
async function pruneForCreation(root, now) {
|
|
253
|
+
async function pruneForCreation(root, now, preserveUnavailable = false) {
|
|
240
254
|
const referenced = await (await import('./curation-semantic-jobs.js')).semanticReferencedReviews(root);
|
|
241
255
|
const records = [];
|
|
242
256
|
for (const name of await names(root)) {
|
|
243
|
-
const id = name.slice(0, -5)
|
|
257
|
+
const id = name.slice(0, -5);
|
|
258
|
+
let stored;
|
|
259
|
+
try {
|
|
260
|
+
stored = await loadStored(root, id);
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
if (!preserveUnavailable || !(error instanceof CurationReviewError))
|
|
264
|
+
throw error;
|
|
265
|
+
const stat = await fs.stat(await reviewPath(root, id));
|
|
266
|
+
records.push({ id, pinned: true, time: stat.mtimeMs });
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
const { review, journal } = stored;
|
|
244
270
|
const stat = await fs.stat(await reviewPath(root, id));
|
|
245
271
|
const pinned = referenced.has(review.snapshot.reviewId) || !!review.snapshot.revision?.parentRevisionId || journal.events.length > 0 || Object.keys(journal.base.decisions).length > 0 || journal.base.paused;
|
|
246
272
|
records.push({ id: review.snapshot.reviewId, pinned, time: stat.mtimeMs });
|
|
@@ -293,7 +319,7 @@ async function legacyPause(root, plan, journal) {
|
|
|
293
319
|
return journal.base.paused;
|
|
294
320
|
return !!checkpoint || !!journal?.base.paused;
|
|
295
321
|
}
|
|
296
|
-
export async function createCurationReview(root, successor, memorySyncSources) {
|
|
322
|
+
export async function createCurationReview(root, successor, memorySyncSources, options = {}) {
|
|
297
323
|
const create = async () => {
|
|
298
324
|
if (await fs.stat(await checkedDataPath(root, '.transactions/pending.json')).then(() => true, () => false))
|
|
299
325
|
fail('recovery_required', 'Finish the pending transaction before starting a new review.');
|
|
@@ -309,8 +335,9 @@ export async function createCurationReview(root, successor, memorySyncSources) {
|
|
|
309
335
|
carriedSemantic = { kind: 'semantic-v2', validator: 'semantic-1', basePlan: baselinePlan, analyses, plan };
|
|
310
336
|
}
|
|
311
337
|
}
|
|
338
|
+
const fresh = options.fresh === true || successor?.parent.snapshot.revision?.historyPolicy === 'fresh-current';
|
|
312
339
|
const units = reviewUnits(plan);
|
|
313
|
-
const inherited = successor ? { decisions: {}, from: [successor.parent.snapshot.reviewId] } : await inheritedDecisions(root, plan, units);
|
|
340
|
+
const inherited = successor ? { decisions: {}, from: [successor.parent.snapshot.reviewId] } : fresh ? { decisions: {}, from: [] } : await inheritedDecisions(root, plan, units);
|
|
314
341
|
const invalidatedDecisions = [];
|
|
315
342
|
if (successor)
|
|
316
343
|
for (const [id, decision] of Object.entries(successor.parent.state.decisions)) {
|
|
@@ -322,18 +349,18 @@ export async function createCurationReview(root, successor, memorySyncSources) {
|
|
|
322
349
|
else
|
|
323
350
|
invalidatedDecisions.push(id);
|
|
324
351
|
}
|
|
325
|
-
const previous = await latestCurationReview(root, plan.planId);
|
|
352
|
+
const previous = fresh ? undefined : await latestCurationReview(root, plan.planId);
|
|
326
353
|
const body = { version: carriedSemantic ? 3 : 2, reviewId: successor ? `review_${hash(successor.operationId).slice(0, 40)}` : sortableId('obs').replace(/^obs_/, 'review_'), project: plan.project, projectIdentity: await fs.realpath(root), planHash: plan.planId, createdAt: new Date(now).toISOString(), plan, units };
|
|
327
354
|
let snapshot = JSON.parse(JSON.stringify({ ...body, snapshotHash: hash(body) }));
|
|
328
355
|
const journal = { version: 2, projectIdentity: snapshot.projectIdentity, snapshotHash: snapshot.snapshotHash,
|
|
329
|
-
base: { revision: 0, decisions: inherited.decisions, paused: successor?.parent.state.paused ?? previous?.state.paused ?? await legacyPause(root, plan), origin: inherited.from.length ? 'inherited' : 'new', inheritedFrom: inherited.from }, events: [] };
|
|
356
|
+
base: { revision: 0, decisions: inherited.decisions, paused: successor?.parent.state.paused ?? previous?.state.paused ?? (fresh ? false : await legacyPause(root, plan)), origin: inherited.from.length ? 'inherited' : 'new', inheritedFrom: inherited.from }, events: [] };
|
|
330
357
|
journal.base.pauseUpdatedAt = new Date(now).toISOString();
|
|
331
358
|
journal.base.analysis = 'complete';
|
|
332
359
|
journal.base.clarifications = Object.fromEntries(Object.entries(stateFrom(snapshot, journal).items).filter(([, i]) => i.issue).map(([id, i]) => [id, i.issue]));
|
|
333
360
|
const initial = stateFrom(snapshot, journal), catalog = await loadMemoryCatalog(root, { persistCache: false });
|
|
334
361
|
const revisionSources = catalog.summaries.filter(s => !memorySyncSources || memorySyncSources.includes(s.id));
|
|
335
362
|
await Promise.all(revisionSources.map(s => catalog.load(s.id)));
|
|
336
|
-
const revision = sealRevision({ version: carriedSemantic ? 2 : 1, lineageId: successor?.parent.snapshot.revision?.lineageId ?? successor?.parent.snapshot.reviewId ?? snapshot.reviewId,
|
|
363
|
+
const revision = sealRevision({ ...(fresh ? { historyPolicy: 'fresh-current' } : {}), version: carriedSemantic ? 2 : 1, lineageId: successor?.parent.snapshot.revision?.lineageId ?? successor?.parent.snapshot.reviewId ?? snapshot.reviewId,
|
|
337
364
|
parentRevisionId: successor?.parent.snapshot.revision?.id ?? null, parentReviewId: successor?.parent.snapshot.reviewId ?? null, reviewId: snapshot.reviewId, projectIdentity: snapshot.projectIdentity,
|
|
338
365
|
baseline: plan.fingerprint, sources: revisionSources.map(s => ({ id: s.id, revision: catalog.originalHashes.get(s.id) })).sort((a, b) => a.id.localeCompare(b.id)),
|
|
339
366
|
evidence: [...new Map(plan.proposals.flatMap(p => (p.investigation?.evidence ?? []).map(e => [e.path, { path: e.path, revision: e.revision }]))).values()].sort((a, b) => a.path.localeCompare(b.path)),
|
|
@@ -347,7 +374,7 @@ export async function createCurationReview(root, successor, memorySyncSources) {
|
|
|
347
374
|
if (Buffer.byteLength(JSON.stringify(snapshot)) > REVIEW_RETENTION.maxBytes)
|
|
348
375
|
fail('review_too_large', 'Review is too large. Reduce the review scope first.');
|
|
349
376
|
await ensureDataDirectory(root, directory);
|
|
350
|
-
await pruneForCreation(root, now);
|
|
377
|
+
await pruneForCreation(root, now, fresh);
|
|
351
378
|
// Publish the immutable snapshot last. A crash can leave only an unpublished seed.
|
|
352
379
|
await writeFileAtomic(await reviewPath(root, snapshot.reviewId, true), JSON.stringify(journal));
|
|
353
380
|
await writeFileAtomic(await reviewPath(root, snapshot.reviewId), JSON.stringify(snapshot));
|
|
@@ -419,18 +446,18 @@ export async function latestCurationReview(root, planHash) {
|
|
|
419
446
|
return reviews.filter(r => !parents.has(r.snapshot.revision?.id) && (r.snapshot.planHash === planHash || (r.snapshot.revision ? deterministicBaseline(r.snapshot.revision).planId : r.snapshot.planHash) === planHash)).sort((a, b) => b.snapshot.createdAt.localeCompare(a.snapshot.createdAt))[0];
|
|
420
447
|
}
|
|
421
448
|
/** Authoritative saved-state projection, including old pauses and completed receipts. */
|
|
422
|
-
export async function curationReviewView(root, plan, supplied) {
|
|
423
|
-
const candidate = supplied ?? await latestCurationReview(root, plan.planId);
|
|
449
|
+
export async function curationReviewView(root, plan, supplied, options = {}) {
|
|
450
|
+
const candidate = supplied ?? (options.fresh ? undefined : await latestCurationReview(root, plan.planId));
|
|
424
451
|
const stored = candidate ? await loadStored(root, candidate.snapshot.reviewId) : undefined;
|
|
425
452
|
const review = stored?.review;
|
|
426
453
|
plan = review?.snapshot.plan ?? plan;
|
|
427
454
|
const units = review?.snapshot.units ?? reviewUnits(plan);
|
|
428
|
-
const inherited = review ? undefined : await inheritedDecisions(root, plan, units);
|
|
455
|
+
const inherited = review ? undefined : options.fresh ? { decisions: {}, from: [] } : await inheritedDecisions(root, plan, units);
|
|
429
456
|
const journal = stored?.journal;
|
|
430
457
|
const state = review ? structuredClone(review.state) : stateFrom({ snapshotHash: '', plan, units }, {
|
|
431
458
|
base: { revision: 0, decisions: inherited.decisions, paused: false, origin: 'inherited', inheritedFrom: inherited.from }, events: [],
|
|
432
459
|
});
|
|
433
|
-
state.paused = await legacyPause(root, plan, journal);
|
|
460
|
+
state.paused = options.fresh && !review ? false : await legacyPause(root, plan, journal);
|
|
434
461
|
if (review)
|
|
435
462
|
await applyReceiptState(root, { snapshot: review.snapshot, state });
|
|
436
463
|
const semantic = review ? await (await import('./curation-semantic-jobs.js')).semanticReviewStatus(root, review) : undefined;
|
|
@@ -494,7 +521,15 @@ export async function resumeCurationReview(root, reviewId) {
|
|
|
494
521
|
seen.add(review.snapshot.reviewId);
|
|
495
522
|
let child;
|
|
496
523
|
for (const name of await names(root)) {
|
|
497
|
-
|
|
524
|
+
let stored;
|
|
525
|
+
try {
|
|
526
|
+
stored = await loadStored(root, name.slice(0, -5));
|
|
527
|
+
}
|
|
528
|
+
catch (error) {
|
|
529
|
+
if (review.snapshot.revision?.historyPolicy !== 'fresh-current' || !(error instanceof CurationReviewError))
|
|
530
|
+
throw error;
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
498
533
|
if (stored.review.snapshot.revision?.parentRevisionId === review.snapshot.revision?.id && review.snapshot.revision) {
|
|
499
534
|
if (child)
|
|
500
535
|
fail('invalid_review', 'Forked review lineage.');
|
|
@@ -521,11 +556,20 @@ export function validateRevisionReceipt(receipt) {
|
|
|
521
556
|
if (![2, 3].includes(snapshot.version) || state.snapshotHash !== snapshotHash || snapshot.projectIdentity !== receipt.projectIdentity || hash(receipt.applied) !== hash(receipt.selectedIds) || receipt.operationId !== curationOperationId(receipt.reviewId, receipt.selectedIds, receipt.reviewRevisionId, receipt.selectionHash) || hash(body) !== snapshotHash || snapshot.planHash !== receipt.planHash || snapshot.reviewId !== receipt.reviewId || snapshot.revision.id !== receipt.reviewRevisionId || hash(snapshot.revision.typedPlan.plan) !== hash(snapshot.plan) || hash(state.decisions) !== hash(receipt.decisions) || state.revision !== receipt.decisionRevision || receipt.selectionHash !== curationSelectionHash(snapshot.revision.id, state.revision, receipt.selectedIds) || receipt.executionHash !== hash([snapshotHash, state, receipt.selectedIds]))
|
|
522
557
|
fail('invalid_review', 'Execution binding changed.');
|
|
523
558
|
}
|
|
524
|
-
export async function reserveSuccessorCapacity(root) { await pruneForCreation(root, Date.now()); }
|
|
559
|
+
export async function reserveSuccessorCapacity(root, preserveUnavailable = false) { await pruneForCreation(root, Date.now(), preserveUnavailable); }
|
|
525
560
|
/** Read-only lineage check; semantic tools must never invoke transaction recovery. */
|
|
526
561
|
export async function assertCurationHead(root, review) {
|
|
527
562
|
for (const name of await names(root)) {
|
|
528
|
-
|
|
563
|
+
let stored;
|
|
564
|
+
try {
|
|
565
|
+
stored = await loadStored(root, name.slice(0, -5));
|
|
566
|
+
}
|
|
567
|
+
catch (error) {
|
|
568
|
+
if (review.snapshot.revision?.historyPolicy !== 'fresh-current' || !(error instanceof CurationReviewError))
|
|
569
|
+
throw error;
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
const { review: other } = stored;
|
|
529
573
|
if (other.snapshot.revision?.lineageId !== review.snapshot.revision?.lineageId && other.snapshot.createdAt > review.snapshot.createdAt && other.snapshot.revision && review.snapshot.revision && deterministicBaseline(other.snapshot.revision).planId === deterministicBaseline(review.snapshot.revision).planId)
|
|
530
574
|
fail('stale_review', 'A newer review supersedes this input.');
|
|
531
575
|
if (other.snapshot.revision?.parentRevisionId === review.snapshot.revision?.id && review.snapshot.revision)
|
|
@@ -59,6 +59,8 @@ export function selectionProblems(manifest, selectedIds) {
|
|
|
59
59
|
export function sealRevision(body) { return { ...body, id: `revision_${revisionHash(body)}` }; }
|
|
60
60
|
export function validateRevision(revision) {
|
|
61
61
|
assertSafeCurationData(revision);
|
|
62
|
+
if (revision.historyPolicy !== undefined && revision.historyPolicy !== 'fresh-current')
|
|
63
|
+
throw new Error('Unsupported curation history policy');
|
|
62
64
|
if (![1, 2].includes(revision.version) || revision.typedPlan.plan.version !== 1 || revision.dependencies.version !== 1 || revision.dependencies.engine !== 'structural-1')
|
|
63
65
|
throw new Error('Unsupported curation revision validator');
|
|
64
66
|
const { id, ...body } = revision;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { assertSafeCurationData } from './curation-evidence.js';
|
|
4
4
|
import fs from 'node:fs/promises';
|
|
5
5
|
import { z } from 'zod';
|
|
6
|
-
import {
|
|
6
|
+
import { jsonSchema } from './json-schema.js';
|
|
7
7
|
import { checkedDataPath, ensureDataDirectory, writeFileAtomic } from './storage.js';
|
|
8
8
|
import { withSyncLock, ownsSyncLock } from './sync-transaction.js';
|
|
9
9
|
import { loadMemoryCatalog } from './memory-index.js';
|
|
@@ -160,7 +160,7 @@ async function makeBundle(root, review, componentId, limits) {
|
|
|
160
160
|
bundle.id = `bundle_${hash(bundle)}`;
|
|
161
161
|
return bundle;
|
|
162
162
|
}
|
|
163
|
-
function response(job) { const { bundle, ...input } = job.input; const history = job.events.map(({ proposal, ...event }) => event); return { jobId: job.input.jobId, status: last(job).status, job: { ...job, input, events: history }, task: { instructions: SEMANTIC_INSTRUCTIONS, proposalSchema:
|
|
163
|
+
function response(job) { const { bundle, ...input } = job.input; const history = job.events.map(({ proposal, ...event }) => event); return { jobId: job.input.jobId, status: last(job).status, job: { ...job, input, events: history }, task: { instructions: SEMANTIC_INSTRUCTIONS, proposalSchema: jsonSchema(semanticProposalSchema), data: bundle }, acceptance: 'staged_candidate_only', validation: last(job).reason, resultReviewId: last(job).resultReviewId }; }
|
|
164
164
|
const passCount = (history, lineageId) => history.filter(j => j.input.lineageId === lineageId && j.input.reason !== 'validation_correction').length;
|
|
165
165
|
const passLimit = (history, lineageId, limits) => Math.min(limits.maxPasses, ...history.filter(j => j.input.lineageId === lineageId).map(j => j.input.bundle.limits.maxPasses));
|
|
166
166
|
function boundedRecoveryLimits(previous, requested) {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/** Preserve the existing draft-7 input contract, including optional defaults. */
|
|
3
|
+
export function jsonSchema(schema) {
|
|
4
|
+
return z.toJSONSchema(schema, { target: 'draft-7', io: 'input', reused: 'inline' });
|
|
5
|
+
}
|
|
6
|
+
//# sourceMappingURL=json-schema.js.map
|
|
@@ -99,7 +99,7 @@ const summarySchema = z.object({
|
|
|
99
99
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Invalid v3 applicability cache entry' });
|
|
100
100
|
}
|
|
101
101
|
});
|
|
102
|
-
const memoryCacheSchema = z.object({ version: z.literal(10), entries: z.record(summarySchema) });
|
|
102
|
+
const memoryCacheSchema = z.object({ version: z.literal(10), entries: z.record(z.string(), summarySchema) });
|
|
103
103
|
async function readCache(root, persistCache) {
|
|
104
104
|
if (persistCache)
|
|
105
105
|
await ensureDerivedStateRoot(root);
|
|
@@ -5,7 +5,7 @@ import { checkedDataPath, ensureDataDirectory, writeFileAtomic, sha256, parseChe
|
|
|
5
5
|
import { withSyncLock } from './sync-transaction.js';
|
|
6
6
|
export const MEMORY_PRESSURE_LIMITS = { sessions: 32, syncs: 32, records: 128, relationships: 128, bytes: 1048576 };
|
|
7
7
|
export const MEMORY_PRESSURE_PATH = 'local/memory-pressure.json';
|
|
8
|
-
const reasons = ['selected', 'marginal-value', 'context-budget', 'required-budget', 'item-limit', 'rank-window', 'inactive', 'superseded', 'scope-mismatch', 'no-match'];
|
|
8
|
+
const reasons = ['selected', 'task-dispute', 'marginal-value', 'context-budget', 'required-budget', 'item-limit', 'rank-window', 'inactive', 'superseded', 'scope-mismatch', 'no-match'];
|
|
9
9
|
const count = z.number().finite().nonnegative();
|
|
10
10
|
const id = z.string().max(256);
|
|
11
11
|
const recordSchema = z.object({
|
|
@@ -20,10 +20,19 @@ const selectionSchema = recordSchema.extend({ bucket: z.enum(['required', 'conte
|
|
|
20
20
|
const totalsSchema = z.object({ records: count, tokens: count }).strict();
|
|
21
21
|
const retrievalSchema = z.object({
|
|
22
22
|
queryHash: z.string().regex(/^[a-f0-9]{64}$/), purpose: z.string().max(16),
|
|
23
|
+
taskContextHash: z.string().regex(/^[a-f0-9]{64}$/).optional(),
|
|
23
24
|
budgets: z.object({ contextBudgetTokens: count, requiredContextBudgetTokens: count, maxRequiredItems: count, maxContextItems: count }).strict(),
|
|
24
|
-
totals: z.
|
|
25
|
+
totals: z.partialRecord(z.enum(reasons), totalsSchema), requiredTruncated: z.boolean(), requiredOmitted: count,
|
|
25
26
|
records: z.array(selectionSchema).max(MEMORY_PRESSURE_LIMITS.records), detailsDropped: count,
|
|
26
27
|
}).strict();
|
|
28
|
+
/** Bind optional retrieval telemetry to the actual latest native task scope. */
|
|
29
|
+
export function retrievalTaskContextHash(taskContext) {
|
|
30
|
+
const value = (taskContext && typeof taskContext === 'object' ? taskContext : {});
|
|
31
|
+
const dimensions = (key) => Array.isArray(value[key]) ? [...new Set(value[key])].sort() : [];
|
|
32
|
+
// Signal provenance is diagnostic; replaying the same normalized scope can
|
|
33
|
+
// legitimately change 'task-text' to 'argument' without changing authority.
|
|
34
|
+
return sha256(JSON.stringify({ taskText: value.taskText ?? '', paths: dimensions('paths'), subsystems: dimensions('subsystems'), taskKinds: dimensions('taskKinds'), documentationOnly: value.documentationOnly ?? false }));
|
|
35
|
+
}
|
|
27
36
|
/** Called at existing selection/filter branches; never evaluates relevance itself. */
|
|
28
37
|
export class RetrievalEvidenceCollector {
|
|
29
38
|
samples = {};
|
|
@@ -40,8 +49,10 @@ export class RetrievalEvidenceCollector {
|
|
|
40
49
|
sample.push({ ...pressureRecord(item), bucket, reason, score, rankScore, signals: signals.slice(0, 12).map(({ kind, score }) => ({ kind, score })) });
|
|
41
50
|
}
|
|
42
51
|
finish(query, purpose, budgets) {
|
|
43
|
-
//
|
|
44
|
-
|
|
52
|
+
// Task-bound dispute diagnostics are read-only evidence, never selected
|
|
53
|
+
// context or a license to activate disputed knowledge. Keep their small
|
|
54
|
+
// bounded set before ordinary samples so real conflicts reach the agent.
|
|
55
|
+
const records = ['task-dispute', 'selected', 'required-budget', 'context-budget', 'item-limit', 'marginal-value', 'rank-window', 'scope-mismatch', 'inactive', 'superseded', 'no-match'].flatMap(r => this.samples[r] ?? []).slice(0, MEMORY_PRESSURE_LIMITS.records);
|
|
45
56
|
return { queryHash: sha256(query.trim()), purpose, budgets, totals: this.totals, requiredTruncated: this.requiredOmitted > 0, requiredOmitted: this.requiredOmitted, records, detailsDropped: Object.values(this.totals).reduce((n, t) => n + (t?.records ?? 0), 0) - records.length };
|
|
46
57
|
}
|
|
47
58
|
}
|
|
@@ -50,7 +61,7 @@ const relationshipSchema = z.object({ source: id, target: recordSchema, classifi
|
|
|
50
61
|
const syncSchema = z.object({ checkpoint: id, sessionIds: z.array(id).max(128), sessionIdsDropped: count,
|
|
51
62
|
changes: z.array(recordSchema.extend({ actions: z.array(z.enum(['created', 'updated', 'merged', 'superseded'])).max(4) }).strict()).max(MEMORY_PRESSURE_LIMITS.records),
|
|
52
63
|
totals: z.object({ created: count, updated: count, merged: count, superseded: count }).strict(), changesDropped: count,
|
|
53
|
-
reconciliation: z.array(relationshipSchema).max(MEMORY_PRESSURE_LIMITS.relationships), classifications: z.
|
|
64
|
+
reconciliation: z.array(relationshipSchema).max(MEMORY_PRESSURE_LIMITS.relationships), classifications: z.partialRecord(z.enum(categories), count), relationshipsDropped: count,
|
|
54
65
|
}).strict();
|
|
55
66
|
export class SyncEvidenceCollector {
|
|
56
67
|
relationships = [];
|
package/dist/core/memory-sync.js
CHANGED
|
@@ -25,7 +25,7 @@ export const MEMORY_SYNC_LIMITS = { workflows: 1, candidates: 20, promotions: 50
|
|
|
25
25
|
const digest = (v) => sha256(JSON.stringify(v));
|
|
26
26
|
const id = z.string().regex(/^area_[a-f0-9]{20}$/);
|
|
27
27
|
const fingerprintSchema = z.object({ knowledge: z.string(), telemetry: z.string(), checkpoints: z.string() }).strict();
|
|
28
|
-
const jobSchema = z.object({ id, label: z.string(), signals: z.array(z.string()), recordCount: z.number(), counts: z.record(z.number()).optional(), categories: z.record(z.number()).optional(), sourceIds: z.array(z.string()).max(24), layers: z.object({ local: z.number(), versioned: z.number() }), selected: z.boolean(), status: z.enum(['unselected', 'pending', 'semantic_work', 'awaiting_review', 'awaiting_clarification', 'deferred', 'applied', 'skipped', 'failed']), curationReviewId: z.string().optional(), curationStateHash: z.string().optional(), error: z.string().optional(), unresolved: z.number().optional(), result: z.unknown().optional() }).strict();
|
|
28
|
+
const jobSchema = z.object({ id, label: z.string(), signals: z.array(z.string()), recordCount: z.number(), counts: z.record(z.string(), z.number()).optional(), categories: z.record(z.string(), z.number()).optional(), sourceIds: z.array(z.string()).max(24), layers: z.object({ local: z.number(), versioned: z.number() }), selected: z.boolean(), status: z.enum(['unselected', 'pending', 'semantic_work', 'awaiting_review', 'awaiting_clarification', 'deferred', 'applied', 'skipped', 'failed']), curationReviewId: z.string().optional(), curationStateHash: z.string().optional(), error: z.string().optional(), unresolved: z.number().optional(), result: z.unknown().optional() }).strict();
|
|
29
29
|
const stateSchema = z.object({ version: z.literal(1), id: z.string().uuid(), root: z.string(), revision: z.number().int(), createdAt: z.string(), state: z.enum(['awaiting_selection', 'semantic_work', 'awaiting_clarification', 'awaiting_review', 'applying', 'awaiting_promotion_selection', 'completed', 'stale']), improveEnabled: z.boolean().optional(), repairLinksEnabled: z.boolean().optional(), brokenLinks: z.number().int().nonnegative().optional(), repairLinksDone: z.boolean().optional(), repairLinksResult: z.object({ fixed: z.number(), remaining: z.number() }).optional(),
|
|
30
30
|
usage: z.object({ sessions: z.number(), available: z.boolean() }).optional(), baseline: fingerprintSchema, jobs: z.array(jobSchema).max(20), promotionEnabled: z.boolean(), promotionDone: z.boolean(), promotionCandidates: z.array(z.object({ id: z.string(), title: z.string(), score: z.number(), signals: z.array(z.string()), recommended: z.boolean() })).max(50), promotionTotal: z.number(), promotionResult: z.unknown().optional(), recovery: z.object({ sessions: z.number(), created: z.number() }), error: z.string().optional(),
|
|
31
31
|
active: z.object({ kind: z.enum(['transformation', 'promotion', 'link-repair']), reviewId: z.string(), candidateId: id.optional(), preview: z.unknown(), approved: z.boolean(), unaffectedKnowledge: z.string().optional(), touchedIds: z.array(z.string()).max(150).optional() }).strict().optional(),
|
|
@@ -135,7 +135,7 @@ function view(w, clarification, deferred = []) {
|
|
|
135
135
|
estimatedTokens: { scope: 'transformed_areas', before: applied.reduce((n, r) => n + r.estimatedTokens.before, 0), after: applied.reduce((n, r) => n + r.estimatedTokens.after, 0) },
|
|
136
136
|
versionedCreated: applied.reduce((n, r) => n + r.created.filter(c => c.layer === 'versioned').length, 0), promoted: promotion?.published ?? 0, promotionCreated: promotion?.created.length ?? 0, promotionUpdated: promotion?.updated.length ?? 0, promotionCovered: promotion?.covered.length ?? 0, localArchived: applied.reduce((n, r) => n + r.archives.filter(a => a.layer === 'local').length, 0), versionedArchived: applied.reduce((n, r) => n + r.archives.filter(a => a.layer === 'versioned').length, 0), acknowledgedCandidateIds: w.jobs.filter(j => j.status === 'applied').map(j => j.id), promotionConflicts: promotion?.conflicts.length ?? 0, historicalProvenancePreserved: true },
|
|
137
137
|
coverage: { promotionCandidatesOmitted: Math.max(0, w.promotionTotal - w.promotionCandidates.length) },
|
|
138
|
-
next: w.state === 'semantic_work' ? { action: 'semantic_work_required', candidateId: currentJob(w)?.id, instructions: w.improveEnabled ? 'Use prodero_memory_sync prepare for the next candidate. For its curation job, use the existing prodero_curation_reasoning_prepare/propose protocol, then inspect this Sync workflow. Continue sequentially until a question, review or completion. Never Apply or approve; Core handles eligible Local changes. No session lifecycle or extra scans.' : 'Use prodero_memory_sync action=prepare, then reason only over that one bundle and submit action=propose with the returned workflow ID/revision. Continue sequentially while semantic_work_required. Never approve review automatically. Do not use session prepare/record/close for this workflow.' } : ['awaiting_clarification', 'awaiting_promotion_selection', 'awaiting_review'].includes(w.state) ? { action: w.state === 'awaiting_review' ? 'human_review_required' : w.state, tool: 'prodero_sync', arguments: { projectRoot: w.root, workflowId: w.id }, instructions: '
|
|
138
|
+
next: w.state === 'semantic_work' ? { action: 'semantic_work_required', candidateId: currentJob(w)?.id, instructions: w.improveEnabled ? 'Use prodero_memory_sync prepare for the next candidate. For its curation job, use the existing prodero_curation_reasoning_prepare/propose protocol, then inspect this Sync workflow. Continue sequentially until a question, review or completion. Never Apply or approve; Core handles eligible Local changes. No session lifecycle or extra scans.' : 'Use prodero_memory_sync action=prepare, then reason only over that one bundle and submit action=propose with the returned workflow ID/revision. Continue sequentially while semantic_work_required. Never approve review automatically. Do not use session prepare/record/close for this workflow.' } : ['awaiting_clarification', 'awaiting_promotion_selection', 'awaiting_review'].includes(w.state) ? { action: w.state === 'awaiting_review' ? 'human_review_required' : w.state, tool: 'prodero_sync', arguments: { projectRoot: w.root, workflowId: w.id }, instructions: 'Legacy technical continuation only. Preserve this exact saved workflow and its unresolved originals; do not restore the removed moderation UI or ask routine housekeeping questions. A technical caller may inspect these bindings on explicit request. Required exact human approval still applies to protected/shared/link-removal/publication operations; never infer it, fabricate answers or approve for the user. Ordinary work uses the bounded current-session reconciliation protocol.' } : { action: w.state },
|
|
139
139
|
};
|
|
140
140
|
}
|
|
141
141
|
async function workflowView(root, w, clarification, deferred) {
|
|
@@ -516,20 +516,27 @@ export async function runMemorySync(root, raw) {
|
|
|
516
516
|
}
|
|
517
517
|
/** Open-only diagnostic projection. Never resumes, recovers, migrates or persists a workflow. */
|
|
518
518
|
export async function getMemorySyncOverview(root) {
|
|
519
|
-
|
|
520
|
-
const pending = workflow && workflow.state !== 'completed' ? workflow : null;
|
|
521
|
-
const stale = pending && (pending.state === 'stale' || pending.state !== 'applying' && digest(await fingerprint(root)) !== digest(pending.baseline));
|
|
522
|
-
const workflowStatus = pending ? { id: pending.id, state: stale ? 'stale' : pending.state } : null;
|
|
519
|
+
let workflowStatus = null;
|
|
523
520
|
try {
|
|
521
|
+
const workflow = await load(root);
|
|
522
|
+
const pending = workflow && workflow.state !== 'completed' ? workflow : null;
|
|
523
|
+
const stale = pending && (pending.state === 'stale' || pending.state !== 'applying' && digest(await fingerprint(root)) !== digest(pending.baseline));
|
|
524
|
+
workflowStatus = pending ? { id: pending.id, state: stale ? 'stale' : pending.state, ...(pending.error ? { error: publicDiagnostic(pending.error) } : {}) } : null;
|
|
524
525
|
const plan = await getMemoryIntelligencePlan(root);
|
|
525
526
|
return { available: plan.coverage.telemetry !== 'unavailable', sessions: plan.coverage.sessions,
|
|
526
527
|
areas: plan.candidates.map(c => ({ id: c.id, label: c.label, recordCount: c.recordCount, signals: c.signals, counts: c.counts, layers: c.layers, categories: c.categories })),
|
|
527
|
-
areasOmitted: plan.truncated.candidates, workflow: workflowStatus
|
|
528
|
+
areasOmitted: plan.truncated.candidates, workflow: workflowStatus,
|
|
529
|
+
...(workflow?.error ? { diagnostic: { code: 'PRODERO_MEMORY_MAINTENANCE_INCOMPLETE', cause: publicDiagnostic(workflow.error), recovery: 'Original knowledge and saved progress are retained. Inspect the saved operation before retrying.' } } : {}) };
|
|
528
530
|
}
|
|
529
|
-
catch {
|
|
530
|
-
return
|
|
531
|
+
catch (error) {
|
|
532
|
+
return memoryOverviewFailure(error, workflowStatus);
|
|
531
533
|
}
|
|
532
534
|
}
|
|
535
|
+
/** Preserve a sanitized failure separately from missing optional usage history. */
|
|
536
|
+
export function memoryOverviewFailure(error, workflow = null) {
|
|
537
|
+
return { available: false, sessions: 0, areas: [], areasOmitted: 0, workflow,
|
|
538
|
+
diagnostic: { code: 'PRODERO_MEMORY_OVERVIEW_UNAVAILABLE', cause: publicDiagnostic(error), recovery: 'Saved knowledge is preserved. Inspect the storage diagnostic before retrying; no maintenance was applied by this read.' } };
|
|
539
|
+
}
|
|
533
540
|
/** Resolve continuation ownership from persisted review lineage, not a particular App instance. */
|
|
534
541
|
export async function syncWorkflowForCuration(root, lineageId) {
|
|
535
542
|
const w = await load(root);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { applicabilitySchema } from "./applicability.js";
|
|
2
2
|
/** Semantic proposals only: no writer, lifecycle mutation, recovery or provider. */
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
-
import {
|
|
4
|
+
import { jsonSchema } from './json-schema.js';
|
|
5
5
|
import { buildMemoryTransformationBundle } from './memory-transformation-bundle.js';
|
|
6
6
|
import { assertPathScope } from './path-scope.js';
|
|
7
7
|
import { sha256 } from './storage.js';
|
|
@@ -38,7 +38,7 @@ For each source explain coverage, explicitly list only information omitted as re
|
|
|
38
38
|
export async function prepareMemoryTransformation(root, candidateId) {
|
|
39
39
|
const bundle = await buildMemoryTransformationBundle(root, candidateId);
|
|
40
40
|
return { bundle, instructions: MEMORY_TRANSFORMATION_INSTRUCTIONS,
|
|
41
|
-
proposalSchema:
|
|
41
|
+
proposalSchema: jsonSchema(memoryTransformationProposalSchema),
|
|
42
42
|
binding: { version: 1, bundleId: bundle.bundleId, candidateId, sourceRevisions: bundle.revisions },
|
|
43
43
|
canPropose: bundle.readyForSemanticWork, persistence: 'return_only' };
|
|
44
44
|
}
|
package/dist/core/project.js
CHANGED
|
@@ -5,6 +5,7 @@ import { recordRetrievalEvidence, recordSyncEvidenceLocked } from "./memory-pres
|
|
|
5
5
|
import { publicDiagnostic } from "./diagnostics.js";
|
|
6
6
|
import { explicitProjectRoot } from "./project-root.js";
|
|
7
7
|
import { reviewSessionKnowledge } from "./session-review.js";
|
|
8
|
+
import { prepareSessionReconciliation } from './session-reconciliation.js';
|
|
8
9
|
import { assertNoSecrets } from "./secrets.js";
|
|
9
10
|
import { intelligenceTelemetry } from "./intelligence.js";
|
|
10
11
|
import { planAssistedSync, observationEvidence, observationKey } from "./knowledge-engine.js";
|
|
@@ -432,9 +433,15 @@ export async function prepareProject(start = process.cwd(), options = {}) {
|
|
|
432
433
|
onRequiredOmitted: options.onRequiredOmitted,
|
|
433
434
|
});
|
|
434
435
|
context.agentUsage = options.agentUsage ?? null;
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
436
|
+
// Audit uncertainty/budget is deferred maintenance, not a task blocker or an
|
|
437
|
+
// instruction to ask the user to moderate memory. Context still owns actual
|
|
438
|
+
// task-relevant disputes and Required-policy readiness.
|
|
439
|
+
const maintenance = {
|
|
440
|
+
automatic: { sessions: automaticSync.sessions, created: automaticSync.created, consolidated: automaticSync.consolidated, duplicates: automaticSync.duplicates, checkpoint: automaticSync.checkpoint },
|
|
441
|
+
audit: automaticSync.intelligence?.audit,
|
|
442
|
+
unresolvedRetained: automaticSync.intelligence?.audit.status === 'attention',
|
|
443
|
+
userInputRequired: false,
|
|
444
|
+
};
|
|
438
445
|
let session = options.sessionId ? await readSession(root, options.sessionId) : undefined;
|
|
439
446
|
if (session?.status === "closed")
|
|
440
447
|
session = undefined;
|
|
@@ -444,9 +451,16 @@ export async function prepareProject(start = process.cwd(), options = {}) {
|
|
|
444
451
|
session = await updateSessionTaskContext(root, session.id, context.taskContext);
|
|
445
452
|
associatePreparedContext(context, session.id);
|
|
446
453
|
const memoryTelemetry = retrievalEvidence ? await recordRetrievalEvidence(root, session.id, retrievalEvidence) : "unavailable";
|
|
454
|
+
const reconciliation = context.policyReadiness?.blocking || memoryTelemetry !== 'recorded'
|
|
455
|
+
? { state: 'deferred', reason: context.policyReadiness?.blocking ? 'current-task-policy-not-ready' : 'current-retrieval-evidence-unavailable', humanDecisionRequired: false }
|
|
456
|
+
: await prepareSessionReconciliation(root, { sessionId: session.id }).catch(error => {
|
|
457
|
+
const diagnostic = publicDiagnostic(error);
|
|
458
|
+
context.attention.push({ kind: 'memory-health', severity: 'important', title: 'Memory maintenance could not finish', detail: diagnostic });
|
|
459
|
+
return { state: 'deferred', reason: 'maintenance-storage-failure', diagnostic, humanDecisionRequired: false };
|
|
460
|
+
});
|
|
447
461
|
const status = await getProjectStatus(root);
|
|
448
462
|
const uiState = { blocking: context.policyReadiness?.blocking ?? false, view: context.attention.length ? "attention" : "context-ready", attention: context.attention };
|
|
449
|
-
return { memoryTelemetry, project: status, session: { id: session.id, path: `.prodero/sessions/${session.path}`, status: session.status }, automaticSync, context, proderoMetrics: context.metrics, agentUsage: context.agentUsage, uiState };
|
|
463
|
+
return { memoryTelemetry, project: status, session: { id: session.id, path: `.prodero/sessions/${session.path}`, status: session.status }, automaticSync, maintenance: { ...maintenance, reconciliation }, context, proderoMetrics: context.metrics, agentUsage: context.agentUsage, uiState };
|
|
450
464
|
}
|
|
451
465
|
export async function recordProjectSession(start, sessionId, entries) {
|
|
452
466
|
const root = await findProjectRoot(start);
|
|
@@ -10,8 +10,8 @@ function sameStamp(left, right) { return Boolean(left && left.size === right.siz
|
|
|
10
10
|
const stampSchema = z.object({ size: z.number().nonnegative(), mtimeMs: z.number().finite(), ctimeMs: z.number().finite() });
|
|
11
11
|
const cacheSchema = z.object({
|
|
12
12
|
version: z.literal(3),
|
|
13
|
-
checkpoints: z.record(stampSchema.extend({ id: z.string(), createdAt: z.string(), processedKeys: z.array(z.string()), observationHashes: z.array(z.string()).optional() })),
|
|
14
|
-
sessions: z.record(stampSchema.extend({ id: z.string(), parentSessionId: z.string().regex(/^sess_[a-zA-Z0-9_]+$/).optional(), sha256: z.string().regex(/^[a-f0-9]{64}$/), status: z.enum(["active", "open", "closed"]), hasObservations: z.boolean() })),
|
|
13
|
+
checkpoints: z.record(z.string(), stampSchema.extend({ id: z.string(), createdAt: z.string(), processedKeys: z.array(z.string()), observationHashes: z.array(z.string()).optional() })),
|
|
14
|
+
sessions: z.record(z.string(), stampSchema.extend({ id: z.string(), parentSessionId: z.string().regex(/^sess_[a-zA-Z0-9_]+$/).optional(), sha256: z.string().regex(/^[a-f0-9]{64}$/), status: z.enum(["active", "open", "closed"]), hasObservations: z.boolean() })),
|
|
15
15
|
});
|
|
16
16
|
async function readCache(root, persistCache) {
|
|
17
17
|
if (persistCache)
|