Skip to content
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
808ba56
Query related obvious memory improvements
lucksus Jan 7, 2026
96044e1
Short prolog engine pool clean-up timings
lucksus Jan 7, 2026
66a7d2c
Reuse whisper model for multiple streams
lucksus Jan 7, 2026
9670dd5
Don't update prolog engines when links change
lucksus Jan 7, 2026
ff4b33e
No filtered pools
lucksus Jan 7, 2026
862a999
Reuse multiple whisper models of different size, but only one per siz…
lucksus Jan 7, 2026
7d3ba8e
Prolog mode simple
lucksus Jan 8, 2026
934cd76
Reactivate prolog subscriptions in simple mode
lucksus Jan 8, 2026
9e03f8f
FIx updating SDNA in prolog simple mode
lucksus Jan 8, 2026
d41bc13
Ad4mModel write operations wihtout Prolog
lucksus Jan 8, 2026
6686201
Prolog disabled mode
lucksus Jan 9, 2026
2c18906
PrologMode::SdnaOnly
lucksus Jan 9, 2026
58d33dd
Fix SdnaOnly mode with correct ownder_did and case differentiation.
lucksus Jan 9, 2026
102f28f
Always recreate SdnaOnly Prolog engine (no memory creep but slow)
lucksus Jan 9, 2026
61baa48
Update SdnaOnly prolog engine only if sdna links have changed
lucksus Jan 9, 2026
46c9db4
fmt
lucksus Jan 9, 2026
6492e60
Allow AI task creation wiht PROMPT capability
lucksus Jan 12, 2026
be96913
Set pool's FILTERING_THRESHOLD back to 6000 to fix test builds
lucksus Jan 12, 2026
8c84c61
Reduce log-level of new Prolog mode debug logs
lucksus Jan 12, 2026
92a78f1
Replace Prolog in isSubjectInstance() with Surreal
lucksus Jan 12, 2026
a52e03a
Don't try to resolve empty string target
lucksus Jan 12, 2026
08babbe
FIx new collection queries
lucksus Jan 13, 2026
0aef120
Collection ordering
lucksus Jan 13, 2026
a980367
Ad4mModel.getData() with surreal
lucksus Jan 13, 2026
ec99aa6
Static instance queries with surreal
lucksus Jan 13, 2026
fa49105
Skip tests for Prolog in Ad4mModels (will change this with SDNA refac…
lucksus Jan 13, 2026
d63056f
Clean up tests
lucksus Jan 13, 2026
ad52121
Fix @Flags by parsing also target from instance triples
lucksus Jan 13, 2026
0609c1b
Fix test by cleaning up first
lucksus Jan 13, 2026
3ace84f
Skip prolog embedding vector encoding test
lucksus Jan 13, 2026
d40af15
Unflake some tests
lucksus Jan 13, 2026
e3d3eb7
Remove subscription truncation
lucksus Jan 13, 2026
18c56e5
Ignore Prolog pool unit test for SdnaOnly mode
lucksus Jan 14, 2026
1346b8e
Skip tests that don't work in SdnaOnly Prolog mode
lucksus Jan 14, 2026
9706bbd
Prevent SQL injections by escaping values put into surreal queries
lucksus Jan 14, 2026
7395c58
More surreal string escaping
lucksus Jan 14, 2026
3d9187b
SDNA parsing robustness
lucksus Jan 15, 2026
6ca5c68
getCollectionValuesViaSurreal(): avoid N+1 isSubjectInstance() checks…
lucksus Jan 15, 2026
936d6b6
Make getAllSubjectProxies() actually return subject proxies again
lucksus Jan 15, 2026
bb38c41
Improve SdnaOnly mode link filtering, split from simple mode, don't p…
lucksus Jan 15, 2026
5f05954
warning--
lucksus Jan 15, 2026
f8f83a1
Scope locks in Prolog service to avoid waiting and deadlocks
lucksus Jan 15, 2026
e6f3c6f
Clarify test case interdependency
lucksus Jan 15, 2026
98f102d
fmt
lucksus Jan 15, 2026
b05d683
Link predicate queries: don't get all and filter in mem, constrain DB…
lucksus Jan 15, 2026
83e73fc
Try to unflake subscription test
lucksus Jan 15, 2026
d3fd341
Don't try Prolog first where we have surreal fallback - only surreal
lucksus Jan 15, 2026
cbd8f5c
Refactor PrologModes in perspective_instace
lucksus Jan 15, 2026
795d868
Refactor PrologModes in prolog_service/mod.rs
lucksus Jan 15, 2026
6c11269
Unflake more tests by replacing static sleeps for subscriptions etc. …
lucksus Jan 19, 2026
39a2057
Use latest link for property value in surreal based getData() impl
lucksus Jan 19, 2026
e7fbb41
Avoid property setter generation if read-only
lucksus Jan 19, 2026
fd7b198
fix: truthy checks drop valid collection values (e.g., 0, false, "").
lucksus Jan 19, 2026
5d8d49a
fix: empty arrays can’t clear a collection.
lucksus Jan 19, 2026
1d09f70
DB: Normalize empty predicate to None for consistency.
lucksus Jan 19, 2026
1c60d37
Clarify code comment about SdnaOnly mode and dirty flag
lucksus Jan 19, 2026
d2b8211
fmt
lucksus Jan 19, 2026
3edca4d
Fix handling of fact data load errors in simple prolog mode
lucksus Jan 20, 2026
dac7e24
Dedup key should include timestamp to avoid dropping distinct SDNA li…
lucksus Jan 20, 2026
8346b5a
Avoid clobbering engines on concurrent updates.
lucksus Jan 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
336 changes: 258 additions & 78 deletions core/src/model/Ad4mModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,81 @@ export class Ad4mModel {
return this.#perspective;
}

/**
* Get property metadata from decorator (Phase 1: Prolog-free refactor)
* @private
*/
private getPropertyMetadata(key: string): PropertyOptions | undefined {
const proto = Object.getPrototypeOf(this);
return proto.__properties?.[key];
}

/**
* Get collection metadata from decorator (Phase 1: Prolog-free refactor)
* @private
*/
private getCollectionMetadata(key: string): CollectionOptions | undefined {
const proto = Object.getPrototypeOf(this);
return proto.__collections?.[key];
}

/**
* Generate property setter action from metadata (Phase 1: Prolog-free refactor)
* Replaces Prolog query: property_setter(C, key, Setter)
* @private
*/
private generatePropertySetterAction(key: string, metadata: PropertyOptions): any[] {
if (metadata.setter) {
// Custom setter - throw error for now (Phase 2)
throw new Error(
`Custom setter for property "${key}" not yet supported without Prolog. ` +
`Use standard @Property decorator or enable Prolog for custom setters.`
);
}

if (!metadata.through) {
throw new Error(`Property "${key}" has no 'through' predicate defined`);
}

return [{
action: "setSingleTarget",
source: "this",
predicate: metadata.through,
target: "value",
...(metadata.local && { local: true })
}];
}

/**
* Generate collection action from metadata (Phase 1: Prolog-free refactor)
* Replaces Prolog queries: collection_adder, collection_remover, collection_setter
* @private
*/
private generateCollectionAction(key: string, actionType: 'adder' | 'remover' | 'setter'): any[] {
const metadata = this.getCollectionMetadata(key);
if (!metadata) {
throw new Error(`Collection "${key}" has no metadata defined`);
}

if (!metadata.through) {
throw new Error(`Collection "${key}" has no 'through' predicate defined`);
}

const actionMap = {
adder: "addLink",
remover: "removeLink",
setter: "collectionSetter"
};

return [{
action: actionMap[actionType],
source: "this",
predicate: metadata.through,
target: "value",
...(metadata.local && { local: true })
}];
}

public static async assignValuesToInstance(perspective: PerspectiveProxy, instance: Ad4mModel, values: ValueTuple[]) {
// Map properties to object
const propsObject = Object.fromEntries(
Expand Down Expand Up @@ -675,11 +750,100 @@ export class Ad4mModel {
${subQueries.join(", ")}
`;

const result = await this.#perspective.infer(fullQuery);
if (result?.[0]) {
const { Properties, Collections, Timestamp, Author } = result?.[0];
const values = [...Properties, ...Collections, ["timestamp", Timestamp], ["author", Author]];
await Ad4mModel.assignValuesToInstance(this.#perspective, this, values);
// Try Prolog first
try {
const result = await this.#perspective.infer(fullQuery);
if (result?.[0] && result[0].Properties && result[0].Properties.length > 0) {
const { Properties, Collections, Timestamp, Author } = result[0];
const values = [...Properties, ...Collections, ["timestamp", Timestamp], ["author", Author]];
await Ad4mModel.assignValuesToInstance(this.#perspective, this, values);
return this;
}
} catch (e) {
console.log(`Prolog getData failed for ${this.#baseExpression}, falling back to SurrealDB`);
}

// Fallback to SurrealDB (SdnaOnly mode)
try {
const ctor = this.constructor as typeof Ad4mModel;
const metadata = ctor.getModelMetadata();

// Query for all links from this specific node (base expression)
const linksQuery = `
SELECT id, predicate, out.uri AS target, author, timestamp
FROM link
WHERE in.uri = '${this.#baseExpression}'
ORDER BY timestamp ASC
`;
const links = await this.#perspective.querySurrealDB(linksQuery);

if (links && links.length > 0) {
let maxTimestamp = null;
let latestAuthor = null;

// Process properties
for (const [propName, propMeta] of Object.entries(metadata.properties)) {
const matching = links.filter((l: any) => l.predicate === propMeta.predicate);
if (matching.length > 0) {
const link = matching[0]; // Take first/latest
let value = link.target;

// Track timestamp/author
if (link.timestamp && (!maxTimestamp || link.timestamp > maxTimestamp)) {
maxTimestamp = link.timestamp;
latestAuthor = link.author;
}

// Handle resolveLanguage
if (propMeta.resolveLanguage && propMeta.resolveLanguage !== 'literal') {
try {
const expression = await this.#perspective.getExpression(value);
if (expression) {
try {
value = JSON.parse(expression.data);
} catch {
value = expression.data;
}
}
} catch (e) {
console.warn(`Failed to resolve expression for ${propName}:`, e);
}
} else if (typeof value === 'string' && value.startsWith('literal://')) {
// Parse literal URL
try {
const parsed = Literal.fromUrl(value).get();
value = parsed.data !== undefined ? parsed.data : parsed;
} catch (e) {
// Keep original value
}
}

// Apply transform if exists
if (propMeta.transform && typeof propMeta.transform === 'function') {
value = propMeta.transform(value);
}

(this as any)[propName] = value;
}
}

// Process collections
for (const [collName, collMeta] of Object.entries(metadata.collections)) {
const matching = links.filter((l: any) => l.predicate === collMeta.predicate);
// Links are already sorted by timestamp ASC from the query, so map preserves order
(this as any)[collName] = matching.map((l: any) => l.target);
}

// Set author and timestamp
if (latestAuthor) {
(this as any).author = latestAuthor;
}
if (maxTimestamp) {
(this as any).timestamp = maxTimestamp;
}
}
} catch (e) {
console.error(`SurrealDB getData also failed for ${this.#baseExpression}:`, e);
}

return this;
Expand Down Expand Up @@ -1320,7 +1484,7 @@ WHERE ${whereConditions.join(' AND ')}
let convertedValue = target;

// Only process if target has a value
if (target !== undefined && target !== null) {
if (target !== undefined && target !== null && target !== '') {
// Check if we need to resolve a non-literal language expression
if (propMeta.resolveLanguage != undefined && propMeta.resolveLanguage !== 'literal' && typeof target === 'string') {
// For non-literal languages, resolve the expression via perspective.getExpression()
Expand All @@ -1337,7 +1501,8 @@ WHERE ${whereConditions.join(' AND ')}
}
}
} catch (e) {
console.warn(`Failed to resolve expression for ${propName}:`, e);
console.warn(`Failed to resolve expression for ${propName} with target "${target}":`, e);
console.warn("Falling back to raw value");
convertedValue = target; // Fall back to raw value
}
} else if (typeof target === 'string' && target.startsWith('literal://')) {
Expand Down Expand Up @@ -1817,90 +1982,95 @@ WHERE ${whereConditions.join(' AND ')}
}

private async setProperty(key: string, value: any, batchId?: string) {
const setters = await this.#perspective.infer(
`subject_class("${this.#subjectClassName}", C), property_setter(C, "${key}", Setter)`
);
if (setters && setters.length > 0) {
const actions = eval(setters[0].Setter);
const resolveLanguageResults = await this.#perspective.infer(
`subject_class("${this.#subjectClassName}", C), property_resolve_language(C, "${key}", Language)`
);
let resolveLanguage;
if (resolveLanguageResults && resolveLanguageResults.length > 0) {
resolveLanguage = resolveLanguageResults[0].Language;
}
// Phase 1: Use metadata instead of Prolog queries
const metadata = this.getPropertyMetadata(key);
if (!metadata) {
console.warn(`Property "${key}" has no metadata, skipping`);
return;
}

if (resolveLanguage) {
value = await this.#perspective.createExpression(value, resolveLanguage);
}
await this.#perspective.executeAction(actions, this.#baseExpression, [{ name: "value", value }], batchId);
// Generate actions from metadata (replaces Prolog query)
const actions = this.generatePropertySetterAction(key, metadata);

// Get resolve language from metadata (replaces Prolog query)
let resolveLanguage = metadata.resolveLanguage;

if (resolveLanguage) {
value = await this.#perspective.createExpression(value, resolveLanguage);
}

await this.#perspective.executeAction(actions, this.#baseExpression, [{ name: "value", value }], batchId);
}

private async setCollectionSetter(key: string, value: any, batchId?: string) {
let collectionSetters = await this.#perspective.infer(
`subject_class("${this.#subjectClassName}", C), collection_setter(C, "${singularToPlural(key)}", Setter)`
);
if (!collectionSetters) collectionSetters = [];

if (collectionSetters.length > 0) {
const actions = eval(collectionSetters[0].Setter);

if (value) {
if (Array.isArray(value)) {
await this.#perspective.executeAction(
actions,
this.#baseExpression,
value.map((v) => ({ name: "value", value: v })),
batchId
);
} else {
await this.#perspective.executeAction(actions, this.#baseExpression, [{ name: "value", value }], batchId);
}
// Phase 1: Use metadata instead of Prolog queries
const metadata = this.getCollectionMetadata(key);
if (!metadata) {
console.warn(`Collection "${key}" has no metadata, skipping`);
return;
}

// Generate actions from metadata (replaces Prolog query)
const actions = this.generateCollectionAction(key, 'setter');

if (value) {
if (Array.isArray(value)) {
await this.#perspective.executeAction(
actions,
this.#baseExpression,
value.map((v) => ({ name: "value", value: v })),
batchId
);
} else {
await this.#perspective.executeAction(actions, this.#baseExpression, [{ name: "value", value }], batchId);
}
}
}

private async setCollectionAdder(key: string, value: any, batchId?: string) {
let adders = await this.#perspective.infer(
`subject_class("${this.#subjectClassName}", C), collection_adder(C, "${singularToPlural(key)}", Adder)`
);
if (!adders) adders = [];

if (adders.length > 0) {
const actions = eval(adders[0].Adder);
if (value) {
if (Array.isArray(value)) {
await Promise.all(
value.map((v) =>
this.#perspective.executeAction(actions, this.#baseExpression, [{ name: "value", value: v }], batchId)
)
);
} else {
await this.#perspective.executeAction(actions, this.#baseExpression, [{ name: "value", value }], batchId);
}
// Phase 1: Use metadata instead of Prolog queries
const metadata = this.getCollectionMetadata(key);
if (!metadata) {
console.warn(`Collection "${key}" has no metadata, skipping`);
return;
}

// Generate actions from metadata (replaces Prolog query)
const actions = this.generateCollectionAction(key, 'adder');

if (value) {
if (Array.isArray(value)) {
await Promise.all(
value.map((v) =>
this.#perspective.executeAction(actions, this.#baseExpression, [{ name: "value", value: v }], batchId)
)
);
} else {
await this.#perspective.executeAction(actions, this.#baseExpression, [{ name: "value", value }], batchId);
}
}
}

private async setCollectionRemover(key: string, value: any, batchId?: string) {
let removers = await this.#perspective.infer(
`subject_class("${this.#subjectClassName}", C), collection_remover(C, "${singularToPlural(key)}", Remover)`
);
if (!removers) removers = [];

if (removers.length > 0) {
const actions = eval(removers[0].Remover);
if (value) {
if (Array.isArray(value)) {
await Promise.all(
value.map((v) =>
this.#perspective.executeAction(actions, this.#baseExpression, [{ name: "value", value: v }], batchId)
)
);
} else {
await this.#perspective.executeAction(actions, this.#baseExpression, [{ name: "value", value }], batchId);
}
// Phase 1: Use metadata instead of Prolog queries
const metadata = this.getCollectionMetadata(key);
if (!metadata) {
console.warn(`Collection "${key}" has no metadata, skipping`);
return;
}

// Generate actions from metadata (replaces Prolog query)
const actions = this.generateCollectionAction(key, 'remover');

if (value) {
if (Array.isArray(value)) {
await Promise.all(
value.map((v) =>
this.#perspective.executeAction(actions, this.#baseExpression, [{ name: "value", value: v }], batchId)
)
);
} else {
await this.#perspective.executeAction(actions, this.#baseExpression, [{ name: "value", value }], batchId);
}
}
}
Expand Down Expand Up @@ -2003,10 +2173,20 @@ WHERE ${whereConditions.join(' AND ')}
await this.setCollectionSetter(key, value.value, batchId);
break;
}
} else if (Array.isArray(value) && value.length > 0) {
await this.setCollectionSetter(key, value, batchId);
} else if (Array.isArray(value)) {
// Handle all arrays as collections, even empty ones
if (value.length > 0) {
await this.setCollectionSetter(key, value, batchId);
}
// Skip empty arrays - don't try to set them as properties
} else if (value !== undefined && value !== null && value !== "") {
if (setProperties) {
// Check if this is a collection property (has collection metadata)
const collMetadata = this.getCollectionMetadata(key);
if (collMetadata) {
// Skip - it's a collection, not a regular property
continue;
}
await this.setProperty(key, value, batchId);
}
}
Expand Down
Loading