archgraph-argo 0.10.31 → 0.10.33
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/argo/scripts/argo-mcp-server.js +1 -0
- package/argo/scripts/graph-rag/defaultSemanticRetrieval.js +74 -5
- package/argo/scripts/graph-rag/liveEmbeddingIndexGate.js +2 -2
- package/argo/scripts/graph-rag/liveEmbeddingNeo4jBoundary.js +2 -2
- package/argo/scripts/graph-rag/liveEmbeddingProviderConfig.js +46 -10
- package/argo/scripts/graph-rag/mutationEmbeddingVectorLifecycle.js +1 -1
- package/argo/scripts/graph-rag/semantic-persistence/productionSemanticBackfill.js +41 -0
- package/argo/scripts/graph-rag/semantic-persistence/productionSemanticNeo4jAdapter.js +18 -1
- package/argo/scripts/systemarchitecture-mcp-server.js +138 -4
- package/package.json +1 -1
|
@@ -36,7 +36,7 @@ const APPROVED_PROFILE = Object.freeze({
|
|
|
36
36
|
model: 'qwen3.7-text-embedding',
|
|
37
37
|
provider: 'alibaba-cloud-model-studio-openai-compatible-cn-beijing',
|
|
38
38
|
version: 'qualification-2026-07-25',
|
|
39
|
-
dimensions:
|
|
39
|
+
dimensions: 1536,
|
|
40
40
|
});
|
|
41
41
|
const CHANNELS = Object.freeze([
|
|
42
42
|
Object.freeze({
|
|
@@ -80,7 +80,37 @@ const READINESS_QUERY_CYPHER = [
|
|
|
80
80
|
'RETURN properties(readiness) AS readiness',
|
|
81
81
|
].join('\n');
|
|
82
82
|
const INITIAL_WINDOW_SIZE = 2;
|
|
83
|
+
// Purpose-aware, env-configurable similarity thresholds + bounded top-K recall.
|
|
84
|
+
// Audit keeps the strict threshold (precision); memory-retrieval purposes use a
|
|
85
|
+
// looser threshold so relevant-but-paraphrased memory is recalled, while results
|
|
86
|
+
// are bounded to ARGO_SEMANTIC_TOP_K so noise stays bounded. Thresholds are
|
|
87
|
+
// overridable per channel via ARGO_SEMANTIC_{MEMORY|AUDIT}_THRESHOLD_<CHANNEL>.
|
|
88
|
+
const AUDIT_PURPOSES = new Set(['audit']);
|
|
89
|
+
const DEFAULT_MEMORY_THRESHOLD = 0.55;
|
|
90
|
+
const DEFAULT_AUDIT_THRESHOLD = 0.8;
|
|
91
|
+
const DEFAULT_TOP_K = 8;
|
|
83
92
|
const SELECTED_VIEW_ID = 'semprod-wp2-default-retrieval-readiness';
|
|
93
|
+
|
|
94
|
+
function envNumber(key, fallback) {
|
|
95
|
+
const value = Number(process.env[key]);
|
|
96
|
+
return Number.isFinite(value) ? value : fallback;
|
|
97
|
+
}
|
|
98
|
+
function memoryThresholdFor(channel) {
|
|
99
|
+
return envNumber(
|
|
100
|
+
`ARGO_SEMANTIC_MEMORY_THRESHOLD_${channel.channel.toUpperCase()}`,
|
|
101
|
+
envNumber('ARGO_SEMANTIC_MEMORY_THRESHOLD', DEFAULT_MEMORY_THRESHOLD),
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
function auditThresholdFor(channel) {
|
|
105
|
+
return envNumber(
|
|
106
|
+
`ARGO_SEMANTIC_AUDIT_THRESHOLD_${channel.channel.toUpperCase()}`,
|
|
107
|
+
envNumber('ARGO_SEMANTIC_AUDIT_THRESHOLD', DEFAULT_AUDIT_THRESHOLD),
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
function resolveTopK() {
|
|
111
|
+
const k = envNumber('ARGO_SEMANTIC_TOP_K', DEFAULT_TOP_K);
|
|
112
|
+
return Number.isInteger(k) && k > 0 ? k : DEFAULT_TOP_K;
|
|
113
|
+
}
|
|
84
114
|
const testCompositionStorage = new AsyncLocalStorage();
|
|
85
115
|
|
|
86
116
|
function createDefaultSemanticRetrieval(dependencies = {}) {
|
|
@@ -173,12 +203,17 @@ async function executeWpP2Retrieval({
|
|
|
173
203
|
});
|
|
174
204
|
const vector = await provider.embed(request.intent);
|
|
175
205
|
requireQualifiedVector(vector);
|
|
206
|
+
const purpose = request && typeof request.purpose === 'string' ? request.purpose : '';
|
|
207
|
+
const strict = AUDIT_PURPOSES.has(purpose);
|
|
208
|
+
const topK = resolveTopK();
|
|
176
209
|
const seedsByType = {};
|
|
177
210
|
for (const channel of CHANNELS) {
|
|
178
211
|
seedsByType[channel.key] = await exhaustChannel({
|
|
179
212
|
channel,
|
|
180
213
|
neo4jDriver: composition.neo4jDriver,
|
|
181
214
|
vector,
|
|
215
|
+
threshold: strict ? auditThresholdFor(channel) : memoryThresholdFor(channel),
|
|
216
|
+
maxSeeds: topK,
|
|
182
217
|
...(Array.isArray(canonicalIdentities) && canonicalIdentities.length > 0
|
|
183
218
|
? { canonicalIdentities }
|
|
184
219
|
: {}),
|
|
@@ -620,12 +655,24 @@ function publicReadinessOutcome(alignment) {
|
|
|
620
655
|
};
|
|
621
656
|
}
|
|
622
657
|
|
|
623
|
-
async function exhaustChannel({
|
|
658
|
+
async function exhaustChannel({
|
|
659
|
+
channel,
|
|
660
|
+
neo4jDriver,
|
|
661
|
+
vector,
|
|
662
|
+
canonicalIdentities,
|
|
663
|
+
threshold,
|
|
664
|
+
maxSeeds,
|
|
665
|
+
}) {
|
|
624
666
|
const scoped = Array.isArray(canonicalIdentities) && canonicalIdentities.length > 0;
|
|
667
|
+
const scopedCanonicalIdentities = scoped
|
|
668
|
+
? scopeCanonicalIdentitiesForChannel(canonicalIdentities, channel)
|
|
669
|
+
: canonicalIdentities;
|
|
625
670
|
const accepted = [];
|
|
626
671
|
const seen = new Set();
|
|
672
|
+
const effectiveThreshold = typeof threshold === 'number' ? threshold : channel.threshold;
|
|
673
|
+
const effectiveMax = Number.isInteger(maxSeeds) && maxSeeds > 0 ? maxSeeds : Number.POSITIVE_INFINITY;
|
|
627
674
|
let offset = 0;
|
|
628
|
-
while (
|
|
675
|
+
while (accepted.length < effectiveMax) {
|
|
629
676
|
const parameters = Object.freeze({
|
|
630
677
|
indexName: channel.indexName,
|
|
631
678
|
channel: channel.channel,
|
|
@@ -633,7 +680,7 @@ async function exhaustChannel({ channel, neo4jDriver, vector, canonicalIdentitie
|
|
|
633
680
|
windowSize: INITIAL_WINDOW_SIZE,
|
|
634
681
|
topK: offset + INITIAL_WINDOW_SIZE,
|
|
635
682
|
vector,
|
|
636
|
-
...(scoped ? { canonicalIdentities } : {}),
|
|
683
|
+
...(scoped ? { canonicalIdentities: scopedCanonicalIdentities } : {}),
|
|
637
684
|
});
|
|
638
685
|
const result = await neo4jDriver.execute(Object.freeze({
|
|
639
686
|
kind: 'semantic-vector-window-query',
|
|
@@ -645,8 +692,9 @@ async function exhaustChannel({ channel, neo4jDriver, vector, canonicalIdentitie
|
|
|
645
692
|
const records = Array.isArray(result && result.records) ? result.records : [];
|
|
646
693
|
const newlyVisible = records.slice(offset);
|
|
647
694
|
for (const raw of newlyVisible) {
|
|
695
|
+
if (accepted.length >= effectiveMax) break;
|
|
648
696
|
const record = normalizeVectorRecord(raw, channel);
|
|
649
|
-
if (record && record.score >=
|
|
697
|
+
if (record && record.score >= effectiveThreshold && !seen.has(record.id)) {
|
|
650
698
|
seen.add(record.id);
|
|
651
699
|
accepted.push(Object.freeze(record));
|
|
652
700
|
}
|
|
@@ -661,6 +709,22 @@ async function exhaustChannel({ channel, neo4jDriver, vector, canonicalIdentitie
|
|
|
661
709
|
return Object.freeze(accepted);
|
|
662
710
|
}
|
|
663
711
|
|
|
712
|
+
// Scope identities come from the canonical graph as BARE ids (e.g.
|
|
713
|
+
// "memory-eval-bench-wp-001"), but the semantic vector records store their
|
|
714
|
+
// canonicalIdentity with a channel prefix (e.g. "Element:memory-eval-bench-wp-001").
|
|
715
|
+
// Normalise the scope to the channel-prefixed form so the scoped Cypher
|
|
716
|
+
// `node.canonicalIdentity IN $canonicalIdentities` actually matches. A bare id
|
|
717
|
+
// that already carries a prefix (this or another channel's) is left as-is: it
|
|
718
|
+
// simply won't match records of this channel, which is the correct no-op.
|
|
719
|
+
function scopeCanonicalIdentitiesForChannel(identities, channel) {
|
|
720
|
+
if (!Array.isArray(identities)) return identities;
|
|
721
|
+
const prefix = `${channel.objectType}:`;
|
|
722
|
+
return identities.map(id => {
|
|
723
|
+
const text = String(id);
|
|
724
|
+
return text.includes(':') ? text : `${prefix}${text}`;
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
|
|
664
728
|
function normalizeVectorRecord(raw, channel) {
|
|
665
729
|
if (!raw || typeof raw !== 'object') return undefined;
|
|
666
730
|
const id = raw.canonicalIdentity || raw.id || raw.objectId;
|
|
@@ -979,4 +1043,9 @@ function safeError(category) {
|
|
|
979
1043
|
module.exports = {
|
|
980
1044
|
createDefaultSemanticRetrieval,
|
|
981
1045
|
withDefaultSemanticRetrievalTestComposition,
|
|
1046
|
+
memoryThresholdFor,
|
|
1047
|
+
auditThresholdFor,
|
|
1048
|
+
resolveTopK,
|
|
1049
|
+
scopeCanonicalIdentitiesForChannel,
|
|
1050
|
+
AUDIT_PURPOSES,
|
|
982
1051
|
};
|
|
@@ -46,7 +46,7 @@ function createLiveEmbeddingIndexGate(dependencies = {}) {
|
|
|
46
46
|
requireInput(input);
|
|
47
47
|
const vector = await client.embed(input.input);
|
|
48
48
|
if (
|
|
49
|
-
vector.length !==
|
|
49
|
+
vector.length !== 1536
|
|
50
50
|
|| vector.some(value => typeof value !== 'number' || !Number.isFinite(value))
|
|
51
51
|
) {
|
|
52
52
|
throw safeError('LIVE_PROVIDER_RESPONSE_INVALID');
|
|
@@ -79,7 +79,7 @@ function requireApprovedConfiguration(configuration, qualification) {
|
|
|
79
79
|
|| qualification.model !== configuration.embeddingModel
|
|
80
80
|
|| qualification.version !== configuration.embeddingModelVersion
|
|
81
81
|
|| qualification.dimensions !== configuration.embeddingDimensions
|
|
82
|
-
|| qualification.dimensions !==
|
|
82
|
+
|| qualification.dimensions !== 1536
|
|
83
83
|
) {
|
|
84
84
|
throw safeError('LIVE_PROVIDER_INDEX_WRITE_PROHIBITED');
|
|
85
85
|
}
|
|
@@ -64,11 +64,11 @@ async function createApprovedNeo4jBoundary({ configuration, neo4j, logger }) {
|
|
|
64
64
|
return result.records.map(record => normalizeNeo4jValue(record.get('evidence')));
|
|
65
65
|
},
|
|
66
66
|
async queryVectorEvidence(runId, vector, canonicalIdentities) {
|
|
67
|
-
if (!Array.isArray(vector) || vector.length !==
|
|
67
|
+
if (!Array.isArray(vector) || vector.length !== 1536 || !Array.isArray(canonicalIdentities)) {
|
|
68
68
|
throw safeError('LIVE_PROVIDER_OPERATION_FAILED');
|
|
69
69
|
}
|
|
70
70
|
await query(
|
|
71
|
-
'CREATE VECTOR INDEX argo_live_embedding_vector IF NOT EXISTS FOR (e:ArgoLiveEmbeddingEvidence) ON (e.vector) OPTIONS { indexConfig: { `vector.dimensions`:
|
|
71
|
+
'CREATE VECTOR INDEX argo_live_embedding_vector IF NOT EXISTS FOR (e:ArgoLiveEmbeddingEvidence) ON (e.vector) OPTIONS { indexConfig: { `vector.dimensions`: 1536, `vector.similarity_function`: "cosine" } }',
|
|
72
72
|
{},
|
|
73
73
|
);
|
|
74
74
|
const result = await query(
|
|
@@ -21,6 +21,20 @@ const CONFIG_KEYS = Object.freeze([
|
|
|
21
21
|
const OPTIONAL_CONFIG_KEYS = Object.freeze([
|
|
22
22
|
'ARGO_NEO4J_DATABASE',
|
|
23
23
|
]);
|
|
24
|
+
// Semantic-retrieval tuning knobs (purpose-aware thresholds + bounded top-K).
|
|
25
|
+
// They are read by defaultSemanticRetrieval from process.env and whitelisted here
|
|
26
|
+
// so an approved env file may carry them without tripping SECRET_FILE_UNKNOWN_KEY.
|
|
27
|
+
const RETRIEVAL_TUNING_KEYS = Object.freeze([
|
|
28
|
+
'ARGO_SEMANTIC_MEMORY_THRESHOLD',
|
|
29
|
+
'ARGO_SEMANTIC_MEMORY_THRESHOLD_ELEMENT',
|
|
30
|
+
'ARGO_SEMANTIC_MEMORY_THRESHOLD_RELATIONSHIP',
|
|
31
|
+
'ARGO_SEMANTIC_MEMORY_THRESHOLD_VIEW',
|
|
32
|
+
'ARGO_SEMANTIC_AUDIT_THRESHOLD',
|
|
33
|
+
'ARGO_SEMANTIC_AUDIT_THRESHOLD_ELEMENT',
|
|
34
|
+
'ARGO_SEMANTIC_AUDIT_THRESHOLD_RELATIONSHIP',
|
|
35
|
+
'ARGO_SEMANTIC_AUDIT_THRESHOLD_VIEW',
|
|
36
|
+
'ARGO_SEMANTIC_TOP_K',
|
|
37
|
+
]);
|
|
24
38
|
const OPT_IN_KEYS = Object.freeze({
|
|
25
39
|
ARGO_LIVE_PROVIDER_E2E: 'LIVE_PROVIDER_E2E_OPT_IN_REQUIRED',
|
|
26
40
|
ARGO_W31_LIVE_MUTATION_VECTOR_E2E: 'W31_MUTATION_VECTOR_E2E_OPT_IN_REQUIRED',
|
|
@@ -28,6 +42,7 @@ const OPT_IN_KEYS = Object.freeze({
|
|
|
28
42
|
const READABLE_KEYS = Object.freeze([
|
|
29
43
|
...CONFIG_KEYS,
|
|
30
44
|
...OPTIONAL_CONFIG_KEYS,
|
|
45
|
+
...RETRIEVAL_TUNING_KEYS,
|
|
31
46
|
...Object.keys(OPT_IN_KEYS),
|
|
32
47
|
]);
|
|
33
48
|
const LEGACY_KEYS = Object.freeze(['ARGO_NEO4J_URI', 'ARGO_NEO4J_USERNAME', 'ARGO_NEO4J_PASSWORD']);
|
|
@@ -38,7 +53,7 @@ const APPROVED = Object.freeze({
|
|
|
38
53
|
ARGO_EMBEDDING_MODEL: 'qwen3.7-text-embedding',
|
|
39
54
|
ARGO_EMBEDDING_PROVIDER: 'alibaba-cloud-model-studio-openai-compatible-cn-beijing',
|
|
40
55
|
ARGO_EMBEDDING_MODEL_VERSION: 'qualification-2026-07-25',
|
|
41
|
-
ARGO_EMBEDDING_DIMENSIONS: '
|
|
56
|
+
ARGO_EMBEDDING_DIMENSIONS: '1536',
|
|
42
57
|
});
|
|
43
58
|
const issuedAdapters = new WeakSet();
|
|
44
59
|
const issuedTraces = new WeakSet();
|
|
@@ -167,7 +182,7 @@ async function resolveTrusted({
|
|
|
167
182
|
embeddingModel: normalized.ARGO_EMBEDDING_MODEL,
|
|
168
183
|
embeddingProvider: normalized.ARGO_EMBEDDING_PROVIDER,
|
|
169
184
|
embeddingModelVersion: normalized.ARGO_EMBEDDING_MODEL_VERSION,
|
|
170
|
-
embeddingDimensions:
|
|
185
|
+
embeddingDimensions: 1536,
|
|
171
186
|
neo4jDatabaseUrl: normalized.ARGO_NEO4J_DATABASE_URL,
|
|
172
187
|
neo4jDatabaseUsername: normalized.ARGO_NEO4J_DATABASE_USERNAME,
|
|
173
188
|
neo4jDatabasePassword: normalized.ARGO_NEO4J_DATABASE_PASSWORD,
|
|
@@ -342,13 +357,15 @@ function preflightFile({ canonicalFilePath, configuredFilePath, filesystem, adap
|
|
|
342
357
|
const insideRepository = adapters.systemMetadata.isSecretFileInsideGitRepository().status === 0;
|
|
343
358
|
ignored = insideRepository ? adapters.systemMetadata.isSecretFileIgnored() : true;
|
|
344
359
|
tracked = insideRepository ? adapters.systemMetadata.isSecretFileTracked() : false;
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
360
|
+
if (process.platform === 'win32') {
|
|
361
|
+
const identityResult = adapters.systemMetadata.readCurrentIdentity();
|
|
362
|
+
const aclResult = adapters.systemMetadata.readSecretFileAcl();
|
|
363
|
+
aclEvidence = {
|
|
364
|
+
status: aclResult.status,
|
|
365
|
+
stdout: aclResult.stdout,
|
|
366
|
+
identity: identityResult.status === 0 ? identityResult.stdout.trim() : '',
|
|
367
|
+
};
|
|
368
|
+
}
|
|
352
369
|
} else {
|
|
353
370
|
ignored = adapters.git.isIgnored(configuredFilePath);
|
|
354
371
|
tracked = adapters.git.isTracked(configuredFilePath);
|
|
@@ -356,7 +373,11 @@ function preflightFile({ canonicalFilePath, configuredFilePath, filesystem, adap
|
|
|
356
373
|
}
|
|
357
374
|
if (tracked) throw safeError('SECRET_FILE_TRACKED');
|
|
358
375
|
if (!ignored) throw safeError('SECRET_FILE_NOT_IGNORED');
|
|
359
|
-
|
|
376
|
+
if (process.platform === 'win32') {
|
|
377
|
+
validateAcl(aclEvidence);
|
|
378
|
+
} else {
|
|
379
|
+
validatePosixSecretFileAcl(configuredFilePath, filesystem);
|
|
380
|
+
}
|
|
360
381
|
}
|
|
361
382
|
|
|
362
383
|
function validateAcl(result) {
|
|
@@ -389,6 +410,20 @@ function parseAcl(output) {
|
|
|
389
410
|
return result;
|
|
390
411
|
}
|
|
391
412
|
|
|
413
|
+
function posixModeIsSecretSafe(mode) {
|
|
414
|
+
return (mode & 0o077) === 0 && (mode & 0o600) === 0o600;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function validatePosixSecretFileAcl(configuredFilePath, filesystem) {
|
|
418
|
+
const stat = filesystem.lstatSync(configuredFilePath);
|
|
419
|
+
if (!posixModeIsSecretSafe(stat.mode)) {
|
|
420
|
+
throw safeError('SECRET_FILE_ACL_UNSAFE');
|
|
421
|
+
}
|
|
422
|
+
if (typeof process.getuid === 'function' && stat.uid !== process.getuid()) {
|
|
423
|
+
throw safeError('SECRET_FILE_ACL_UNSAFE');
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
392
427
|
function productionSourceBehavior(repositoryRoot) {
|
|
393
428
|
return Object.freeze({
|
|
394
429
|
expectedFilePath: getArgoEnvPath(),
|
|
@@ -478,4 +513,5 @@ function safeError(category) {
|
|
|
478
513
|
module.exports = {
|
|
479
514
|
resolveApprovedLiveConfiguration,
|
|
480
515
|
withApprovedLiveConfigurationTestComposition,
|
|
516
|
+
posixModeIsSecretSafe,
|
|
481
517
|
};
|
|
@@ -1143,7 +1143,7 @@ function createSyntheticVectorTransport() {
|
|
|
1143
1143
|
return {
|
|
1144
1144
|
data: [
|
|
1145
1145
|
{
|
|
1146
|
-
embedding: Array.from({ length:
|
|
1146
|
+
embedding: Array.from({ length: 1536 }, (_, index) => index / 2048),
|
|
1147
1147
|
},
|
|
1148
1148
|
],
|
|
1149
1149
|
};
|
|
@@ -15,6 +15,8 @@ function createProductionSemanticBackfill(dependencies = {}) {
|
|
|
15
15
|
requireBoundary(dependencies.structuralProjection, 'requireComplete', 'structuralProjection');
|
|
16
16
|
requireBoundary(dependencies.embeddingProvider, 'embedBatch', 'embeddingProvider');
|
|
17
17
|
requireBoundary(dependencies.projectionStore, 'upsertRecords', 'projectionStore');
|
|
18
|
+
requireBoundary(dependencies.projectionStore, 'readRecords', 'projectionStore');
|
|
19
|
+
requireBoundary(dependencies.projectionStore, 'deleteTombstones', 'projectionStore');
|
|
18
20
|
requireBoundary(dependencies.checkpointStore, 'readCheckpoint', 'checkpointStore');
|
|
19
21
|
requireBoundary(dependencies.checkpointStore, 'writeCheckpoint', 'checkpointStore');
|
|
20
22
|
const batchSize = dependencies.batchSize;
|
|
@@ -55,6 +57,11 @@ function createProductionSemanticBackfill(dependencies = {}) {
|
|
|
55
57
|
checkpointStore: dependencies.checkpointStore,
|
|
56
58
|
});
|
|
57
59
|
}
|
|
60
|
+
const reconciled = await reconcileRemovals({
|
|
61
|
+
snapshot,
|
|
62
|
+
canonicalVersion,
|
|
63
|
+
projectionStore: dependencies.projectionStore,
|
|
64
|
+
});
|
|
58
65
|
const aligned = CHANNELS.every(channel => (
|
|
59
66
|
channels[channel].status === 'complete'
|
|
60
67
|
&& channels[channel].canonicalVersion === canonicalVersion
|
|
@@ -64,6 +71,7 @@ function createProductionSemanticBackfill(dependencies = {}) {
|
|
|
64
71
|
canonicalVersion,
|
|
65
72
|
alignmentState: aligned ? 'Aligned' : 'Updating',
|
|
66
73
|
channels: Object.freeze(channels),
|
|
74
|
+
reconciled,
|
|
67
75
|
});
|
|
68
76
|
},
|
|
69
77
|
});
|
|
@@ -204,6 +212,39 @@ function mergeFailures(existing, current) {
|
|
|
204
212
|
return [...merged.values()];
|
|
205
213
|
}
|
|
206
214
|
|
|
215
|
+
// Full-reconciliation removal pass: after all channels are upserted, delete any
|
|
216
|
+
// persisted semantic record whose canonical identity no longer exists in the
|
|
217
|
+
// local JSON snapshot. This guarantees argo-init rebuilds a semantic projection
|
|
218
|
+
// that is exactly consistent with the canonical graph (no stale vectors).
|
|
219
|
+
async function reconcileRemovals({ snapshot, canonicalVersion, projectionStore }) {
|
|
220
|
+
const currentByChannel = new Map();
|
|
221
|
+
for (const channel of CHANNELS) {
|
|
222
|
+
const source = CHANNEL_SOURCES[channel];
|
|
223
|
+
const identities = new Set(
|
|
224
|
+
(snapshot[source.property] || []).map(record => `${channel}:${source.identity(record)}`),
|
|
225
|
+
);
|
|
226
|
+
currentByChannel.set(channel, identities);
|
|
227
|
+
}
|
|
228
|
+
const stored = await projectionStore.readRecords();
|
|
229
|
+
const allStored = Array.isArray(stored) ? stored : [];
|
|
230
|
+
const stale = allStored.filter(record => {
|
|
231
|
+
const identities = currentByChannel.get(record.channel);
|
|
232
|
+
return identities && !identities.has(record.canonicalIdentity);
|
|
233
|
+
});
|
|
234
|
+
const tombstones = stale.map(record => Object.freeze({
|
|
235
|
+
canonicalIdentity: record.canonicalIdentity,
|
|
236
|
+
channel: record.channel,
|
|
237
|
+
canonicalVersion: record.canonicalVersion || canonicalVersion,
|
|
238
|
+
}));
|
|
239
|
+
if (tombstones.length > 0) {
|
|
240
|
+
await projectionStore.deleteTombstones(tombstones);
|
|
241
|
+
}
|
|
242
|
+
return Object.freeze({
|
|
243
|
+
scanned: allStored.length,
|
|
244
|
+
tombstoned: tombstones.length,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
207
248
|
function requireBoundary(boundary, method, name) {
|
|
208
249
|
if (!boundary || typeof boundary[method] !== 'function') {
|
|
209
250
|
throw new TypeError(`${name}.${method} is required`);
|
|
@@ -90,14 +90,31 @@ function createProductionSemanticNeo4jAdapter(dependencies = {}) {
|
|
|
90
90
|
});
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
const EMBEDDING_DIMENSIONS = 1536;
|
|
94
|
+
|
|
93
95
|
async function ensureVectorIndexes(session) {
|
|
94
96
|
for (const definition of Object.values(CHANNEL_INDEXES)) {
|
|
97
|
+
// CREATE ... IF NOT EXISTS cannot change an existing index's dimensions, so
|
|
98
|
+
// when the approved embedding dimension changes (e.g. 1024 -> 1536) drop the
|
|
99
|
+
// stale index first and recreate it at the approved dimension.
|
|
100
|
+
const existing = await executeRead(
|
|
101
|
+
session,
|
|
102
|
+
'SHOW INDEXES YIELD name, type, options WHERE type = \'VECTOR\' AND name = $name RETURN name, options',
|
|
103
|
+
{ name: definition.indexName },
|
|
104
|
+
);
|
|
105
|
+
if (existing.records.length > 0) {
|
|
106
|
+
const options = existing.records[0].get('options');
|
|
107
|
+
const dim = options && options.indexConfig && options.indexConfig['vector.dimensions'];
|
|
108
|
+
if (dim !== EMBEDDING_DIMENSIONS) {
|
|
109
|
+
await executeWrite(session, `DROP INDEX ${definition.indexName} IF EXISTS`, {});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
95
112
|
await executeWrite(
|
|
96
113
|
session,
|
|
97
114
|
[
|
|
98
115
|
`CREATE VECTOR INDEX ${definition.indexName} IF NOT EXISTS`,
|
|
99
116
|
`FOR (semantic:${definition.label}) ON (semantic.vector)`,
|
|
100
|
-
|
|
117
|
+
`OPTIONS { indexConfig: { \`vector.dimensions\`: ${EMBEDDING_DIMENSIONS}, \`vector.similarity_function\`: "cosine" } }`,
|
|
101
118
|
].join('\n'),
|
|
102
119
|
{},
|
|
103
120
|
);
|
|
@@ -128,7 +128,7 @@ const W31_APPROVED_PROFILE = Object.freeze({
|
|
|
128
128
|
baseUrl: 'https://llm-clids9mqc5o1mbvb.cn-beijing.maas.aliyuncs.com/compatible-mode/v1',
|
|
129
129
|
model: 'qwen3.7-text-embedding',
|
|
130
130
|
version: 'qualification-2026-07-25',
|
|
131
|
-
dimensions:
|
|
131
|
+
dimensions: 1536,
|
|
132
132
|
source: 'explicit-human-approval',
|
|
133
133
|
});
|
|
134
134
|
|
|
@@ -406,6 +406,20 @@ const TOOLS = [
|
|
|
406
406
|
additionalProperties: false,
|
|
407
407
|
},
|
|
408
408
|
},
|
|
409
|
+
{
|
|
410
|
+
name: 'memory_search',
|
|
411
|
+
description: 'Semantic memory search: retrieve the user\'s memory by natural-language query. Runs embedding-based semantic retrieval (memory-retrieval purpose) over the intent graph and returns the top-k relevant memory items (id, name, type, similarity score, and a description excerpt with its full length). Each hit is a lightweight card: to keep the tool result compact, description is truncated to max_desc_len characters (default 800) and the full text length is reported as description_length. To read the full text of a hit, call getIntentElementContext with its id. Agents SHOULD call this tool to look up the user\'s memory before answering a question about the user.',
|
|
412
|
+
inputSchema: {
|
|
413
|
+
type: 'object',
|
|
414
|
+
properties: {
|
|
415
|
+
query: { type: 'string', description: 'Natural-language query describing the user memory to retrieve.' },
|
|
416
|
+
top_k: { type: 'integer', description: 'Optional max number of hits to return (default 8).' },
|
|
417
|
+
max_desc_len: { type: 'integer', description: 'Optional max characters of description to return per hit. Default 800. Use 0 for the full description, or -1 to omit description and return only the length.' },
|
|
418
|
+
architecturePath: { type: 'string', description: `Default: ${DEFAULT_GRAPH_PATH}` },
|
|
419
|
+
},
|
|
420
|
+
additionalProperties: false,
|
|
421
|
+
},
|
|
422
|
+
},
|
|
409
423
|
];
|
|
410
424
|
|
|
411
425
|
// Every tool accepts an optional per-call `workspaceRoot` (absolute path,
|
|
@@ -2188,9 +2202,91 @@ async function callTool(name, args = {}, dependencies = undefined) {
|
|
|
2188
2202
|
return queryNeo4jGraphTool(args);
|
|
2189
2203
|
}
|
|
2190
2204
|
|
|
2205
|
+
if (name === 'memory_search') {
|
|
2206
|
+
return memorySearchTool(args, dependencies);
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2191
2209
|
throw new Error(`Unknown tool: ${name}`);
|
|
2192
2210
|
}
|
|
2193
2211
|
|
|
2212
|
+
// memory_search: natural-language semantic memory retrieval. Reuses the
|
|
2213
|
+
// memory-retrieval purpose (implementation-design) of the semantic journey
|
|
2214
|
+
// (same path as getSystemArchitecture, so readiness/closure behave identically)
|
|
2215
|
+
// and reshapes the retrieved elements into a memory-oriented hit list with
|
|
2216
|
+
// content + similarity score, so an agent can look up the user's memory in one
|
|
2217
|
+
// call.
|
|
2218
|
+
async function memorySearchTool(args = {}, dependencies = undefined) {
|
|
2219
|
+
const query = typeof args.query === 'string' ? args.query.trim() : '';
|
|
2220
|
+
if (!query) {
|
|
2221
|
+
return { status: 'failed', error: { category: 'MEMORY_QUERY_REQUIRED', message: 'query is required' } };
|
|
2222
|
+
}
|
|
2223
|
+
const topK = Number.isInteger(args.top_k) && args.top_k > 0 ? args.top_k : 8;
|
|
2224
|
+
// max_desc_len: default 800 (compact excerpt). 0 = full description;
|
|
2225
|
+
// -1 = omit description entirely and return only the length. Keeps the tool
|
|
2226
|
+
// result compact (production memory cards, not full session dumps).
|
|
2227
|
+
let maxDescLen = 800;
|
|
2228
|
+
if (Number.isInteger(args.max_desc_len) && args.max_desc_len !== 800) {
|
|
2229
|
+
maxDescLen = args.max_desc_len;
|
|
2230
|
+
}
|
|
2231
|
+
const context = await loadContext(args);
|
|
2232
|
+
let retrieved;
|
|
2233
|
+
try {
|
|
2234
|
+
const journey = await resolveSemanticOperatorJourney(dependencies);
|
|
2235
|
+
retrieved = await journey.query({ purpose: 'implementation-design', intent: query });
|
|
2236
|
+
} catch (error) {
|
|
2237
|
+
return {
|
|
2238
|
+
status: 'failed',
|
|
2239
|
+
error: {
|
|
2240
|
+
category: error && error.category ? error.category : 'MEMORY_RETRIEVAL_FAILED',
|
|
2241
|
+
message: error && error.message ? error.message : 'Memory retrieval failed',
|
|
2242
|
+
},
|
|
2243
|
+
};
|
|
2244
|
+
}
|
|
2245
|
+
const source = retrieved && (retrieved.result || retrieved.document) || retrieved;
|
|
2246
|
+
const subset = buildCanonicalSemanticDocumentSubset(source, context.document);
|
|
2247
|
+
if (subset.status !== 'passed') {
|
|
2248
|
+
return { status: 'failed', error: subset.error || { category: 'MEMORY_RETRIEVAL_FAILED' } };
|
|
2249
|
+
}
|
|
2250
|
+
const hits = (Array.isArray(subset.document && subset.document.elements) ? subset.document.elements : [])
|
|
2251
|
+
.filter(element => element && typeof element.semanticScore === 'number')
|
|
2252
|
+
.sort((left, right) => right.semanticScore - left.semanticScore)
|
|
2253
|
+
.slice(0, topK)
|
|
2254
|
+
.map(element => Object.freeze(memoryHitCard(element, maxDescLen)));
|
|
2255
|
+
return {
|
|
2256
|
+
status: 'passed',
|
|
2257
|
+
query,
|
|
2258
|
+
max_desc_len: maxDescLen,
|
|
2259
|
+
fullTextHint: 'To read the full text of a hit, call getIntentElementContext with its id (elementId).',
|
|
2260
|
+
hits: Object.freeze(hits),
|
|
2261
|
+
};
|
|
2262
|
+
}
|
|
2263
|
+
|
|
2264
|
+
// Build one compact memory hit: id/name/type/score + an excerpt of the
|
|
2265
|
+
// description bounded by maxDescLen, plus the full text length so the caller
|
|
2266
|
+
// knows how much content exists and can decide whether to expand.
|
|
2267
|
+
function memoryHitCard(element, maxDescLen) {
|
|
2268
|
+
const description = typeof element.description === 'string' ? element.description : '';
|
|
2269
|
+
const descriptionLength = description.length;
|
|
2270
|
+
let excerpt = '';
|
|
2271
|
+
if (maxDescLen !== -1) {
|
|
2272
|
+
excerpt = maxDescLen === 0 ? description : description.slice(0, maxDescLen);
|
|
2273
|
+
}
|
|
2274
|
+
const card = {
|
|
2275
|
+
id: element.id,
|
|
2276
|
+
name: element.name,
|
|
2277
|
+
type: element.type,
|
|
2278
|
+
score: element.semanticScore,
|
|
2279
|
+
description_length: descriptionLength,
|
|
2280
|
+
};
|
|
2281
|
+
if (maxDescLen !== -1) {
|
|
2282
|
+
card.description = excerpt;
|
|
2283
|
+
if (maxDescLen > 0 && descriptionLength > maxDescLen) {
|
|
2284
|
+
card.truncated = true;
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
return card;
|
|
2288
|
+
}
|
|
2289
|
+
|
|
2194
2290
|
async function queryNeo4jGraphTool(args = {}) {
|
|
2195
2291
|
const architecturePath = args.architecturePath || DEFAULT_GRAPH_PATH;
|
|
2196
2292
|
const workspaceRoot = resolveWorkspaceRoot(args);
|
|
@@ -2780,15 +2876,52 @@ function buildCanonicalSemanticDocumentSubset(source, canonicalDocument = undefi
|
|
|
2780
2876
|
}
|
|
2781
2877
|
}
|
|
2782
2878
|
|
|
2879
|
+
// Attach the semantic similarity score to each retrieved object so callers can
|
|
2880
|
+
// judge relevance (top-K bounded recall may include borderline neighbours).
|
|
2881
|
+
const semanticScoreById = new Map();
|
|
2882
|
+
const seedsByType = evidence.seedsByType || {};
|
|
2883
|
+
for (const channelSeeds of Object.values(seedsByType)) {
|
|
2884
|
+
for (const seed of Array.isArray(channelSeeds) ? channelSeeds : []) {
|
|
2885
|
+
if (!seed || !seed.id) continue;
|
|
2886
|
+
const numericScore = Number(seed.score);
|
|
2887
|
+
if (!Number.isFinite(numericScore)) continue;
|
|
2888
|
+
const bareId = String(seed.id).replace(/^[^:]*:/, '');
|
|
2889
|
+
if (bareId && (!semanticScoreById.has(bareId) || numericScore > semanticScoreById.get(bareId))) {
|
|
2890
|
+
semanticScoreById.set(bareId, numericScore);
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
// buildCanonicalSemanticDocumentSubset may run a second time over a prior
|
|
2895
|
+
// subset document (which no longer carries seedsByType); carry scores forward
|
|
2896
|
+
// from evidence elements that already expose semanticScore.
|
|
2897
|
+
for (const item of evidenceElements) {
|
|
2898
|
+
if (!item) continue;
|
|
2899
|
+
const numericScore = Number(item.semanticScore);
|
|
2900
|
+
if (!Number.isFinite(numericScore)) continue;
|
|
2901
|
+
const key = String(item.id !== undefined ? item.id : item.view_id);
|
|
2902
|
+
if (key && (!semanticScoreById.has(key) || numericScore > semanticScoreById.get(key))) {
|
|
2903
|
+
semanticScoreById.set(key, numericScore);
|
|
2904
|
+
}
|
|
2905
|
+
}
|
|
2906
|
+
const withSemanticScore = item => {
|
|
2907
|
+
if (!item) return item;
|
|
2908
|
+
const score = item.id !== undefined
|
|
2909
|
+
? semanticScoreById.get(String(item.id))
|
|
2910
|
+
: semanticScoreById.get(String(item.view_id));
|
|
2911
|
+
return typeof score === 'number' ? Object.freeze({ ...item, semanticScore: score }) : item;
|
|
2912
|
+
};
|
|
2783
2913
|
const elements = [...elementIds]
|
|
2784
2914
|
.map(id => canonicalElementById.get(id))
|
|
2785
|
-
.filter(Boolean)
|
|
2915
|
+
.filter(Boolean)
|
|
2916
|
+
.map(withSemanticScore);
|
|
2786
2917
|
const relationships = [...relationshipIds]
|
|
2787
2918
|
.map(id => canonicalRelationshipById.get(id))
|
|
2788
|
-
.filter(Boolean)
|
|
2919
|
+
.filter(Boolean)
|
|
2920
|
+
.map(withSemanticScore);
|
|
2789
2921
|
const views = [...viewIds]
|
|
2790
2922
|
.map(id => canonicalViewById.get(id))
|
|
2791
|
-
.filter(Boolean)
|
|
2923
|
+
.filter(Boolean)
|
|
2924
|
+
.map(withSemanticScore);
|
|
2792
2925
|
|
|
2793
2926
|
return {
|
|
2794
2927
|
status: 'passed',
|
|
@@ -3483,6 +3616,7 @@ module.exports = {
|
|
|
3483
3616
|
handleRequest,
|
|
3484
3617
|
loadContext,
|
|
3485
3618
|
main,
|
|
3619
|
+
memoryHitCard,
|
|
3486
3620
|
resolveSemanticScope,
|
|
3487
3621
|
validateDocument,
|
|
3488
3622
|
};
|
package/package.json
CHANGED