neotoma 0.4.2 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -0
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +332 -150
- package/dist/actions.js.map +1 -1
- package/dist/cli/commands/mirror.d.ts +52 -0
- package/dist/cli/commands/mirror.d.ts.map +1 -0
- package/dist/cli/commands/mirror.js +147 -0
- package/dist/cli/commands/mirror.js.map +1 -0
- package/dist/cli/config.d.ts +8 -0
- package/dist/cli/config.d.ts.map +1 -1
- package/dist/cli/config.js.map +1 -1
- package/dist/cli/doctor.d.ts +76 -0
- package/dist/cli/doctor.d.ts.map +1 -0
- package/dist/cli/doctor.js +345 -0
- package/dist/cli/doctor.js.map +1 -0
- package/dist/cli/hooks.d.ts +45 -0
- package/dist/cli/hooks.d.ts.map +1 -0
- package/dist/cli/hooks.js +273 -0
- package/dist/cli/hooks.js.map +1 -0
- package/dist/cli/hooks_detect.d.ts +53 -0
- package/dist/cli/hooks_detect.d.ts.map +1 -0
- package/dist/cli/hooks_detect.js +220 -0
- package/dist/cli/hooks_detect.js.map +1 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +595 -49
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/permissions.d.ts +46 -0
- package/dist/cli/permissions.d.ts.map +1 -0
- package/dist/cli/permissions.js +191 -0
- package/dist/cli/permissions.js.map +1 -0
- package/dist/cli/setup.d.ts +46 -0
- package/dist/cli/setup.d.ts.map +1 -0
- package/dist/cli/setup.js +105 -0
- package/dist/cli/setup.js.map +1 -0
- package/dist/core/operations.d.ts +145 -0
- package/dist/core/operations.d.ts.map +1 -0
- package/dist/core/operations.js +106 -0
- package/dist/core/operations.js.map +1 -0
- package/dist/openclaw_entry.d.ts +45 -0
- package/dist/openclaw_entry.d.ts.map +1 -0
- package/dist/openclaw_entry.js +75 -0
- package/dist/openclaw_entry.js.map +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +107 -748
- package/dist/server.js.map +1 -1
- package/dist/services/batch_correction.d.ts +129 -0
- package/dist/services/batch_correction.d.ts.map +1 -0
- package/dist/services/batch_correction.js +231 -0
- package/dist/services/batch_correction.js.map +1 -0
- package/dist/services/canonical_markdown.d.ts +145 -0
- package/dist/services/canonical_markdown.d.ts.map +1 -0
- package/dist/services/canonical_markdown.js +425 -0
- package/dist/services/canonical_markdown.js.map +1 -0
- package/dist/services/canonical_mirror.d.ts +127 -0
- package/dist/services/canonical_mirror.d.ts.map +1 -0
- package/dist/services/canonical_mirror.js +1047 -0
- package/dist/services/canonical_mirror.js.map +1 -0
- package/dist/services/canonical_mirror_git.d.ts +71 -0
- package/dist/services/canonical_mirror_git.d.ts.map +1 -0
- package/dist/services/canonical_mirror_git.js +211 -0
- package/dist/services/canonical_mirror_git.js.map +1 -0
- package/dist/services/entity_resolution.d.ts +17 -1
- package/dist/services/entity_resolution.d.ts.map +1 -1
- package/dist/services/entity_resolution.js +97 -24
- package/dist/services/entity_resolution.js.map +1 -1
- package/dist/services/entity_type_equivalence.d.ts +40 -0
- package/dist/services/entity_type_equivalence.d.ts.map +1 -0
- package/dist/services/entity_type_equivalence.js +109 -0
- package/dist/services/entity_type_equivalence.js.map +1 -0
- package/dist/services/entity_type_guard.d.ts +52 -0
- package/dist/services/entity_type_guard.d.ts.map +1 -0
- package/dist/services/entity_type_guard.js +204 -0
- package/dist/services/entity_type_guard.js.map +1 -0
- package/dist/services/file_text_extraction.d.ts.map +1 -1
- package/dist/services/file_text_extraction.js +5 -0
- package/dist/services/file_text_extraction.js.map +1 -1
- package/dist/services/flat_packed_detection.d.ts +32 -0
- package/dist/services/flat_packed_detection.d.ts.map +1 -0
- package/dist/services/flat_packed_detection.js +96 -0
- package/dist/services/flat_packed_detection.js.map +1 -0
- package/dist/services/interpretation.d.ts.map +1 -1
- package/dist/services/interpretation.js +9 -0
- package/dist/services/interpretation.js.map +1 -1
- package/dist/services/memory_export.d.ts +56 -0
- package/dist/services/memory_export.d.ts.map +1 -0
- package/dist/services/memory_export.js +295 -0
- package/dist/services/memory_export.js.map +1 -0
- package/dist/services/raw_fragments.js +1 -1
- package/dist/services/raw_fragments.js.map +1 -1
- package/dist/services/raw_storage.d.ts +6 -0
- package/dist/services/raw_storage.d.ts.map +1 -1
- package/dist/services/raw_storage.js +26 -0
- package/dist/services/raw_storage.js.map +1 -1
- package/dist/services/schema_definitions.d.ts.map +1 -1
- package/dist/services/schema_definitions.js +37 -0
- package/dist/services/schema_definitions.js.map +1 -1
- package/dist/services/schema_reference_linking.d.ts +38 -0
- package/dist/services/schema_reference_linking.d.ts.map +1 -0
- package/dist/services/schema_reference_linking.js +130 -0
- package/dist/services/schema_reference_linking.js.map +1 -0
- package/dist/services/schema_registry.d.ts +39 -0
- package/dist/services/schema_registry.d.ts.map +1 -1
- package/dist/services/schema_registry.js +91 -2
- package/dist/services/schema_registry.js.map +1 -1
- package/dist/services/snapshot_computation.d.ts.map +1 -1
- package/dist/services/snapshot_computation.js +10 -0
- package/dist/services/snapshot_computation.js.map +1 -1
- package/dist/services/timeline_events.d.ts +24 -5
- package/dist/services/timeline_events.d.ts.map +1 -1
- package/dist/services/timeline_events.js +89 -13
- package/dist/services/timeline_events.js.map +1 -1
- package/dist/shared/action_schemas.d.ts +18 -2
- package/dist/shared/action_schemas.d.ts.map +1 -1
- package/dist/shared/action_schemas.js +8 -0
- package/dist/shared/action_schemas.js.map +1 -1
- package/dist/shared/local_transport.d.ts +13 -0
- package/dist/shared/local_transport.d.ts.map +1 -1
- package/dist/shared/local_transport.js +89 -6
- package/dist/shared/local_transport.js.map +1 -1
- package/dist/tool_definitions.d.ts +22 -0
- package/dist/tool_definitions.d.ts.map +1 -0
- package/dist/tool_definitions.js +747 -0
- package/dist/tool_definitions.js.map +1 -0
- package/openapi.yaml +13 -0
- package/openclaw.plugin.json +87 -0
- package/package.json +31 -1
- package/skills/query-memory/SKILL.md +67 -0
- package/skills/recover-sqlite-database/SKILL.md +46 -0
- package/skills/store-data/SKILL.md +58 -0
package/dist/actions.js
CHANGED
|
@@ -14,7 +14,7 @@ import { ensurePublicKeyRegistered, getPublicKey, getUserIdFromBearerToken, isBe
|
|
|
14
14
|
import { verifyRequest, parseAuthHeader } from "./crypto/auth.js";
|
|
15
15
|
import { encryptResponseMiddleware } from "./middleware/encrypt_response.js";
|
|
16
16
|
import { initServerKeys } from "./services/encryption_service.js";
|
|
17
|
-
import { storeRawContent, downloadRawContent, SourceFileNotFoundError } from "./services/raw_storage.js";
|
|
17
|
+
import { storeRawContent, downloadRawContent, resolveLocalSourceFilePath, SourceFileNotFoundError, } from "./services/raw_storage.js";
|
|
18
18
|
import { attachSourceLabelsToObservations } from "./services/observation_source_label.js";
|
|
19
19
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
20
20
|
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
@@ -26,6 +26,7 @@ import { getSqliteDb } from "./repositories/sqlite/sqlite_client.js";
|
|
|
26
26
|
import { getMcpAuthToken } from "./crypto/mcp_auth_token.js";
|
|
27
27
|
import { isOauthKeyCredentialValid, normalizeOauthNextPath, OAuthKeySessionStore, } from "./services/oauth_key_gate.js";
|
|
28
28
|
import { AnalyzeSchemaCandidatesRequestSchema, CorrectEntityRequestSchema, CreateRelationshipRequestSchema, DeleteEntityRequestSchema, DeleteRelationshipRequestSchema, EntitiesQueryRequestSchema, EntitySnapshotRequestSchema, FieldProvenanceRequestSchema, GetSchemaRecommendationsRequestSchema, ListObservationsRequestSchema, ListRelationshipsRequestSchema, MergeEntitiesRequestSchema, ObservationsQueryRequestSchema, RegisterSchemaRequestSchema, RelationshipSnapshotRequestSchema, RestoreEntityRequestSchema, RestoreRelationshipRequestSchema, RetrieveEntityByIdentifierSchema, RetrieveGraphNeighborhoodSchema, RetrieveRelatedEntitiesSchema, StoreRequestSchema, StoreUnstructuredRequestSchema, UpdateSchemaIncrementalRequestSchema, } from "./shared/action_schemas.js";
|
|
29
|
+
import { getMimeTypeFromExtension } from "./services/file_text_extraction.js";
|
|
29
30
|
import { queryEntitiesWithCount } from "./shared/action_handlers/entity_handlers.js";
|
|
30
31
|
import { retrieveEntityByIdentifierWithFallback } from "./shared/action_handlers/entity_identifier_handler.js";
|
|
31
32
|
import { prepareEntitySnapshotWithEmbedding, upsertEntitySnapshotWithEmbedding, } from "./services/entity_snapshot_embedding.js";
|
|
@@ -1611,6 +1612,115 @@ app.get("/entities/:id", async (req, res) => {
|
|
|
1611
1612
|
return sendError(res, 500, "DB_QUERY_FAILED", message);
|
|
1612
1613
|
}
|
|
1613
1614
|
});
|
|
1615
|
+
// GET /api/entities/:id/markdown - Canonical markdown rendering of an entity
|
|
1616
|
+
// snapshot (Phase 4). Backs Inspector's markdown preview panel and the
|
|
1617
|
+
// `neotoma memory-export` CLI. Deterministic: same inputs produce byte-for-byte
|
|
1618
|
+
// identical output across calls, matching the filesystem mirror.
|
|
1619
|
+
app.get("/entities/:id/markdown", async (req, res) => {
|
|
1620
|
+
try {
|
|
1621
|
+
const entityId = req.params.id;
|
|
1622
|
+
const userId = await getAuthenticatedUserId(req, req.query.user_id);
|
|
1623
|
+
const { data: entity, error: entityError } = await db
|
|
1624
|
+
.from("entities")
|
|
1625
|
+
.select("id, user_id")
|
|
1626
|
+
.eq("id", entityId)
|
|
1627
|
+
.eq("user_id", userId)
|
|
1628
|
+
.single();
|
|
1629
|
+
if (entityError || !entity) {
|
|
1630
|
+
return sendError(res, 404, "RESOURCE_NOT_FOUND", "Entity not found");
|
|
1631
|
+
}
|
|
1632
|
+
const { getEntityWithProvenance } = await import("./services/entity_queries.js");
|
|
1633
|
+
const current = await getEntityWithProvenance(entityId);
|
|
1634
|
+
if (!current) {
|
|
1635
|
+
return sendError(res, 404, "RESOURCE_NOT_FOUND", "Entity not found");
|
|
1636
|
+
}
|
|
1637
|
+
let schemaFieldOrder;
|
|
1638
|
+
try {
|
|
1639
|
+
const { schemaRegistry } = await import("./services/schema_registry.js");
|
|
1640
|
+
const schema = await schemaRegistry.loadActiveSchema(current.entity_type, userId);
|
|
1641
|
+
if (schema?.schema_definition?.fields) {
|
|
1642
|
+
schemaFieldOrder = Object.keys(schema.schema_definition.fields);
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
catch {
|
|
1646
|
+
// Alphabetical fallback when schema is unavailable.
|
|
1647
|
+
}
|
|
1648
|
+
const { renderEntityMarkdown } = await import("./services/canonical_markdown.js");
|
|
1649
|
+
const markdown = renderEntityMarkdown({
|
|
1650
|
+
entity_id: current.entity_id ?? entityId,
|
|
1651
|
+
entity_type: current.entity_type,
|
|
1652
|
+
schema_version: current.schema_version ?? "1.0",
|
|
1653
|
+
snapshot: current.snapshot ?? {},
|
|
1654
|
+
computed_at: current.computed_at ?? null,
|
|
1655
|
+
observation_count: current.observation_count ?? 0,
|
|
1656
|
+
last_observation_at: current.last_observation_at ?? null,
|
|
1657
|
+
}, schemaFieldOrder);
|
|
1658
|
+
// etag is derived from last_observation_at — any new observation moves it,
|
|
1659
|
+
// which is exactly the Phase 4 optimistic concurrency boundary.
|
|
1660
|
+
const etag = current.last_observation_at
|
|
1661
|
+
? `W/"${Buffer.from(current.last_observation_at).toString("base64")}"`
|
|
1662
|
+
: undefined;
|
|
1663
|
+
if (etag)
|
|
1664
|
+
res.setHeader("ETag", etag);
|
|
1665
|
+
res.setHeader("Content-Type", "text/markdown; charset=utf-8");
|
|
1666
|
+
return res.send(markdown);
|
|
1667
|
+
}
|
|
1668
|
+
catch (error) {
|
|
1669
|
+
if (error instanceof Error && error.message.includes("Not authenticated")) {
|
|
1670
|
+
return sendError(res, 401, "AUTH_REQUIRED", error.message);
|
|
1671
|
+
}
|
|
1672
|
+
logError("APIError:entity_markdown", req, error);
|
|
1673
|
+
const message = error instanceof Error ? error.message : "Failed to render entity markdown";
|
|
1674
|
+
return sendError(res, 500, "DB_QUERY_FAILED", message);
|
|
1675
|
+
}
|
|
1676
|
+
});
|
|
1677
|
+
// POST /entities/:id/batch_correct - Atomic multi-field correction (Phase 4).
|
|
1678
|
+
// Shares the same backend as Phase 4b `neotoma edit` CLI; the Inspector Edit
|
|
1679
|
+
// tab issues one of these per save instead of N POST /correct calls so the
|
|
1680
|
+
// optimistic concurrency check and schema validation are atomic.
|
|
1681
|
+
app.post("/entities/:id/batch_correct", async (req, res) => {
|
|
1682
|
+
try {
|
|
1683
|
+
const entityId = req.params.id;
|
|
1684
|
+
const userId = await getAuthenticatedUserId(req, req.body?.user_id);
|
|
1685
|
+
const { data: entity, error: entityError } = await db
|
|
1686
|
+
.from("entities")
|
|
1687
|
+
.select("id, user_id")
|
|
1688
|
+
.eq("id", entityId)
|
|
1689
|
+
.eq("user_id", userId)
|
|
1690
|
+
.single();
|
|
1691
|
+
if (entityError || !entity) {
|
|
1692
|
+
return sendError(res, 404, "RESOURCE_NOT_FOUND", "Entity not found");
|
|
1693
|
+
}
|
|
1694
|
+
const body = req.body;
|
|
1695
|
+
if (!body?.changes || !Array.isArray(body.changes)) {
|
|
1696
|
+
return sendError(res, 400, "VALIDATION_ERROR", "`changes` array is required");
|
|
1697
|
+
}
|
|
1698
|
+
const { applyBatchCorrection } = await import("./services/batch_correction.js");
|
|
1699
|
+
const result = await applyBatchCorrection({
|
|
1700
|
+
entity_id: entityId,
|
|
1701
|
+
user_id: userId,
|
|
1702
|
+
expected_last_observation_at: body.expected_last_observation_at ?? null,
|
|
1703
|
+
overwrite: body.overwrite === true,
|
|
1704
|
+
changes: body.changes,
|
|
1705
|
+
idempotency_prefix: body.idempotency_prefix,
|
|
1706
|
+
});
|
|
1707
|
+
if (result.status === "conflict") {
|
|
1708
|
+
return res.status(409).json({ success: false, ...result });
|
|
1709
|
+
}
|
|
1710
|
+
if (result.status === "validation_error") {
|
|
1711
|
+
return res.status(400).json({ success: false, ...result });
|
|
1712
|
+
}
|
|
1713
|
+
return res.json({ success: true, ...result });
|
|
1714
|
+
}
|
|
1715
|
+
catch (error) {
|
|
1716
|
+
if (error instanceof Error && error.message.includes("Not authenticated")) {
|
|
1717
|
+
return sendError(res, 401, "AUTH_REQUIRED", error.message);
|
|
1718
|
+
}
|
|
1719
|
+
logError("APIError:entity_batch_correct", req, error);
|
|
1720
|
+
const message = error instanceof Error ? error.message : "Failed to apply batch correction";
|
|
1721
|
+
return sendError(res, 500, "DB_QUERY_FAILED", message);
|
|
1722
|
+
}
|
|
1723
|
+
});
|
|
1614
1724
|
// GET /api/entities/:id/observations - Get observations for entity (FU-601)
|
|
1615
1725
|
// REQUIRES AUTHENTICATION - verifies entity belongs to authenticated user
|
|
1616
1726
|
app.get("/entities/:id/observations", async (req, res) => {
|
|
@@ -2206,17 +2316,24 @@ app.get("/sources", async (req, res) => {
|
|
|
2206
2316
|
const offset = parseInt(req.query.offset) || 0;
|
|
2207
2317
|
// Build query - ALWAYS filter by authenticated user_id
|
|
2208
2318
|
let query = db.from("sources").select("*", { count: "exact" }).eq("user_id", userId); // SECURITY: Always filter by authenticated user
|
|
2209
|
-
// Filter by MIME
|
|
2319
|
+
// Filter by MIME/type query.
|
|
2320
|
+
// UX behavior: "wav" should match either MIME (audio/wav) OR filename extension (.wav).
|
|
2210
2321
|
if (mimeType) {
|
|
2211
|
-
|
|
2322
|
+
const mimeNeedle = mimeType.trim();
|
|
2323
|
+
if (mimeNeedle.includes("/")) {
|
|
2324
|
+
query = query.ilike("mime_type", `%${mimeNeedle}%`);
|
|
2325
|
+
}
|
|
2326
|
+
else {
|
|
2327
|
+
query = query.or(`mime_type.ilike.%${mimeNeedle}%,original_filename.ilike.%${mimeNeedle}%`);
|
|
2328
|
+
}
|
|
2212
2329
|
}
|
|
2213
|
-
// Filter by source type
|
|
2330
|
+
// Filter by source type (partial match for UX consistency)
|
|
2214
2331
|
if (sourceType) {
|
|
2215
|
-
query = query.
|
|
2332
|
+
query = query.ilike("source_type", `%${sourceType}%`);
|
|
2216
2333
|
}
|
|
2217
|
-
// Search in
|
|
2334
|
+
// Search in filenames
|
|
2218
2335
|
if (search) {
|
|
2219
|
-
query = query.
|
|
2336
|
+
query = query.ilike("original_filename", `%${search}%`);
|
|
2220
2337
|
}
|
|
2221
2338
|
// Sort by created_at descending (most recent first)
|
|
2222
2339
|
query = query.order("created_at", { ascending: false });
|
|
@@ -2259,7 +2376,11 @@ app.get("/sources/:id", async (req, res) => {
|
|
|
2259
2376
|
if (!source) {
|
|
2260
2377
|
return sendError(res, 404, "RESOURCE_NOT_FOUND", "Source not found");
|
|
2261
2378
|
}
|
|
2262
|
-
|
|
2379
|
+
const filesystemAbsolutePath = resolveLocalSourceFilePath(source.storage_url);
|
|
2380
|
+
const payload = filesystemAbsolutePath != null
|
|
2381
|
+
? { ...source, filesystem_absolute_path: filesystemAbsolutePath }
|
|
2382
|
+
: { ...source };
|
|
2383
|
+
return res.json(payload);
|
|
2263
2384
|
}
|
|
2264
2385
|
catch (error) {
|
|
2265
2386
|
return handleApiError(req, res, error, "Failed to get source", "DB_QUERY_FAILED", "APIError:source_detail");
|
|
@@ -2312,9 +2433,37 @@ app.get("/sources/:id/content", async (req, res) => {
|
|
|
2312
2433
|
return sendError(res, 404, "NO_CONTENT", "Source has no stored file content");
|
|
2313
2434
|
}
|
|
2314
2435
|
const buffer = await downloadRawContent(sourceToServe.storage_url);
|
|
2315
|
-
const mimeType = sourceToServe.mime_type || "application/octet-stream";
|
|
2316
2436
|
const filename = sourceToServe.original_filename || `source-${sourceId}`;
|
|
2317
|
-
const
|
|
2437
|
+
const rawMime = (sourceToServe.mime_type || "").trim().toLowerCase();
|
|
2438
|
+
let mimeType = (sourceToServe.mime_type || "").trim() || "application/octet-stream";
|
|
2439
|
+
if (!rawMime || rawMime === "application/octet-stream") {
|
|
2440
|
+
const f = filename.toLowerCase();
|
|
2441
|
+
if (f.endsWith(".wav"))
|
|
2442
|
+
mimeType = "audio/wav";
|
|
2443
|
+
else if (f.endsWith(".mp3"))
|
|
2444
|
+
mimeType = "audio/mpeg";
|
|
2445
|
+
else if (f.endsWith(".m4a"))
|
|
2446
|
+
mimeType = "audio/mp4";
|
|
2447
|
+
else if (f.endsWith(".aac"))
|
|
2448
|
+
mimeType = "audio/aac";
|
|
2449
|
+
else if (f.endsWith(".ogg") || f.endsWith(".oga"))
|
|
2450
|
+
mimeType = "audio/ogg";
|
|
2451
|
+
else if (f.endsWith(".flac"))
|
|
2452
|
+
mimeType = "audio/flac";
|
|
2453
|
+
else if (f.endsWith(".webm"))
|
|
2454
|
+
mimeType = "audio/webm";
|
|
2455
|
+
else if (f.endsWith(".pdf"))
|
|
2456
|
+
mimeType = "application/pdf";
|
|
2457
|
+
else if (f.endsWith(".png"))
|
|
2458
|
+
mimeType = "image/png";
|
|
2459
|
+
else if (f.endsWith(".jpg") || f.endsWith(".jpeg"))
|
|
2460
|
+
mimeType = "image/jpeg";
|
|
2461
|
+
else if (f.endsWith(".gif"))
|
|
2462
|
+
mimeType = "image/gif";
|
|
2463
|
+
else if (f.endsWith(".webp"))
|
|
2464
|
+
mimeType = "image/webp";
|
|
2465
|
+
}
|
|
2466
|
+
const inline = /^(application\/pdf|text\/|image\/|audio\/)/i.test(mimeType);
|
|
2318
2467
|
res.setHeader("Content-Type", mimeType);
|
|
2319
2468
|
res.setHeader("Content-Length", buffer.length);
|
|
2320
2469
|
res.setHeader("Content-Disposition", `${inline ? "inline" : "attachment"}; filename="${filename.replace(/"/g, '\\"')}"`);
|
|
@@ -2473,6 +2622,17 @@ app.post("/observations/create", async (req, res) => {
|
|
|
2473
2622
|
async function storeStructuredForApi(params) {
|
|
2474
2623
|
const { userId, entities, sourcePriority, idempotencyKey, originalFilename } = params;
|
|
2475
2624
|
const { resolveEntity } = await import("./services/entity_resolution.js");
|
|
2625
|
+
const { detectFlatPackedRows, FlatPackedRowsError } = await import("./services/flat_packed_detection.js");
|
|
2626
|
+
// Reject flat-packed rows (whole tables smuggled into a single entity as
|
|
2627
|
+
// `<prefix>_<index>_<suffix>` keys). These cannot produce per-row snapshots
|
|
2628
|
+
// and are almost always a caller bug. The caller should split into one
|
|
2629
|
+
// entity per row and retry.
|
|
2630
|
+
for (const entityData of entities) {
|
|
2631
|
+
const detection = detectFlatPackedRows(entityData);
|
|
2632
|
+
if (detection.detected) {
|
|
2633
|
+
throw new FlatPackedRowsError(detection);
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2476
2636
|
const { data: existingSource, error: existingSourceError } = await db
|
|
2477
2637
|
.from("sources")
|
|
2478
2638
|
.select("id")
|
|
@@ -2520,10 +2680,29 @@ async function storeStructuredForApi(params) {
|
|
|
2520
2680
|
});
|
|
2521
2681
|
const createdEntities = [];
|
|
2522
2682
|
for (const entityData of entities) {
|
|
2523
|
-
|
|
2683
|
+
let entity_type = entityData.entity_type;
|
|
2524
2684
|
if (!entity_type) {
|
|
2525
2685
|
throw new Error("entity_type is required for each entity");
|
|
2526
2686
|
}
|
|
2687
|
+
// Schema-agnostic duplicate-type collapse (e.g. `places` -> `place`,
|
|
2688
|
+
// aliased-type -> canonical). Applied before storing so the resolved
|
|
2689
|
+
// entity_id hashes into the canonical type rather than a near-duplicate.
|
|
2690
|
+
try {
|
|
2691
|
+
const { schemaRegistry } = await import("./services/schema_registry.js");
|
|
2692
|
+
const active = await schemaRegistry.loadActiveSchema(entity_type, userId);
|
|
2693
|
+
if (!active) {
|
|
2694
|
+
const { findEquivalentEntityType } = await import("./services/entity_type_equivalence.js");
|
|
2695
|
+
const match = await findEquivalentEntityType(entity_type, { userId });
|
|
2696
|
+
if (match) {
|
|
2697
|
+
logger.warn(`[STORE] Collapsing new entity_type "${entity_type}" -> existing ` +
|
|
2698
|
+
`"${match.canonical_entity_type}" (reason: ${match.reason}).`);
|
|
2699
|
+
entity_type = match.canonical_entity_type;
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
catch (equivErr) {
|
|
2704
|
+
logger.warn(`Equivalence check failed for ${entity_type}: ${equivErr instanceof Error ? equivErr.message : String(equivErr)}`);
|
|
2705
|
+
}
|
|
2527
2706
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- intentional omit
|
|
2528
2707
|
const { entity_type: _removed, ...fields } = entityData;
|
|
2529
2708
|
const entity_id = await resolveEntity({
|
|
@@ -2550,6 +2729,28 @@ async function storeStructuredForApi(params) {
|
|
|
2550
2729
|
catch (snapshotErr) {
|
|
2551
2730
|
logger.warn(`Snapshot recompute failed for ${entity_id}: ${snapshotErr}`);
|
|
2552
2731
|
}
|
|
2732
|
+
// Schema-driven auto-linking: if the entity's active schema declares
|
|
2733
|
+
// reference_fields, create typed relationships to the referenced
|
|
2734
|
+
// entities (REFERS_TO by default). Silent fallback when no schema or no
|
|
2735
|
+
// match exists — never invent targets.
|
|
2736
|
+
try {
|
|
2737
|
+
const { schemaRegistry } = await import("./services/schema_registry.js");
|
|
2738
|
+
const schemaEntry = await schemaRegistry.loadActiveSchema(entity_type, userId);
|
|
2739
|
+
if (schemaEntry?.schema_definition?.reference_fields?.length) {
|
|
2740
|
+
const { autoLinkReferenceFields } = await import("./services/schema_reference_linking.js");
|
|
2741
|
+
await autoLinkReferenceFields({
|
|
2742
|
+
entityId: entity_id,
|
|
2743
|
+
entityType: entity_type,
|
|
2744
|
+
fields,
|
|
2745
|
+
schema: schemaEntry.schema_definition,
|
|
2746
|
+
userId,
|
|
2747
|
+
sourceId: storageResult.sourceId,
|
|
2748
|
+
});
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
catch (linkErr) {
|
|
2752
|
+
logger.warn(`Auto-link reference fields failed for ${entity_type}/${entity_id}: ${linkErr instanceof Error ? linkErr.message : String(linkErr)}`);
|
|
2753
|
+
}
|
|
2553
2754
|
createdEntities.push({
|
|
2554
2755
|
entity_id,
|
|
2555
2756
|
entity_type,
|
|
@@ -2565,13 +2766,16 @@ async function storeStructuredForApi(params) {
|
|
|
2565
2766
|
};
|
|
2566
2767
|
}
|
|
2567
2768
|
async function storeUnstructuredForApi(params) {
|
|
2568
|
-
const { fileContent, mimeType, idempotencyKey, originalFilename, userId, } = params;
|
|
2569
|
-
const
|
|
2769
|
+
const { fileContent, fileBuffer, mimeType, idempotencyKey, originalFilename, userId, } = params;
|
|
2770
|
+
const resolvedFileBuffer = fileBuffer ?? (fileContent !== undefined ? Buffer.from(fileContent, "base64") : undefined);
|
|
2771
|
+
if (!resolvedFileBuffer) {
|
|
2772
|
+
throw new Error("fileContent or fileBuffer is required for unstructured storage");
|
|
2773
|
+
}
|
|
2570
2774
|
const resolvedIdempotencyKey = idempotencyKey ??
|
|
2571
|
-
(await import("node:crypto")).createHash("sha256").update(
|
|
2775
|
+
(await import("node:crypto")).createHash("sha256").update(resolvedFileBuffer).digest("hex");
|
|
2572
2776
|
const storageResult = await storeRawContent({
|
|
2573
2777
|
userId,
|
|
2574
|
-
fileBuffer,
|
|
2778
|
+
fileBuffer: resolvedFileBuffer,
|
|
2575
2779
|
mimeType,
|
|
2576
2780
|
originalFilename: originalFilename?.trim() || undefined,
|
|
2577
2781
|
idempotencyKey: resolvedIdempotencyKey,
|
|
@@ -2616,36 +2820,30 @@ app.post("/store", async (req, res) => {
|
|
|
2616
2820
|
});
|
|
2617
2821
|
}
|
|
2618
2822
|
if (hasUnstructured) {
|
|
2619
|
-
|
|
2823
|
+
const fileContent = parsed.data.file_content;
|
|
2620
2824
|
let mimeType = parsed.data.mime_type;
|
|
2621
2825
|
let originalFilename = parsed.data.original_filename;
|
|
2826
|
+
let resolvedFileBuffer;
|
|
2622
2827
|
if (hasFilePath) {
|
|
2623
2828
|
const resolvedPath = path.isAbsolute(parsed.data.file_path)
|
|
2624
2829
|
? parsed.data.file_path
|
|
2625
2830
|
: path.resolve(process.cwd(), parsed.data.file_path);
|
|
2626
|
-
|
|
2627
|
-
fileContent = fileBuffer.toString("base64");
|
|
2831
|
+
resolvedFileBuffer = fs.readFileSync(resolvedPath);
|
|
2628
2832
|
if (!mimeType) {
|
|
2629
2833
|
const ext = path.extname(resolvedPath).toLowerCase();
|
|
2630
|
-
mimeType =
|
|
2631
|
-
ext === ".pdf"
|
|
2632
|
-
? "application/pdf"
|
|
2633
|
-
: ext === ".csv"
|
|
2634
|
-
? "text/csv"
|
|
2635
|
-
: ext === ".json"
|
|
2636
|
-
? "application/json"
|
|
2637
|
-
: "text/plain";
|
|
2834
|
+
mimeType = getMimeTypeFromExtension(ext) || "application/octet-stream";
|
|
2638
2835
|
}
|
|
2639
2836
|
originalFilename = originalFilename || path.basename(resolvedPath);
|
|
2640
2837
|
}
|
|
2641
|
-
if (!fileContent || !mimeType) {
|
|
2838
|
+
if ((!fileContent && !resolvedFileBuffer) || !mimeType) {
|
|
2642
2839
|
return sendError(res, 400, "VALIDATION_ERROR", "Unstructured store requires file_content+mime_type or file_path");
|
|
2643
2840
|
}
|
|
2644
2841
|
unstructuredResult = await storeUnstructuredForApi({
|
|
2645
2842
|
userId,
|
|
2646
2843
|
fileContent,
|
|
2844
|
+
fileBuffer: resolvedFileBuffer,
|
|
2647
2845
|
mimeType,
|
|
2648
|
-
idempotencyKey: parsed.data.file_idempotency_key,
|
|
2846
|
+
idempotencyKey: parsed.data.file_idempotency_key ?? (!hasEntities ? parsed.data.idempotency_key : undefined),
|
|
2649
2847
|
originalFilename,
|
|
2650
2848
|
});
|
|
2651
2849
|
}
|
|
@@ -2664,6 +2862,34 @@ app.post("/store", async (req, res) => {
|
|
|
2664
2862
|
if (error instanceof Error && error.message.includes("Not authenticated")) {
|
|
2665
2863
|
return sendError(res, 401, "AUTH_REQUIRED", error.message);
|
|
2666
2864
|
}
|
|
2865
|
+
const errCode = error && typeof error === "object"
|
|
2866
|
+
? error.code
|
|
2867
|
+
: undefined;
|
|
2868
|
+
if (errCode === "ERR_FORBIDDEN_ENTITY_TYPE" ||
|
|
2869
|
+
errCode === "ERR_PLURAL_ENTITY_TYPE") {
|
|
2870
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2871
|
+
logWarn("EntityTypeGuardError:store", req, { code: errCode, message });
|
|
2872
|
+
return sendError(res, 400, errCode, message);
|
|
2873
|
+
}
|
|
2874
|
+
if (error &&
|
|
2875
|
+
typeof error === "object" &&
|
|
2876
|
+
errCode === "ERR_FLAT_PACKED_ROWS") {
|
|
2877
|
+
const err = error;
|
|
2878
|
+
logWarn("FlatPackedRowsError:store", req, {
|
|
2879
|
+
prefix: err.detection.prefix,
|
|
2880
|
+
row_count: err.detection.indices?.length ?? 0,
|
|
2881
|
+
});
|
|
2882
|
+
return res.status(400).json({
|
|
2883
|
+
error: {
|
|
2884
|
+
code: "ERR_FLAT_PACKED_ROWS",
|
|
2885
|
+
message: err.message,
|
|
2886
|
+
prefix: err.detection.prefix,
|
|
2887
|
+
row_count: err.detection.indices?.length ?? 0,
|
|
2888
|
+
example_keys: err.detection.exampleKeys ?? [],
|
|
2889
|
+
suggested_entities: err.detection.suggestedEntities ?? [],
|
|
2890
|
+
},
|
|
2891
|
+
});
|
|
2892
|
+
}
|
|
2667
2893
|
logError("APIError:store", req, error);
|
|
2668
2894
|
const message = error instanceof Error ? error.message : "Failed to store payload";
|
|
2669
2895
|
return sendError(res, 500, "DB_QUERY_FAILED", message);
|
|
@@ -3384,6 +3610,13 @@ app.post("/get_schema_recommendations", async (req, res) => {
|
|
|
3384
3610
|
}
|
|
3385
3611
|
});
|
|
3386
3612
|
// POST /update_schema_incremental - Incrementally update schema by adding or removing fields
|
|
3613
|
+
//
|
|
3614
|
+
// Routes through SchemaRegistryService.updateSchemaIncremental so that:
|
|
3615
|
+
// - Version bumps are consistent across CLI, MCP and HTTP.
|
|
3616
|
+
// - Schema-level declarations (canonical_name_fields, temporal_fields,
|
|
3617
|
+
// reference_fields, aliases) are preserved across incremental updates.
|
|
3618
|
+
// - migrate_existing correctly backfills raw_fragments → observations.
|
|
3619
|
+
// - Reducer policies track added/removed fields.
|
|
3387
3620
|
app.post("/update_schema_incremental", async (req, res) => {
|
|
3388
3621
|
const parsed = UpdateSchemaIncrementalRequestSchema.safeParse(req.body);
|
|
3389
3622
|
if (!parsed.success) {
|
|
@@ -3394,87 +3627,41 @@ app.post("/update_schema_incremental", async (req, res) => {
|
|
|
3394
3627
|
}
|
|
3395
3628
|
try {
|
|
3396
3629
|
const userId = await getAuthenticatedUserId(req, parsed.data.user_id);
|
|
3397
|
-
const { entity_type, fields_to_add, fields_to_remove, schema_version, user_specific = false, activate = true, } = parsed.data;
|
|
3398
|
-
|
|
3399
|
-
let
|
|
3400
|
-
|
|
3401
|
-
.
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3630
|
+
const { entity_type, fields_to_add, fields_to_remove, schema_version, user_specific = false, activate = true, migrate_existing = false, force = false, } = parsed.data;
|
|
3631
|
+
const { schemaRegistry } = await import("./services/schema_registry.js");
|
|
3632
|
+
let newSchema;
|
|
3633
|
+
try {
|
|
3634
|
+
newSchema = await schemaRegistry.updateSchemaIncremental({
|
|
3635
|
+
entity_type,
|
|
3636
|
+
fields_to_add: (fields_to_add || []),
|
|
3637
|
+
fields_to_remove: fields_to_remove || [],
|
|
3638
|
+
schema_version,
|
|
3639
|
+
user_specific,
|
|
3640
|
+
user_id: userId,
|
|
3641
|
+
activate,
|
|
3642
|
+
migrate_existing,
|
|
3643
|
+
force,
|
|
3644
|
+
});
|
|
3406
3645
|
}
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
logError("DbError:update_schema_incremental", req, fetchError);
|
|
3413
|
-
return sendError(res, 500, "DB_QUERY_FAILED", fetchError.message);
|
|
3414
|
-
}
|
|
3415
|
-
// Build new schema definition
|
|
3416
|
-
const baseSchema = currentSchema?.schema_definition || { fields: {} };
|
|
3417
|
-
const newFields = {};
|
|
3418
|
-
(fields_to_add || []).forEach((field) => {
|
|
3419
|
-
newFields[field.field_name] = {
|
|
3420
|
-
type: field.field_type,
|
|
3421
|
-
required: field.required || false,
|
|
3422
|
-
reducer_strategy: field.reducer_strategy || "highest_priority",
|
|
3423
|
-
};
|
|
3424
|
-
});
|
|
3425
|
-
const mergedFields = { ...baseSchema.fields, ...newFields };
|
|
3426
|
-
// Remove fields (data preserved in observations, excluded from snapshots via reducer projection)
|
|
3427
|
-
const removedFields = [];
|
|
3428
|
-
for (const fieldName of fields_to_remove || []) {
|
|
3429
|
-
if (mergedFields[fieldName]) {
|
|
3430
|
-
delete mergedFields[fieldName];
|
|
3431
|
-
removedFields.push(fieldName);
|
|
3646
|
+
catch (err) {
|
|
3647
|
+
const code = err?.code;
|
|
3648
|
+
if (code === "ERR_FORBIDDEN_ENTITY_TYPE" ||
|
|
3649
|
+
code === "ERR_PLURAL_ENTITY_TYPE") {
|
|
3650
|
+
return sendError(res, 400, code, err.message);
|
|
3432
3651
|
}
|
|
3433
|
-
|
|
3434
|
-
const updatedSchema = {
|
|
3435
|
-
...baseSchema,
|
|
3436
|
-
fields: mergedFields,
|
|
3437
|
-
};
|
|
3438
|
-
// Determine version bump: major for removals, minor for additions only
|
|
3439
|
-
const hasRemovals = removedFields.length > 0;
|
|
3440
|
-
const defaultVersion = hasRemovals
|
|
3441
|
-
? (() => {
|
|
3442
|
-
const parts = (currentSchema?.schema_version || "1.0.0").split(".");
|
|
3443
|
-
return `${parseInt(parts[0] || "1") + 1}.0.0`;
|
|
3444
|
-
})()
|
|
3445
|
-
: `${parseInt(currentSchema?.schema_version || "1") + 1}`;
|
|
3446
|
-
// Insert new schema version
|
|
3447
|
-
const { data: newSchemaVersion, error: insertError } = await db
|
|
3448
|
-
.from("schema_registry")
|
|
3449
|
-
.insert({
|
|
3450
|
-
entity_type,
|
|
3451
|
-
schema_version: schema_version || defaultVersion,
|
|
3452
|
-
schema_definition: updatedSchema,
|
|
3453
|
-
reducer_config: currentSchema?.reducer_config || {},
|
|
3454
|
-
user_id: user_specific ? userId : null,
|
|
3455
|
-
active: activate,
|
|
3456
|
-
created_at: new Date().toISOString(),
|
|
3457
|
-
})
|
|
3458
|
-
.select()
|
|
3459
|
-
.single();
|
|
3460
|
-
if (insertError) {
|
|
3461
|
-
logError("DbError:update_schema_incremental", req, insertError);
|
|
3462
|
-
return sendError(res, 500, "DB_QUERY_FAILED", insertError.message);
|
|
3463
|
-
}
|
|
3464
|
-
// Deactivate old schema if activating new one
|
|
3465
|
-
if (activate && currentSchema) {
|
|
3466
|
-
await db.from("schema_registry").update({ active: false }).eq("id", currentSchema.id);
|
|
3652
|
+
throw err;
|
|
3467
3653
|
}
|
|
3468
3654
|
logDebug("Success:update_schema_incremental", req, {
|
|
3469
3655
|
entity_type,
|
|
3470
3656
|
fields_added: (fields_to_add || []).length,
|
|
3471
|
-
fields_removed:
|
|
3657
|
+
fields_removed: (fields_to_remove || []).length,
|
|
3658
|
+
migrate_existing,
|
|
3472
3659
|
});
|
|
3473
3660
|
return res.json({
|
|
3474
3661
|
success: true,
|
|
3475
|
-
schema:
|
|
3476
|
-
schema_version:
|
|
3477
|
-
fields_removed:
|
|
3662
|
+
schema: newSchema,
|
|
3663
|
+
schema_version: newSchema.schema_version,
|
|
3664
|
+
fields_removed: fields_to_remove || [],
|
|
3478
3665
|
});
|
|
3479
3666
|
}
|
|
3480
3667
|
catch (error) {
|
|
@@ -3482,6 +3669,11 @@ app.post("/update_schema_incremental", async (req, res) => {
|
|
|
3482
3669
|
}
|
|
3483
3670
|
});
|
|
3484
3671
|
// POST /register_schema - Register new schema or schema version
|
|
3672
|
+
//
|
|
3673
|
+
// Routes through SchemaRegistryService.register so that schema_definition and
|
|
3674
|
+
// reducer_config are validated consistently across CLI, MCP and HTTP, and
|
|
3675
|
+
// schema-level declarations (canonical_name_fields, temporal_fields,
|
|
3676
|
+
// reference_fields, aliases) are rejected early when malformed.
|
|
3485
3677
|
app.post("/register_schema", async (req, res) => {
|
|
3486
3678
|
const parsed = RegisterSchemaRequestSchema.safeParse(req.body);
|
|
3487
3679
|
if (!parsed.success) {
|
|
@@ -3492,34 +3684,39 @@ app.post("/register_schema", async (req, res) => {
|
|
|
3492
3684
|
}
|
|
3493
3685
|
try {
|
|
3494
3686
|
const userId = await getAuthenticatedUserId(req, parsed.data.user_id);
|
|
3495
|
-
const { entity_type, schema_definition, reducer_config, schema_version = "1.0", user_specific = false, activate = false, } = parsed.data;
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
.
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3687
|
+
const { entity_type, schema_definition, reducer_config, schema_version = "1.0", user_specific = false, activate = false, force = false, } = parsed.data;
|
|
3688
|
+
const { schemaRegistry } = await import("./services/schema_registry.js");
|
|
3689
|
+
let newSchema;
|
|
3690
|
+
try {
|
|
3691
|
+
newSchema = await schemaRegistry.register({
|
|
3692
|
+
entity_type,
|
|
3693
|
+
schema_version,
|
|
3694
|
+
schema_definition: schema_definition,
|
|
3695
|
+
reducer_config: (reducer_config || { merge_policies: {} }),
|
|
3696
|
+
user_id: userId,
|
|
3697
|
+
user_specific,
|
|
3698
|
+
activate,
|
|
3699
|
+
force,
|
|
3700
|
+
});
|
|
3701
|
+
}
|
|
3702
|
+
catch (err) {
|
|
3703
|
+
const code = err?.code;
|
|
3704
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3705
|
+
logWarn("ValidationError:register_schema", req, { error: message });
|
|
3706
|
+
if (code === "ERR_FORBIDDEN_ENTITY_TYPE" ||
|
|
3707
|
+
code === "ERR_PLURAL_ENTITY_TYPE") {
|
|
3708
|
+
return sendError(res, 400, code, message);
|
|
3709
|
+
}
|
|
3710
|
+
return sendError(res, 400, "SCHEMA_VALIDATION_FAILED", message);
|
|
3513
3711
|
}
|
|
3514
|
-
// If activating, deactivate other schemas for this entity type
|
|
3515
3712
|
if (activate) {
|
|
3516
|
-
|
|
3517
|
-
.
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3713
|
+
try {
|
|
3714
|
+
await schemaRegistry.activate(entity_type, newSchema.schema_version);
|
|
3715
|
+
}
|
|
3716
|
+
catch (err) {
|
|
3717
|
+
logWarn("ActivateError:register_schema", req, {
|
|
3718
|
+
error: err instanceof Error ? err.message : String(err),
|
|
3719
|
+
});
|
|
3523
3720
|
}
|
|
3524
3721
|
}
|
|
3525
3722
|
logDebug("Success:register_schema", req, { entity_type, schema_version });
|
|
@@ -3711,35 +3908,20 @@ app.post("/recompute_snapshots_by_type", async (req, res) => {
|
|
|
3711
3908
|
entity_ids: entityIds,
|
|
3712
3909
|
});
|
|
3713
3910
|
}
|
|
3714
|
-
|
|
3911
|
+
// Use recomputeSnapshot so that:
|
|
3912
|
+
// - The active schema is loaded and applied (schema-driven canonical_name
|
|
3913
|
+
// and timeline events).
|
|
3914
|
+
// - Embedding preparation, snapshot upsert, and timeline event upsert
|
|
3915
|
+
// all run through the shared pipeline.
|
|
3916
|
+
const { recomputeSnapshot } = await import("./services/snapshot_computation.js");
|
|
3715
3917
|
let recomputed = 0;
|
|
3716
3918
|
let errors = 0;
|
|
3717
3919
|
const errorDetails = [];
|
|
3718
3920
|
for (const entityId of entityIds) {
|
|
3719
3921
|
try {
|
|
3720
|
-
const
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
.eq("entity_id", entityId)
|
|
3724
|
-
.order("observed_at", { ascending: false });
|
|
3725
|
-
if (!observations || observations.length === 0)
|
|
3726
|
-
continue;
|
|
3727
|
-
const newSnapshot = await observationReducer.computeSnapshot(entityId, observations);
|
|
3728
|
-
if (!newSnapshot)
|
|
3729
|
-
continue;
|
|
3730
|
-
const rowWithEmbedding = await prepareEntitySnapshotWithEmbedding({
|
|
3731
|
-
entity_id: newSnapshot.entity_id,
|
|
3732
|
-
entity_type: newSnapshot.entity_type,
|
|
3733
|
-
schema_version: newSnapshot.schema_version,
|
|
3734
|
-
snapshot: newSnapshot.snapshot,
|
|
3735
|
-
computed_at: newSnapshot.computed_at,
|
|
3736
|
-
observation_count: newSnapshot.observation_count,
|
|
3737
|
-
last_observation_at: newSnapshot.last_observation_at,
|
|
3738
|
-
provenance: newSnapshot.provenance,
|
|
3739
|
-
user_id: newSnapshot.user_id,
|
|
3740
|
-
});
|
|
3741
|
-
await upsertEntitySnapshotWithEmbedding(rowWithEmbedding);
|
|
3742
|
-
recomputed++;
|
|
3922
|
+
const snapshot = await recomputeSnapshot(entityId, userId);
|
|
3923
|
+
if (snapshot)
|
|
3924
|
+
recomputed++;
|
|
3743
3925
|
}
|
|
3744
3926
|
catch (err) {
|
|
3745
3927
|
errors++;
|