instar 1.3.1674 → 1.3.1676
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/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +55 -6
- package/dist/commands/server.js.map +1 -1
- package/dist/core/CodexLifecycleProductionComposition.d.ts +1 -1
- package/dist/core/CodexLifecycleProductionComposition.d.ts.map +1 -1
- package/dist/core/EvolutionArchive.d.ts +79 -0
- package/dist/core/EvolutionArchive.d.ts.map +1 -0
- package/dist/core/EvolutionArchive.js +309 -0
- package/dist/core/EvolutionArchive.js.map +1 -0
- package/dist/core/EvolutionArchiveSync.d.ts +26 -0
- package/dist/core/EvolutionArchiveSync.d.ts.map +1 -0
- package/dist/core/EvolutionArchiveSync.js +109 -0
- package/dist/core/EvolutionArchiveSync.js.map +1 -0
- package/dist/core/EvolutionManager.d.ts +37 -6
- package/dist/core/EvolutionManager.d.ts.map +1 -1
- package/dist/core/EvolutionManager.js +210 -149
- package/dist/core/EvolutionManager.js.map +1 -1
- package/dist/core/FileClassifier.d.ts.map +1 -1
- package/dist/core/FileClassifier.js +3 -0
- package/dist/core/FileClassifier.js.map +1 -1
- package/dist/core/JobCoverage.d.ts +53 -0
- package/dist/core/JobCoverage.d.ts.map +1 -0
- package/dist/core/JobCoverage.js +194 -0
- package/dist/core/JobCoverage.js.map +1 -0
- package/dist/core/JobCoverageMonitor.d.ts +84 -0
- package/dist/core/JobCoverageMonitor.d.ts.map +1 -0
- package/dist/core/JobCoverageMonitor.js +364 -0
- package/dist/core/JobCoverageMonitor.js.map +1 -0
- package/dist/core/JobDependency.d.ts +4 -0
- package/dist/core/JobDependency.d.ts.map +1 -0
- package/dist/core/JobDependency.js +30 -0
- package/dist/core/JobDependency.js.map +1 -0
- package/dist/core/JobPortableBundle.d.ts +61 -0
- package/dist/core/JobPortableBundle.d.ts.map +1 -0
- package/dist/core/JobPortableBundle.js +302 -0
- package/dist/core/JobPortableBundle.js.map +1 -0
- package/dist/core/JobPortableFs.d.ts +18 -0
- package/dist/core/JobPortableFs.d.ts.map +1 -0
- package/dist/core/JobPortableFs.js +65 -0
- package/dist/core/JobPortableFs.js.map +1 -0
- package/dist/core/JobPortableIntegrity.d.ts +4 -0
- package/dist/core/JobPortableIntegrity.d.ts.map +1 -0
- package/dist/core/JobPortableIntegrity.js +86 -0
- package/dist/core/JobPortableIntegrity.js.map +1 -0
- package/dist/core/JobPortableSync.d.ts +79 -0
- package/dist/core/JobPortableSync.d.ts.map +1 -0
- package/dist/core/JobPortableSync.js +513 -0
- package/dist/core/JobPortableSync.js.map +1 -0
- package/dist/core/LeaseCoordinator.d.ts.map +1 -1
- package/dist/core/LeaseCoordinator.js +4 -3
- package/dist/core/LeaseCoordinator.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +38 -3
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/ReplicatedStoreReconcilerWiring.d.ts +2 -0
- package/dist/core/ReplicatedStoreReconcilerWiring.d.ts.map +1 -1
- package/dist/core/ReplicatedStoreReconcilerWiring.js +4 -4
- package/dist/core/ReplicatedStoreReconcilerWiring.js.map +1 -1
- package/dist/core/WriteDomainRegistry.d.ts.map +1 -1
- package/dist/core/WriteDomainRegistry.js +13 -0
- package/dist/core/WriteDomainRegistry.js.map +1 -1
- package/dist/core/jobMeshAwareness.d.ts +12 -0
- package/dist/core/jobMeshAwareness.d.ts.map +1 -0
- package/dist/core/jobMeshAwareness.js +37 -0
- package/dist/core/jobMeshAwareness.js.map +1 -0
- package/dist/core/roleFollowRestart.d.ts +16 -0
- package/dist/core/roleFollowRestart.d.ts.map +1 -1
- package/dist/core/roleFollowRestart.js +32 -1
- package/dist/core/roleFollowRestart.js.map +1 -1
- package/dist/core/types.d.ts +24 -4
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/data/standards-guard-index.json +1 -1
- package/dist/data/standards-guard-index.meta.json +2 -2
- package/dist/data/standards-registry.meta.json +1 -1
- package/dist/localVerify/FailedFileDiagnosisPeerService.d.ts.map +1 -1
- package/dist/localVerify/FailedFileDiagnosisPeerService.js +3 -1
- package/dist/localVerify/FailedFileDiagnosisPeerService.js.map +1 -1
- package/dist/messaging/TelegramAdapter.d.ts +4 -0
- package/dist/messaging/TelegramAdapter.d.ts.map +1 -1
- package/dist/messaging/TelegramAdapter.js +24 -0
- package/dist/messaging/TelegramAdapter.js.map +1 -1
- package/dist/monitoring/guardManifest.d.ts.map +1 -1
- package/dist/monitoring/guardManifest.js +6 -0
- package/dist/monitoring/guardManifest.js.map +1 -1
- package/dist/native/InstarWedgeCanary.app/Contents/MacOS/InstarWedgeCanary +0 -0
- package/dist/native/instar-desktop-host +0 -0
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +11 -0
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/scheduler/AgentMdJobLoader.d.ts +4 -15
- package/dist/scheduler/AgentMdJobLoader.d.ts.map +1 -1
- package/dist/scheduler/AgentMdJobLoader.js +69 -1
- package/dist/scheduler/AgentMdJobLoader.js.map +1 -1
- package/dist/scheduler/JobCatalogReconcile.d.ts +2 -0
- package/dist/scheduler/JobCatalogReconcile.d.ts.map +1 -0
- package/dist/scheduler/JobCatalogReconcile.js +27 -0
- package/dist/scheduler/JobCatalogReconcile.js.map +1 -0
- package/dist/scheduler/JobLoader.d.ts.map +1 -1
- package/dist/scheduler/JobLoader.js +26 -1
- package/dist/scheduler/JobLoader.js.map +1 -1
- package/dist/scheduler/JobScheduler.d.ts +14 -9
- package/dist/scheduler/JobScheduler.d.ts.map +1 -1
- package/dist/scheduler/JobScheduler.js +182 -9
- package/dist/scheduler/JobScheduler.js.map +1 -1
- package/dist/scheduler/PromotionCatchUpStore.d.ts +20 -0
- package/dist/scheduler/PromotionCatchUpStore.d.ts.map +1 -0
- package/dist/scheduler/PromotionCatchUpStore.js +60 -0
- package/dist/scheduler/PromotionCatchUpStore.js.map +1 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +1 -0
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/CapabilityIndex.d.ts.map +1 -1
- package/dist/server/CapabilityIndex.js +2 -1
- package/dist/server/CapabilityIndex.js.map +1 -1
- package/dist/server/leaseRenewalDependencies.d.ts +4 -0
- package/dist/server/leaseRenewalDependencies.d.ts.map +1 -1
- package/dist/server/leaseRenewalDependencies.js +2 -0
- package/dist/server/leaseRenewalDependencies.js.map +1 -1
- package/dist/server/routes.d.ts +2 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +265 -0
- package/dist/server/routes.js.map +1 -1
- package/dist/testing/selfActionRegistry.d.ts.map +1 -1
- package/dist/testing/selfActionRegistry.js +92 -0
- package/dist/testing/selfActionRegistry.js.map +1 -1
- package/package.json +1 -1
- package/scripts/job-portable-fs.py +254 -0
- package/scripts/job-portable-sync-rollback.mjs +66 -0
- package/src/data/builtin-manifest.json +66 -66
- package/src/data/feature-ladders.json +2 -0
- package/src/data/standards-guard-index.json +1 -1
- package/src/data/standards-guard-index.meta.json +2 -2
- package/src/data/standards-registry.meta.json +1 -1
- package/src/scaffold/templates.ts +11 -0
- package/upgrades/1.3.1675.md +48 -0
- package/upgrades/1.3.1676.md +18 -0
- package/upgrades/side-effects/evolution-retention-cap-saturation.md +172 -0
- package/upgrades/side-effects/lease-job-coverage-and-catchup.md +86 -0
- package/upgrades/side-effects/lease-job-definition-replication.md +94 -0
- package/upgrades/side-effects/takeover-keeps-lease.md +17 -0
- package/upgrades/takeover-keeps-lease.eli16.md +22 -0
|
@@ -17,6 +17,9 @@
|
|
|
17
17
|
import fs from 'node:fs';
|
|
18
18
|
import path from 'node:path';
|
|
19
19
|
import { createHash } from 'node:crypto';
|
|
20
|
+
import { EvolutionArchive, isEvolutionRecordId } from './EvolutionArchive.js';
|
|
21
|
+
import { deriveLearningRecordKey } from './LearningsReplicatedStore.js';
|
|
22
|
+
import { deriveEvolutionActionRecordKey } from './EvolutionActionsReplicatedStore.js';
|
|
20
23
|
import { SafeFsExecutor } from './SafeFsExecutor.js';
|
|
21
24
|
import { AutoExpiryLedger } from './AutoExpiryLedger.js';
|
|
22
25
|
import { TaskFlowError } from '../tasks/task-flow-types.js';
|
|
@@ -61,6 +64,8 @@ function ownerKeyForProposal(proposalId) {
|
|
|
61
64
|
export class EvolutionManager {
|
|
62
65
|
stateDir;
|
|
63
66
|
config;
|
|
67
|
+
archive;
|
|
68
|
+
retentionWarnings = new Map();
|
|
64
69
|
trustElevationTracker = null;
|
|
65
70
|
autonomousEvolution = null;
|
|
66
71
|
autonomyManager = null;
|
|
@@ -131,6 +136,7 @@ export class EvolutionManager {
|
|
|
131
136
|
constructor(config) {
|
|
132
137
|
this.config = config;
|
|
133
138
|
this.stateDir = config.stateDir;
|
|
139
|
+
this.archive = new EvolutionArchive(config.stateDir, config.archiveOrigin);
|
|
134
140
|
this.actionAutoExpiryLedger = new AutoExpiryLedger({ stateDir: config.stateDir, feature: 'evolution-actions' });
|
|
135
141
|
if (config.autoExpiry?.enabled === true) {
|
|
136
142
|
const intervalMs = Math.max(60_000, config.autoExpiry.sweepIntervalMs ?? 21_600_000);
|
|
@@ -637,13 +643,24 @@ export class EvolutionManager {
|
|
|
637
643
|
const fp = this.filePath(name);
|
|
638
644
|
if (!fs.existsSync(fp))
|
|
639
645
|
return defaultValue;
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
+
const parsed = JSON.parse(fs.readFileSync(fp, 'utf-8'));
|
|
647
|
+
const names = { 'evolution-queue': 'proposals', 'learning-registry': 'learnings',
|
|
648
|
+
'capability-gaps': 'gaps', 'action-queue': 'actions' };
|
|
649
|
+
const kind = names[name];
|
|
650
|
+
const rows = parsed?.[kind];
|
|
651
|
+
const statuses = {
|
|
652
|
+
proposals: ['proposed', 'approved', 'in_progress', 'implemented', 'rejected', 'deferred'],
|
|
653
|
+
actions: ['pending', 'in_progress', 'completed', 'cancelled'],
|
|
654
|
+
gaps: ['identified', 'addressed', 'wont_fix'],
|
|
655
|
+
};
|
|
656
|
+
if (!Array.isArray(rows) || rows.some((r) => !r || !isEvolutionRecordId(r.id)
|
|
657
|
+
|| typeof r.title !== 'string' || (kind === 'learnings' ? typeof r.applied !== 'boolean'
|
|
658
|
+
: !statuses[kind]?.includes(r.status)))) {
|
|
659
|
+
throw new Error(`Invalid evolution store: ${fp}`);
|
|
646
660
|
}
|
|
661
|
+
if (new Set(rows.map((row) => row.id)).size !== rows.length)
|
|
662
|
+
throw new Error(`Duplicate evolution IDs: ${fp}`);
|
|
663
|
+
return parsed;
|
|
647
664
|
}
|
|
648
665
|
writeFile(name, data) {
|
|
649
666
|
const fp = this.filePath(name);
|
|
@@ -665,14 +682,104 @@ export class EvolutionManager {
|
|
|
665
682
|
now() {
|
|
666
683
|
return new Date().toISOString();
|
|
667
684
|
}
|
|
685
|
+
target(kind) {
|
|
686
|
+
const aliases = { proposals: 'maxProposals', learnings: 'maxLearnings', gaps: 'maxGaps', actions: 'maxActions' };
|
|
687
|
+
const defaults = { proposals: 200, learnings: 500, gaps: 200, actions: 300 };
|
|
688
|
+
const supplied = this.config.retentionTargets?.[kind];
|
|
689
|
+
const legacy = this.config[aliases[kind]];
|
|
690
|
+
const value = supplied !== undefined ? supplied : legacy !== undefined ? legacy : defaults[kind];
|
|
691
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
692
|
+
}
|
|
693
|
+
terminal(kind, row) {
|
|
694
|
+
switch (kind) {
|
|
695
|
+
case 'proposals': return ['implemented', 'rejected'].includes(row.status);
|
|
696
|
+
case 'learnings': return row.applied === true;
|
|
697
|
+
case 'gaps': return row.status !== 'identified';
|
|
698
|
+
case 'actions': return !row.autoExpiryHold && ['completed', 'cancelled'].includes(row.status);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
retentionStats(kind, rows) {
|
|
702
|
+
const active = rows.filter(row => !this.terminal(kind, row)).length;
|
|
703
|
+
const target = this.target(kind);
|
|
704
|
+
return { total: rows.length, active, terminalRetained: rows.length - active, target,
|
|
705
|
+
terminalAllowance: target === null ? null : Math.max(0, target - active),
|
|
706
|
+
overflow: target === null ? null : Math.max(0, rows.length - target) };
|
|
707
|
+
}
|
|
708
|
+
warnRetention(kind, detail) {
|
|
709
|
+
const signature = JSON.stringify(detail);
|
|
710
|
+
if (this.retentionWarnings.get(kind) === signature)
|
|
711
|
+
return;
|
|
712
|
+
this.retentionWarnings.set(kind, signature);
|
|
713
|
+
console.warn(`[EvolutionManager] retention-pressure ${JSON.stringify({ store: kind, ...detail })}`);
|
|
714
|
+
}
|
|
715
|
+
retain(kind, rows) {
|
|
716
|
+
const target = this.target(kind);
|
|
717
|
+
let retained = rows;
|
|
718
|
+
let preservationError;
|
|
719
|
+
if (target !== null && rows.length > target) {
|
|
720
|
+
const active = rows.filter(row => !this.terminal(kind, row));
|
|
721
|
+
const terminal = rows.filter(row => this.terminal(kind, row));
|
|
722
|
+
const allowance = Math.max(0, target - active.length);
|
|
723
|
+
const keep = allowance > 0 ? terminal.slice(-allowance) : [];
|
|
724
|
+
const removed = terminal.slice(0, terminal.length - keep.length);
|
|
725
|
+
if (removed.length) {
|
|
726
|
+
try {
|
|
727
|
+
this.archive.preserve(kind, removed);
|
|
728
|
+
retained = [...active, ...keep];
|
|
729
|
+
}
|
|
730
|
+
catch (error) {
|
|
731
|
+
// @silent-fallback-ok: preservation failed; every source record remains hot and pressure is observable.
|
|
732
|
+
preservationError = String(error);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
const stats = this.retentionStats(kind, retained);
|
|
737
|
+
if (preservationError)
|
|
738
|
+
this.warnRetention(kind, { ...stats, reason: 'archive-unavailable', error: preservationError });
|
|
739
|
+
else if (target === null || (stats.overflow ?? 0) > 0)
|
|
740
|
+
this.warnRetention(kind, stats);
|
|
741
|
+
else
|
|
742
|
+
this.retentionWarnings.delete(kind);
|
|
743
|
+
return retained;
|
|
744
|
+
}
|
|
745
|
+
getArchivePage(options = {}) {
|
|
746
|
+
return this.archive.page(options);
|
|
747
|
+
}
|
|
748
|
+
getRetentionReport() {
|
|
749
|
+
const loaders = { proposals: () => this.loadEvolution().proposals, learnings: () => this.loadLearnings().learnings,
|
|
750
|
+
gaps: () => this.loadGaps().gaps, actions: () => this.loadActions().actions };
|
|
751
|
+
return Object.fromEntries(Object.entries(loaders).map(([kind, load]) => {
|
|
752
|
+
try {
|
|
753
|
+
return [kind, { status: 'known', ...this.retentionStats(kind, load()) }];
|
|
754
|
+
}
|
|
755
|
+
catch (error) {
|
|
756
|
+
return [kind, { status: 'unknown', error: String(error) }];
|
|
757
|
+
}
|
|
758
|
+
}));
|
|
759
|
+
}
|
|
760
|
+
nextRetainedId(kind, prefix, live) {
|
|
761
|
+
const records = [...live, ...this.archive.localEntries(kind)];
|
|
762
|
+
const max = records.reduce((n, record) => {
|
|
763
|
+
const suffix = record.id.startsWith(prefix + '-') ? record.id.slice(prefix.length + 1) : '';
|
|
764
|
+
if (!/^[0-9]+$/.test(suffix))
|
|
765
|
+
return n;
|
|
766
|
+
const value = Number(suffix);
|
|
767
|
+
if (!Number.isSafeInteger(value))
|
|
768
|
+
throw new Error(`Evolution ${kind} IDs exhausted`);
|
|
769
|
+
return Math.max(n, value);
|
|
770
|
+
}, 0);
|
|
771
|
+
if (!Number.isSafeInteger(max + 1))
|
|
772
|
+
throw new Error(`Evolution ${kind} IDs exhausted`);
|
|
773
|
+
return `${prefix}-${String(max + 1).padStart(3, '0')}`;
|
|
774
|
+
}
|
|
668
775
|
// ── Evolution Queue ─────────────────────────────────────────────
|
|
669
776
|
loadEvolution() {
|
|
670
777
|
return this.readFile('evolution-queue', {
|
|
671
778
|
proposals: [],
|
|
672
|
-
stats: { totalProposals: 0, byStatus: {}, byType: {}, lastUpdated: this.now() },
|
|
779
|
+
stats: { ...this.retentionStats('proposals', []), totalProposals: 0, byStatus: {}, byType: {}, lastUpdated: this.now() },
|
|
673
780
|
});
|
|
674
781
|
}
|
|
675
|
-
|
|
782
|
+
computeEvolutionStateStats(state) {
|
|
676
783
|
// Recompute stats
|
|
677
784
|
const statusCounts = {};
|
|
678
785
|
const typeCounts = {};
|
|
@@ -680,29 +787,21 @@ export class EvolutionManager {
|
|
|
680
787
|
statusCounts[p.status] = (statusCounts[p.status] || 0) + 1;
|
|
681
788
|
typeCounts[p.type] = (typeCounts[p.type] || 0) + 1;
|
|
682
789
|
}
|
|
683
|
-
|
|
790
|
+
return {
|
|
791
|
+
...this.retentionStats('proposals', state.proposals),
|
|
684
792
|
totalProposals: state.proposals.length,
|
|
685
793
|
byStatus: statusCounts,
|
|
686
794
|
byType: typeCounts,
|
|
687
795
|
lastUpdated: this.now(),
|
|
688
796
|
};
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
const archived = state.proposals.filter(p => ['implemented', 'rejected'].includes(p.status));
|
|
694
|
-
// Keep most recent archived
|
|
695
|
-
const keep = archived.slice(-Math.max(0, max - active.length));
|
|
696
|
-
state.proposals = [...active, ...keep];
|
|
697
|
-
}
|
|
797
|
+
}
|
|
798
|
+
saveEvolution(state) {
|
|
799
|
+
state.proposals = this.retain('proposals', state.proposals);
|
|
800
|
+
state.stats = this.computeEvolutionStateStats(state);
|
|
698
801
|
this.writeFile('evolution-queue', state);
|
|
699
802
|
}
|
|
700
803
|
nextProposalId(state) {
|
|
701
|
-
|
|
702
|
-
let num = 1;
|
|
703
|
-
while (existing.has(`EVO-${String(num).padStart(3, '0')}`))
|
|
704
|
-
num++;
|
|
705
|
-
return `EVO-${String(num).padStart(3, '0')}`;
|
|
804
|
+
return this.nextRetainedId('proposals', 'EVO', state.proposals);
|
|
706
805
|
}
|
|
707
806
|
addProposal(opts) {
|
|
708
807
|
const state = this.loadEvolution();
|
|
@@ -801,16 +900,16 @@ export class EvolutionManager {
|
|
|
801
900
|
return proposals;
|
|
802
901
|
}
|
|
803
902
|
getEvolutionStats() {
|
|
804
|
-
return this.loadEvolution()
|
|
903
|
+
return this.computeEvolutionStateStats(this.loadEvolution());
|
|
805
904
|
}
|
|
806
905
|
// ── Learning Registry ───────────────────────────────────────────
|
|
807
906
|
loadLearnings() {
|
|
808
907
|
return this.readFile('learning-registry', {
|
|
809
908
|
learnings: [],
|
|
810
|
-
stats: { totalLearnings: 0, applied: 0, pending: 0, byCategory: {}, lastUpdated: this.now() },
|
|
909
|
+
stats: { ...this.retentionStats('learnings', []), totalLearnings: 0, applied: 0, pending: 0, byCategory: {}, lastUpdated: this.now() },
|
|
811
910
|
});
|
|
812
911
|
}
|
|
813
|
-
|
|
912
|
+
computeLearningStateStats(state) {
|
|
814
913
|
const categoryCounts = {};
|
|
815
914
|
let applied = 0;
|
|
816
915
|
for (const l of state.learnings) {
|
|
@@ -818,50 +917,29 @@ export class EvolutionManager {
|
|
|
818
917
|
if (l.applied)
|
|
819
918
|
applied++;
|
|
820
919
|
}
|
|
821
|
-
|
|
920
|
+
return {
|
|
921
|
+
...this.retentionStats('learnings', state.learnings),
|
|
822
922
|
totalLearnings: state.learnings.length,
|
|
823
923
|
applied,
|
|
824
924
|
pending: state.learnings.length - applied,
|
|
825
925
|
byCategory: categoryCounts,
|
|
826
926
|
lastUpdated: this.now(),
|
|
827
927
|
};
|
|
828
|
-
|
|
928
|
+
}
|
|
929
|
+
saveLearnings(state) {
|
|
829
930
|
const beforePrune = state.learnings;
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
const appliedEntries = state.learnings.filter(l => l.applied);
|
|
833
|
-
const keep = appliedEntries.slice(-Math.max(0, max - unapplied.length));
|
|
834
|
-
state.learnings = [...unapplied, ...keep];
|
|
835
|
-
}
|
|
931
|
+
state.learnings = this.retain('learnings', state.learnings);
|
|
932
|
+
state.stats = this.computeLearningStateStats(state);
|
|
836
933
|
this.writeFile('learning-registry', state);
|
|
837
|
-
//
|
|
838
|
-
//
|
|
839
|
-
// swallows its own errors, but we wrap defensively so a replication fault can NEVER
|
|
840
|
-
// break a local learning write. CRITICAL (tombstone resurrection guard): a learning
|
|
841
|
-
// that was PRUNED over maxLearnings emits an op:delete tombstone, else a peer
|
|
842
|
-
// re-replicates the locally-pruned learning forever.
|
|
934
|
+
// Preserve the final full-content put for evicted records. The archive-inclusive
|
|
935
|
+
// reconciler retries failures; operational eviction is never a memory deletion.
|
|
843
936
|
const emitter = this.learningReplication;
|
|
844
937
|
if (emitter) {
|
|
845
|
-
|
|
846
|
-
const deletedAt = this.now();
|
|
847
|
-
for (const pruned of beforePrune) {
|
|
848
|
-
if (survivors.has(pruned.id))
|
|
849
|
-
continue;
|
|
850
|
-
try {
|
|
851
|
-
emitter.emitDelete(pruned.title, pruned.category, pruned.source, deletedAt);
|
|
852
|
-
}
|
|
853
|
-
catch {
|
|
854
|
-
// @silent-fallback-ok: a replication emit fault must never break or roll back
|
|
855
|
-
// a local learning write — the durable on-disk state is already persisted
|
|
856
|
-
// above. The emitter counts its own failures internally; this guard only
|
|
857
|
-
// ensures a throw from the seam cannot propagate into the local write path.
|
|
858
|
-
}
|
|
859
|
-
this.lastEmittedLearningFp.delete(pruned.id);
|
|
860
|
-
}
|
|
938
|
+
// Hot-list eviction preserves memory; it never emits deletion authority.
|
|
861
939
|
// Standby-write-wedge fix (see saveActions): re-emit ONLY changed learnings. The old
|
|
862
940
|
// "re-emit every survivor" made each write O(records × journalBytes) synchronous
|
|
863
941
|
// journal scans; a status/applied change alters the fingerprint so it still re-emits.
|
|
864
|
-
for (const l of
|
|
942
|
+
for (const l of beforePrune) {
|
|
865
943
|
const fp = this.emitFingerprint(l);
|
|
866
944
|
if (this.lastEmittedLearningFp.get(l.id) === fp)
|
|
867
945
|
continue; // unchanged ⇒ skip re-emit
|
|
@@ -870,7 +948,7 @@ export class EvolutionManager {
|
|
|
870
948
|
this.lastEmittedLearningFp.set(l.id, fp);
|
|
871
949
|
}
|
|
872
950
|
catch {
|
|
873
|
-
// @silent-fallback-ok:
|
|
951
|
+
// @silent-fallback-ok: replication remains best-effort — replication is
|
|
874
952
|
// best-effort and must never break the local write. The fingerprint is NOT
|
|
875
953
|
// recorded on a throw so the next save retries this record.
|
|
876
954
|
}
|
|
@@ -878,11 +956,7 @@ export class EvolutionManager {
|
|
|
878
956
|
}
|
|
879
957
|
}
|
|
880
958
|
nextLearningId(state) {
|
|
881
|
-
|
|
882
|
-
let num = 1;
|
|
883
|
-
while (existing.has(`LRN-${String(num).padStart(3, '0')}`))
|
|
884
|
-
num++;
|
|
885
|
-
return `LRN-${String(num).padStart(3, '0')}`;
|
|
959
|
+
return this.nextRetainedId('learnings', 'LRN', state.learnings);
|
|
886
960
|
}
|
|
887
961
|
addLearning(opts) {
|
|
888
962
|
const state = this.loadLearnings();
|
|
@@ -920,17 +994,49 @@ export class EvolutionManager {
|
|
|
920
994
|
learnings = learnings.filter(l => l.applied === filter.applied);
|
|
921
995
|
return learnings;
|
|
922
996
|
}
|
|
997
|
+
/** Archive-inclusive current projection for puts-only repair. Live records win by replication identity. */
|
|
998
|
+
listLearningsForReplication() {
|
|
999
|
+
const records = new Map();
|
|
1000
|
+
for (const row of this.archive.localEntries('learnings')) {
|
|
1001
|
+
const key = deriveLearningRecordKey(row.title, row.category, row.source);
|
|
1002
|
+
if (key)
|
|
1003
|
+
records.set(key, row);
|
|
1004
|
+
}
|
|
1005
|
+
for (const row of this.listLearnings()) {
|
|
1006
|
+
const key = deriveLearningRecordKey(row.title, row.category, row.source);
|
|
1007
|
+
if (key)
|
|
1008
|
+
records.set(key, row);
|
|
1009
|
+
}
|
|
1010
|
+
return [...records.values()];
|
|
1011
|
+
}
|
|
1012
|
+
listActionsForReplication() {
|
|
1013
|
+
const records = new Map();
|
|
1014
|
+
for (const entry of this.loadActionArchive()) {
|
|
1015
|
+
const row = entry.class === 'retention' || entry.restoredAt ? entry.action : {
|
|
1016
|
+
...entry.action, status: 'cancelled', resolution: 'auto-archived after long inactivity; restorable',
|
|
1017
|
+
};
|
|
1018
|
+
const key = deriveEvolutionActionRecordKey(row.title, row.commitTo, row.createdAt);
|
|
1019
|
+
if (key)
|
|
1020
|
+
records.set(key, row);
|
|
1021
|
+
}
|
|
1022
|
+
for (const row of this.listActions()) {
|
|
1023
|
+
const key = deriveEvolutionActionRecordKey(row.title, row.commitTo, row.createdAt);
|
|
1024
|
+
if (key)
|
|
1025
|
+
records.set(key, row);
|
|
1026
|
+
}
|
|
1027
|
+
return [...records.values()];
|
|
1028
|
+
}
|
|
923
1029
|
getLearningStats() {
|
|
924
|
-
return this.loadLearnings()
|
|
1030
|
+
return this.computeLearningStateStats(this.loadLearnings());
|
|
925
1031
|
}
|
|
926
1032
|
// ── Capability Gap Tracker ──────────────────────────────────────
|
|
927
1033
|
loadGaps() {
|
|
928
1034
|
return this.readFile('capability-gaps', {
|
|
929
1035
|
gaps: [],
|
|
930
|
-
stats: { totalGaps: 0, bySeverity: {}, byCategory: {}, addressed: 0, lastUpdated: this.now() },
|
|
1036
|
+
stats: { ...this.retentionStats('gaps', []), totalGaps: 0, bySeverity: {}, byCategory: {}, addressed: 0, lastUpdated: this.now() },
|
|
931
1037
|
});
|
|
932
1038
|
}
|
|
933
|
-
|
|
1039
|
+
computeGapStateStats(state) {
|
|
934
1040
|
const severityCounts = {};
|
|
935
1041
|
const categoryCounts = {};
|
|
936
1042
|
let addressed = 0;
|
|
@@ -940,28 +1046,22 @@ export class EvolutionManager {
|
|
|
940
1046
|
if (g.status === 'addressed')
|
|
941
1047
|
addressed++;
|
|
942
1048
|
}
|
|
943
|
-
|
|
1049
|
+
return {
|
|
1050
|
+
...this.retentionStats('gaps', state.gaps),
|
|
944
1051
|
totalGaps: state.gaps.length,
|
|
945
1052
|
bySeverity: severityCounts,
|
|
946
1053
|
byCategory: categoryCounts,
|
|
947
1054
|
addressed,
|
|
948
1055
|
lastUpdated: this.now(),
|
|
949
1056
|
};
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
const keep = closed.slice(-Math.max(0, max - open.length));
|
|
955
|
-
state.gaps = [...open, ...keep];
|
|
956
|
-
}
|
|
1057
|
+
}
|
|
1058
|
+
saveGaps(state) {
|
|
1059
|
+
state.gaps = this.retain('gaps', state.gaps);
|
|
1060
|
+
state.stats = this.computeGapStateStats(state);
|
|
957
1061
|
this.writeFile('capability-gaps', state);
|
|
958
1062
|
}
|
|
959
1063
|
nextGapId(state) {
|
|
960
|
-
|
|
961
|
-
let num = 1;
|
|
962
|
-
while (existing.has(`GAP-${String(num).padStart(3, '0')}`))
|
|
963
|
-
num++;
|
|
964
|
-
return `GAP-${String(num).padStart(3, '0')}`;
|
|
1064
|
+
return this.nextRetainedId('gaps', 'GAP', state.gaps);
|
|
965
1065
|
}
|
|
966
1066
|
addGap(opts) {
|
|
967
1067
|
const state = this.loadGaps();
|
|
@@ -1009,13 +1109,13 @@ export class EvolutionManager {
|
|
|
1009
1109
|
return gaps;
|
|
1010
1110
|
}
|
|
1011
1111
|
getGapStats() {
|
|
1012
|
-
return this.loadGaps()
|
|
1112
|
+
return this.computeGapStateStats(this.loadGaps());
|
|
1013
1113
|
}
|
|
1014
1114
|
// ── Action Queue ────────────────────────────────────────────────
|
|
1015
1115
|
loadActions() {
|
|
1016
1116
|
return this.readFile('action-queue', {
|
|
1017
1117
|
actions: [],
|
|
1018
|
-
stats: { totalActions: 0, pending: 0, completed: 0, overdue: 0, lastUpdated: this.now() },
|
|
1118
|
+
stats: { ...this.retentionStats('actions', []), totalActions: 0, pending: 0, completed: 0, overdue: 0, lastUpdated: this.now() },
|
|
1019
1119
|
});
|
|
1020
1120
|
}
|
|
1021
1121
|
/**
|
|
@@ -1098,23 +1198,18 @@ export class EvolutionManager {
|
|
|
1098
1198
|
* `readFile` would hand back an empty archive and the next write would erase it.
|
|
1099
1199
|
*/
|
|
1100
1200
|
loadActionArchive() {
|
|
1101
|
-
|
|
1102
|
-
if (!fs.existsSync(fp))
|
|
1103
|
-
return [];
|
|
1104
|
-
const parsed = JSON.parse(fs.readFileSync(fp, 'utf-8'));
|
|
1105
|
-
if (!Array.isArray(parsed.entries))
|
|
1106
|
-
throw new Error(`no entries array in ${fp}`);
|
|
1107
|
-
return parsed.entries;
|
|
1201
|
+
return this.archive.read().entries;
|
|
1108
1202
|
}
|
|
1109
1203
|
/** Returns false, having changed nothing, when the archive cannot be written. */
|
|
1110
1204
|
archiveActions(actions, items) {
|
|
1111
1205
|
try {
|
|
1112
1206
|
const archivedAt = this.now();
|
|
1113
1207
|
const byId = new Map(items.map((item) => [item.id, item]));
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
const entries = this.loadActionArchive().filter((entry) => entry.restoredAt || !ids.has(entry.action.id));
|
|
1208
|
+
// Preserve every version; identical retry snapshots are idempotent.
|
|
1209
|
+
const entries = this.loadActionArchive();
|
|
1117
1210
|
for (const action of actions) {
|
|
1211
|
+
if (entries.some(entry => !entry.restoredAt && this.emitFingerprint(entry.action) === this.emitFingerprint(action)))
|
|
1212
|
+
continue;
|
|
1118
1213
|
const item = byId.get(action.id);
|
|
1119
1214
|
entries.push({
|
|
1120
1215
|
action,
|
|
@@ -1124,7 +1219,7 @@ export class EvolutionManager {
|
|
|
1124
1219
|
...(item?.reason ? { reason: item.reason } : {}),
|
|
1125
1220
|
});
|
|
1126
1221
|
}
|
|
1127
|
-
this.
|
|
1222
|
+
this.archive.write({ ...this.archive.read(), entries });
|
|
1128
1223
|
return true;
|
|
1129
1224
|
}
|
|
1130
1225
|
catch (err) {
|
|
@@ -1156,7 +1251,7 @@ export class EvolutionManager {
|
|
|
1156
1251
|
*/
|
|
1157
1252
|
restoreArchivedAction(id) {
|
|
1158
1253
|
const entries = this.loadActionArchive();
|
|
1159
|
-
const entry = entries.find((candidate) => candidate.action.id === id && !candidate.restoredAt);
|
|
1254
|
+
const entry = [...entries].reverse().find((candidate) => candidate.action.id === id && !candidate.restoredAt);
|
|
1160
1255
|
if (!entry)
|
|
1161
1256
|
return 'not-archived';
|
|
1162
1257
|
const state = this.loadActions();
|
|
@@ -1166,11 +1261,17 @@ export class EvolutionManager {
|
|
|
1166
1261
|
const action = { ...entry.action, autoExpiryHold: { reason: 'restored from the auto-expiry archive', setAt: restoredAt } };
|
|
1167
1262
|
state.actions.push(action);
|
|
1168
1263
|
this.saveActions(state);
|
|
1169
|
-
|
|
1170
|
-
|
|
1264
|
+
// Retention may have archived another terminal action while saving the restored
|
|
1265
|
+
// row. Merge the marker into the fresh document; never replace its new history.
|
|
1266
|
+
const latest = this.archive.read();
|
|
1267
|
+
const restoredEntry = [...latest.entries].reverse().find(candidate => candidate.archivedAt === entry.archivedAt && this.emitFingerprint(candidate.action) === this.emitFingerprint(entry.action));
|
|
1268
|
+
if (!restoredEntry)
|
|
1269
|
+
throw new Error('restored archive snapshot unavailable');
|
|
1270
|
+
restoredEntry.restoredAt = restoredAt;
|
|
1271
|
+
this.archive.write(latest);
|
|
1171
1272
|
return action;
|
|
1172
1273
|
}
|
|
1173
|
-
|
|
1274
|
+
computeActionStateStats(state) {
|
|
1174
1275
|
let pending = 0, completed = 0, overdue = 0;
|
|
1175
1276
|
const now = new Date();
|
|
1176
1277
|
for (const a of state.actions) {
|
|
@@ -1182,57 +1283,31 @@ export class EvolutionManager {
|
|
|
1182
1283
|
overdue++;
|
|
1183
1284
|
}
|
|
1184
1285
|
}
|
|
1185
|
-
|
|
1286
|
+
return {
|
|
1287
|
+
...this.retentionStats('actions', state.actions),
|
|
1186
1288
|
totalActions: state.actions.length,
|
|
1187
1289
|
pending,
|
|
1188
1290
|
completed,
|
|
1189
1291
|
overdue,
|
|
1190
1292
|
lastUpdated: this.now(),
|
|
1191
1293
|
};
|
|
1192
|
-
|
|
1294
|
+
}
|
|
1295
|
+
saveActions(state) {
|
|
1193
1296
|
const beforePrune = state.actions;
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
const done = state.actions.filter(a => ['completed', 'cancelled'].includes(a.status));
|
|
1197
|
-
const keep = done.slice(-Math.max(0, max - active.length));
|
|
1198
|
-
state.actions = [...active, ...keep];
|
|
1199
|
-
}
|
|
1297
|
+
state.actions = this.retain('actions', state.actions);
|
|
1298
|
+
state.stats = this.computeActionStateStats(state);
|
|
1200
1299
|
this.writeFile('action-queue', state);
|
|
1201
|
-
//
|
|
1202
|
-
//
|
|
1203
|
-
// swallows its own errors, but we wrap defensively so a replication fault can NEVER
|
|
1204
|
-
// break a local action write. CRITICAL (fork #2): a STATUS CHANGE must re-emit — both
|
|
1205
|
-
// addAction and updateAction route through saveActions, so re-emitting every survivor on
|
|
1206
|
-
// every save makes a peer SEE an action's latest status (completed/in_progress) so it does
|
|
1207
|
-
// not redo it. CRITICAL (resurrection guard): an action that was actually REMOVED from the
|
|
1208
|
-
// queue (prune-over-maxActions) emits an op:delete tombstone — a terminal completed/
|
|
1209
|
-
// cancelled action that is RETAINED is NOT tombstoned, only an actual queue-removal is.
|
|
1300
|
+
// Preserve the final put (including status changes) without deletion tombstones.
|
|
1301
|
+
// Full-content custody and puts-only repair remain available from the archive.
|
|
1210
1302
|
const emitter = this.actionReplication;
|
|
1211
1303
|
if (emitter) {
|
|
1212
|
-
|
|
1213
|
-
const deletedAt = this.now();
|
|
1214
|
-
for (const pruned of beforePrune) {
|
|
1215
|
-
if (survivors.has(pruned.id))
|
|
1216
|
-
continue;
|
|
1217
|
-
try {
|
|
1218
|
-
emitter.emitDelete(pruned.title, pruned.commitTo, pruned.createdAt, deletedAt);
|
|
1219
|
-
}
|
|
1220
|
-
catch {
|
|
1221
|
-
// @silent-fallback-ok: a replication emit fault must never break or roll back a
|
|
1222
|
-
// local action write — the durable on-disk state is already persisted above. The
|
|
1223
|
-
// emitter counts its own failures internally; this guard only ensures a throw from
|
|
1224
|
-
// the seam cannot propagate into the local write path.
|
|
1225
|
-
}
|
|
1226
|
-
// A pruned id is gone from the queue; forget its fingerprint so the map does not
|
|
1227
|
-
// grow unbounded and a future id-reuse re-emits.
|
|
1228
|
-
this.lastEmittedActionFp.delete(pruned.id);
|
|
1229
|
-
}
|
|
1304
|
+
// Hot-list eviction preserves memory; it never emits deletion authority.
|
|
1230
1305
|
// Standby-write-wedge fix: re-emit ONLY records whose content changed since their last
|
|
1231
1306
|
// emit. Re-emitting every unchanged survivor (the old behavior) turned each write into
|
|
1232
1307
|
// O(records × journalBytes) synchronous journal scans (loadWitness materializes the
|
|
1233
1308
|
// whole store per emit) — the event-loop wedge. A status change alters the fingerprint,
|
|
1234
1309
|
// so the load-bearing "a peer sees the latest status" property is preserved.
|
|
1235
|
-
for (const a of
|
|
1310
|
+
for (const a of beforePrune) {
|
|
1236
1311
|
const fp = this.emitFingerprint(a);
|
|
1237
1312
|
if (this.lastEmittedActionFp.get(a.id) === fp)
|
|
1238
1313
|
continue; // unchanged ⇒ skip re-emit
|
|
@@ -1241,7 +1316,7 @@ export class EvolutionManager {
|
|
|
1241
1316
|
this.lastEmittedActionFp.set(a.id, fp); // record only after a successful emit
|
|
1242
1317
|
}
|
|
1243
1318
|
catch {
|
|
1244
|
-
// @silent-fallback-ok:
|
|
1319
|
+
// @silent-fallback-ok: replication remains best-effort — replication is best-effort
|
|
1245
1320
|
// and must never break the local write. The fingerprint is NOT recorded on a throw
|
|
1246
1321
|
// so the next save retries this record.
|
|
1247
1322
|
}
|
|
@@ -1249,21 +1324,7 @@ export class EvolutionManager {
|
|
|
1249
1324
|
}
|
|
1250
1325
|
}
|
|
1251
1326
|
nextActionId(state) {
|
|
1252
|
-
|
|
1253
|
-
// damaged archive must not stop new actions: restore refuses a colliding id anyway.
|
|
1254
|
-
let archivedIds = [];
|
|
1255
|
-
try {
|
|
1256
|
-
archivedIds = this.loadActionArchive().map(e => e.action.id);
|
|
1257
|
-
}
|
|
1258
|
-
catch (err) {
|
|
1259
|
-
// @silent-fallback-ok: the sweep refuses on the same damage; id reservation is a courtesy to restore.
|
|
1260
|
-
console.warn(`[EvolutionManager] Action archive unreadable, ids not reserved: ${err instanceof Error ? err.message : String(err)}`);
|
|
1261
|
-
}
|
|
1262
|
-
const existing = new Set([...state.actions.map(a => a.id), ...archivedIds]);
|
|
1263
|
-
let num = 1;
|
|
1264
|
-
while (existing.has(`ACT-${String(num).padStart(3, '0')}`))
|
|
1265
|
-
num++;
|
|
1266
|
-
return `ACT-${String(num).padStart(3, '0')}`;
|
|
1327
|
+
return this.nextRetainedId('actions', 'ACT', state.actions);
|
|
1267
1328
|
}
|
|
1268
1329
|
addAction(opts) {
|
|
1269
1330
|
const state = this.loadActions();
|
|
@@ -1320,7 +1381,7 @@ export class EvolutionManager {
|
|
|
1320
1381
|
a.dueBy && new Date(a.dueBy) < now);
|
|
1321
1382
|
}
|
|
1322
1383
|
getActionStats() {
|
|
1323
|
-
return this.loadActions()
|
|
1384
|
+
return this.computeActionStateStats(this.loadActions());
|
|
1324
1385
|
}
|
|
1325
1386
|
// ── Cross-System Queries ────────────────────────────────────────
|
|
1326
1387
|
/**
|