@mastra/factory 0.21.0-alpha.3 → 0.21.0-alpha.4
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/auth.d.ts +7 -0
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +12 -1
- package/dist/auth.js.map +1 -1
- package/dist/factory.d.ts +9 -0
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +56 -23
- package/dist/factory.js.map +1 -1
- package/dist/integrations/github/routes.js +1 -0
- package/dist/integrations/github/routes.js.map +1 -1
- package/dist/integrations/linear/issue-reconciler.d.ts +3 -1
- package/dist/integrations/linear/issue-reconciler.d.ts.map +1 -1
- package/dist/integrations/linear/issue-reconciler.js +1 -1
- package/dist/integrations/linear/issue-reconciler.js.map +1 -1
- package/dist/integrations/platform/linear/integration.d.ts.map +1 -1
- package/dist/integrations/platform/linear/integration.js +10 -9
- package/dist/integrations/platform/linear/integration.js.map +1 -1
- package/dist/integrations/slack/integration.d.ts.map +1 -1
- package/dist/integrations/slack/integration.js +0 -1
- package/dist/integrations/slack/integration.js.map +1 -1
- package/dist/integrations/slack/slack.d.ts +0 -17
- package/dist/integrations/slack/slack.d.ts.map +1 -1
- package/dist/integrations/slack/slack.js +8 -40
- package/dist/integrations/slack/slack.js.map +1 -1
- package/dist/routes/config.d.ts +15 -36
- package/dist/routes/config.d.ts.map +1 -1
- package/dist/routes/config.js +72 -123
- package/dist/routes/config.js.map +1 -1
- package/dist/routes/knowledge.d.ts +115 -21
- package/dist/routes/knowledge.d.ts.map +1 -1
- package/dist/routes/knowledge.js +715 -30
- package/dist/routes/knowledge.js.map +1 -1
- package/dist/routes/oauth.d.ts +0 -3
- package/dist/routes/oauth.d.ts.map +1 -1
- package/dist/routes/oauth.js +2 -10
- package/dist/routes/oauth.js.map +1 -1
- package/dist/routes/provider-credentials.d.ts.map +1 -1
- package/dist/routes/provider-credentials.js +2 -1
- package/dist/routes/provider-credentials.js.map +1 -1
- package/dist/routes/surface.d.ts +1 -0
- package/dist/routes/surface.d.ts.map +1 -1
- package/dist/routes/surface.js +6 -3
- package/dist/routes/surface.js.map +1 -1
- package/dist/rules/binding-context.d.ts +19 -0
- package/dist/rules/binding-context.d.ts.map +1 -1
- package/dist/rules/binding-context.js +39 -1
- package/dist/rules/binding-context.js.map +1 -1
- package/dist/rules/dispatcher.d.ts +2 -13
- package/dist/rules/dispatcher.d.ts.map +1 -1
- package/dist/rules/dispatcher.js +0 -6
- package/dist/rules/dispatcher.js.map +1 -1
- package/dist/rules/processor.d.ts +35 -1
- package/dist/rules/processor.d.ts.map +1 -1
- package/dist/rules/processor.js +80 -4
- package/dist/rules/processor.js.map +1 -1
- package/dist/rules/start-coordinator.d.ts +1 -2
- package/dist/rules/start-coordinator.d.ts.map +1 -1
- package/dist/rules/start-coordinator.js +2 -6
- package/dist/rules/start-coordinator.js.map +1 -1
- package/dist/session/factory-session.d.ts +5 -43
- package/dist/session/factory-session.d.ts.map +1 -1
- package/dist/session/factory-session.js +6 -47
- package/dist/session/factory-session.js.map +1 -1
- package/dist/session/memory-settings-hydration.d.ts +17 -89
- package/dist/session/memory-settings-hydration.d.ts.map +1 -1
- package/dist/session/memory-settings-hydration.js +25 -111
- package/dist/session/memory-settings-hydration.js.map +1 -1
- package/dist/storage/domains/memory-settings/base.d.ts +5 -17
- package/dist/storage/domains/memory-settings/base.d.ts.map +1 -1
- package/dist/storage/domains/memory-settings/base.js +8 -15
- package/dist/storage/domains/memory-settings/base.js.map +1 -1
- package/dist/supervisor/session.d.ts +2 -4
- package/dist/supervisor/session.d.ts.map +1 -1
- package/dist/supervisor/session.js +3 -5
- package/dist/supervisor/session.js.map +1 -1
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +2 -2
- package/dist/workspace.js.map +1 -1
- package/package.json +7 -7
- package/dist/routes/om-seed.d.ts +0 -22
- package/dist/routes/om-seed.d.ts.map +0 -1
- package/dist/routes/om-seed.js +0 -40
- package/dist/routes/om-seed.js.map +0 -1
package/dist/routes/knowledge.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Route } from "./route.js";
|
|
2
2
|
import { registerApiRoute } from "@mastra/core/server";
|
|
3
|
-
import { canonicalizeKnowledgeScope, isKnowledgeScopeVisible, knowledgeScopeKey, parseKnowledgeWikilinks } from "@mastra/core/storage";
|
|
3
|
+
import { KnowledgeUnsupportedCapabilityError, canonicalizeKnowledgeScope, isKnowledgeScopeVisible, knowledgeScopeKey, parseKnowledgeWikilinks } from "@mastra/core/storage";
|
|
4
4
|
//#region src/routes/knowledge.ts
|
|
5
5
|
/** Reserved node that anchors pinned records (see subconscious/pinned.ts). */
|
|
6
6
|
const PINNED_NODE_NAME = "pinned";
|
|
@@ -10,10 +10,27 @@ function truncateRecordText(text) {
|
|
|
10
10
|
return text.length > RECORD_TEXT_LIMIT ? `${text.slice(0, RECORD_TEXT_LIMIT - 1)}…` : text;
|
|
11
11
|
}
|
|
12
12
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
13
|
+
/** Authorized visible events returned per activity page. */
|
|
14
|
+
const ACTIVITY_PAGE_SIZE = 25;
|
|
15
|
+
/** Events fetched per `listActivity` batch while filling a visible page. */
|
|
16
|
+
const ACTIVITY_BATCH = 100;
|
|
17
|
+
/** Hard scan cap so a huge hidden backlog cannot spin the request. */
|
|
18
|
+
const ACTIVITY_SCAN_CAP = 1e3;
|
|
19
|
+
/** Maximum visible nodes inspected for non-prefix name matches. */
|
|
20
|
+
const SEARCH_SCAN_CAP = 1e3;
|
|
21
|
+
/** Compact result window returned to the search popover. */
|
|
22
|
+
const SEARCH_RESULT_LIMIT = 20;
|
|
23
|
+
/** Prevent an unbounded query string from reaching storage. */
|
|
24
|
+
const SEARCH_QUERY_LIMIT = 128;
|
|
25
|
+
/** Scope nodes returned per parent while lazily expanding the tree. */
|
|
26
|
+
const SCOPE_PAGE_SIZE = 25;
|
|
27
|
+
/** Bound storage pressure while calculating direct-member counts. */
|
|
28
|
+
const SCOPE_COUNT_CONCURRENCY = 8;
|
|
13
29
|
const DEFAULT_LIMITS = {
|
|
14
30
|
maxNodes: 500,
|
|
15
31
|
maxRecords: 2e3,
|
|
16
|
-
maxFallbackLookups: 100
|
|
32
|
+
maxFallbackLookups: 100,
|
|
33
|
+
maxOrgScopePages: 10
|
|
17
34
|
};
|
|
18
35
|
function loose(c) {
|
|
19
36
|
return c;
|
|
@@ -27,6 +44,80 @@ function deepestRung(scope) {
|
|
|
27
44
|
}
|
|
28
45
|
return rung;
|
|
29
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* True when `scope` is a prefix of the active view scope — the content lives
|
|
49
|
+
* inside the current org + project boundary. Structural member lenses use
|
|
50
|
+
* this to keep sibling-project knowledge out of a project-scoped view.
|
|
51
|
+
*/
|
|
52
|
+
function withinViewBoundary(scope, viewScope) {
|
|
53
|
+
return scope.length <= viewScope.length && scope.every((entry, index) => entry === viewScope[index]);
|
|
54
|
+
}
|
|
55
|
+
function directScopeMemberCounts({ members, hasMore }, childScopeCount, viewScope, maxNodes) {
|
|
56
|
+
const contentNodeCount = members.filter((member) => !member.isScope && Array.isArray(member.scope) && withinViewBoundary(member.scope, viewScope)).length;
|
|
57
|
+
const directMemberCount = childScopeCount + contentNodeCount;
|
|
58
|
+
return {
|
|
59
|
+
memberCount: Math.min(directMemberCount, maxNodes),
|
|
60
|
+
memberCountTruncated: directMemberCount > maxNodes || hasMore,
|
|
61
|
+
contentNodeCount,
|
|
62
|
+
childScopeCount
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function compareScopeNodes(a, b) {
|
|
66
|
+
return a.name.localeCompare(b.name) || a.id.localeCompare(b.id);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Scopes in one org (the org scope and everything beneath it), filtered and paged in storage.
|
|
70
|
+
* Reads at most `maxPages` pages; `truncated` is true when scopes remain past that point.
|
|
71
|
+
*/
|
|
72
|
+
async function listOrgScopeNodes(store, orgId, maxPages) {
|
|
73
|
+
const scopes = [];
|
|
74
|
+
let cursor;
|
|
75
|
+
for (let page = 0; page < maxPages; page += 1) {
|
|
76
|
+
const result = await store.listScopeNodes({
|
|
77
|
+
withinAddress: `org:${orgId}`,
|
|
78
|
+
cursor
|
|
79
|
+
});
|
|
80
|
+
scopes.push(...result.scopes);
|
|
81
|
+
if (!result.nextCursor) return {
|
|
82
|
+
scopes,
|
|
83
|
+
truncated: false
|
|
84
|
+
};
|
|
85
|
+
cursor = result.nextCursor;
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
scopes,
|
|
89
|
+
truncated: true
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/** One scope node, only if it is in the org. Independent of how many scopes the org has. */
|
|
93
|
+
async function findOrgScopeNode(store, orgId, scopeNodeId) {
|
|
94
|
+
const { scopes } = await store.listScopeNodes({
|
|
95
|
+
withinAddress: `org:${orgId}`,
|
|
96
|
+
ids: [scopeNodeId],
|
|
97
|
+
limit: 1
|
|
98
|
+
});
|
|
99
|
+
return scopes[0];
|
|
100
|
+
}
|
|
101
|
+
function knowledgeSearchRank(name, query) {
|
|
102
|
+
const normalized = name.toLocaleLowerCase();
|
|
103
|
+
if (normalized === query) return 0;
|
|
104
|
+
if (normalized.startsWith(query)) return 1;
|
|
105
|
+
return 2;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* What a scope node is, for labels: the rung name for vouched identity scopes, otherwise the
|
|
109
|
+
* stored kind, or "topic" for structural scopes created without one. Matches the scope list.
|
|
110
|
+
*/
|
|
111
|
+
function scopeKindLabel(address, kind) {
|
|
112
|
+
if (address.startsWith("org:")) return "org";
|
|
113
|
+
if (address.startsWith("resource:")) return "project";
|
|
114
|
+
if (address.startsWith("thread:")) return "session";
|
|
115
|
+
return kind || "topic";
|
|
116
|
+
}
|
|
117
|
+
/** The thread id of a session (`thread:<id>`) scope node, or undefined for any other scope. */
|
|
118
|
+
function sessionThreadId(node) {
|
|
119
|
+
return node.address.startsWith("thread:") ? boundedThreadId(node.address.slice(7)) : void 0;
|
|
120
|
+
}
|
|
30
121
|
function boundedThreadId(raw) {
|
|
31
122
|
if (!raw) return void 0;
|
|
32
123
|
const trimmed = raw.trim();
|
|
@@ -94,7 +185,9 @@ var WikilinkResolver = class {
|
|
|
94
185
|
#trackOutOfWindow(node) {
|
|
95
186
|
if (node && !this.#windowIds.has(node.id)) this.outOfWindow.set(node.id, {
|
|
96
187
|
id: node.id,
|
|
97
|
-
name: node.name
|
|
188
|
+
name: node.name,
|
|
189
|
+
scope: node.scope,
|
|
190
|
+
rung: deepestRung(node.scope)
|
|
98
191
|
});
|
|
99
192
|
return node;
|
|
100
193
|
}
|
|
@@ -143,14 +236,23 @@ var KnowledgeRoutes = class extends Route {
|
|
|
143
236
|
if (!projectId || !UUID_RE.test(projectId)) return { response: c.json({ error: "Project not found" }, 404) };
|
|
144
237
|
const { projects } = this.deps;
|
|
145
238
|
await projects.ensureReady();
|
|
146
|
-
|
|
239
|
+
const project = await projects.get({
|
|
147
240
|
orgId: tenant.orgId,
|
|
148
241
|
id: projectId
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
|
|
242
|
+
});
|
|
243
|
+
if (!project) return { response: c.json({ error: "Project not found" }, 404) };
|
|
244
|
+
let knowledge;
|
|
245
|
+
let store;
|
|
246
|
+
try {
|
|
247
|
+
const key = this.deps.defaultKnowledgeKey ?? "default";
|
|
248
|
+
knowledge = await this.deps.knowledge(key);
|
|
249
|
+
store = knowledge ? await knowledge.getStorage() : void 0;
|
|
250
|
+
} catch {
|
|
251
|
+
store = void 0;
|
|
252
|
+
}
|
|
253
|
+
if (!store || !knowledge) return { response: c.json({
|
|
152
254
|
error: "knowledge_unavailable",
|
|
153
|
-
message: "The
|
|
255
|
+
message: "The configured Knowledge runtime is unavailable."
|
|
154
256
|
}, 503) };
|
|
155
257
|
const defaultScope = [`org:${tenant.orgId}`, `resource:${projectId}`];
|
|
156
258
|
const resourceRungScope = defaultScope;
|
|
@@ -159,6 +261,8 @@ var KnowledgeRoutes = class extends Route {
|
|
|
159
261
|
if (!threadId) return {
|
|
160
262
|
...tenant,
|
|
161
263
|
factoryProjectId: projectId,
|
|
264
|
+
factoryProjectName: project.name,
|
|
265
|
+
knowledge,
|
|
162
266
|
store,
|
|
163
267
|
view: "project",
|
|
164
268
|
scope: defaultScope,
|
|
@@ -176,6 +280,8 @@ var KnowledgeRoutes = class extends Route {
|
|
|
176
280
|
return {
|
|
177
281
|
...tenant,
|
|
178
282
|
factoryProjectId: projectId,
|
|
283
|
+
factoryProjectName: project.name,
|
|
284
|
+
knowledge,
|
|
179
285
|
store,
|
|
180
286
|
view: "thread",
|
|
181
287
|
threadId,
|
|
@@ -189,6 +295,122 @@ var KnowledgeRoutes = class extends Route {
|
|
|
189
295
|
}]
|
|
190
296
|
};
|
|
191
297
|
}
|
|
298
|
+
#selectedView(view, level) {
|
|
299
|
+
const projectView = {
|
|
300
|
+
orgId: view.orgId,
|
|
301
|
+
userId: view.userId,
|
|
302
|
+
factoryProjectId: view.factoryProjectId,
|
|
303
|
+
factoryProjectName: view.factoryProjectName,
|
|
304
|
+
knowledge: view.knowledge,
|
|
305
|
+
store: view.store,
|
|
306
|
+
view: "project"
|
|
307
|
+
};
|
|
308
|
+
if (level === "org") return {
|
|
309
|
+
...projectView,
|
|
310
|
+
scope: view.scope.slice(0, 1),
|
|
311
|
+
pinRungs: []
|
|
312
|
+
};
|
|
313
|
+
if (level === "resource") return {
|
|
314
|
+
...projectView,
|
|
315
|
+
scope: view.scope.slice(0, 2),
|
|
316
|
+
pinRungs: view.pinRungs.filter((rung) => rung.rung === "resource")
|
|
317
|
+
};
|
|
318
|
+
if (level === "thread" && view.threadId) return view;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Host-vouched identity chain: the org → project → session rungs this
|
|
322
|
+
* request resolved are materialized as ordinary scope nodes linked by
|
|
323
|
+
* membership edges, so the sidebar tree is built from scopes that EXIST —
|
|
324
|
+
* never from the declared structure plan or the rung config. Materialization
|
|
325
|
+
* is create-only and deduped by address (existing nodes keep their declared
|
|
326
|
+
* names/parents), so the steady state is a no-op skip. No companion scope is
|
|
327
|
+
* created here or anywhere automatically. A vouch failure never fails the read —
|
|
328
|
+
* the tree simply serves the scopes that already exist.
|
|
329
|
+
*/
|
|
330
|
+
async #vouchIdentityScopes(view) {
|
|
331
|
+
const orgAddress = `org:${view.orgId}`;
|
|
332
|
+
const resourceAddress = `resource:${view.factoryProjectId}`;
|
|
333
|
+
const chain = [
|
|
334
|
+
{
|
|
335
|
+
address: orgAddress,
|
|
336
|
+
contextualScopeAddress: orgAddress
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
address: resourceAddress,
|
|
340
|
+
name: view.factoryProjectName,
|
|
341
|
+
contextualScopeAddress: orgAddress,
|
|
342
|
+
parentAddresses: [orgAddress]
|
|
343
|
+
},
|
|
344
|
+
...view.threadId ? [{
|
|
345
|
+
address: `thread:${view.threadId}`,
|
|
346
|
+
contextualScopeAddress: resourceAddress,
|
|
347
|
+
parentAddresses: [resourceAddress]
|
|
348
|
+
}] : []
|
|
349
|
+
];
|
|
350
|
+
let existing;
|
|
351
|
+
try {
|
|
352
|
+
const { scopes } = await view.store.listScopeNodes({ addresses: chain.map((link) => link.address) });
|
|
353
|
+
existing = new Set(scopes.map((node) => node.address));
|
|
354
|
+
} catch (error) {
|
|
355
|
+
if (error instanceof KnowledgeUnsupportedCapabilityError) return;
|
|
356
|
+
throw error;
|
|
357
|
+
}
|
|
358
|
+
for (const link of chain) {
|
|
359
|
+
if (existing.has(link.address)) continue;
|
|
360
|
+
await view.knowledge.materializeScope(link).catch(() => void 0);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Session scopes are listed only under the viewed project, and only when the `threadId`
|
|
365
|
+
* drill-down would accept them (a visible record captured in that session), so no listing
|
|
366
|
+
* exposes a session id the drill-down would reject. Returns the ids of hidden session nodes.
|
|
367
|
+
* With `probe: false` only the cheap project check runs; callers probe the bounded page they return.
|
|
368
|
+
*/
|
|
369
|
+
async #hiddenSessionIds(view, nodes, projectNodeId, { probe }) {
|
|
370
|
+
const hidden = /* @__PURE__ */ new Set();
|
|
371
|
+
const projectScope = [`org:${view.orgId}`, `resource:${view.factoryProjectId}`];
|
|
372
|
+
const sessions = [];
|
|
373
|
+
for (const node of nodes) {
|
|
374
|
+
if (!node.address.startsWith("thread:")) continue;
|
|
375
|
+
const threadId = sessionThreadId(node);
|
|
376
|
+
if (!threadId || !projectNodeId || !node.parentIds.includes(projectNodeId)) hidden.add(node.id);
|
|
377
|
+
else if (probe && threadId !== view.threadId) sessions.push({
|
|
378
|
+
id: node.id,
|
|
379
|
+
threadId
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
for (let index = 0; index < sessions.length; index += SCOPE_COUNT_CONCURRENCY) await Promise.all(sessions.slice(index, index + SCOPE_COUNT_CONCURRENCY).map(async ({ id, threadId }) => {
|
|
383
|
+
const { records } = await view.store.knowledgeBySource({
|
|
384
|
+
sourceThreadId: threadId,
|
|
385
|
+
scope: [...projectScope, `thread:${threadId}`],
|
|
386
|
+
limit: 1
|
|
387
|
+
});
|
|
388
|
+
if (records.length === 0) hidden.add(id);
|
|
389
|
+
}));
|
|
390
|
+
return hidden;
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Display names for session scopes, resolved at read time from the thread's current title
|
|
394
|
+
* (never stored on the scope node, where it would go stale). Keyed by scope node id.
|
|
395
|
+
*/
|
|
396
|
+
async #sessionNames(nodes) {
|
|
397
|
+
const names = /* @__PURE__ */ new Map();
|
|
398
|
+
const sessions = nodes.flatMap((node) => {
|
|
399
|
+
const threadId = node.address.startsWith("thread:") ? node.address.slice(7) : "";
|
|
400
|
+
return threadId ? [{
|
|
401
|
+
id: node.id,
|
|
402
|
+
threadId
|
|
403
|
+
}] : [];
|
|
404
|
+
});
|
|
405
|
+
for (let index = 0; index < sessions.length; index += SCOPE_COUNT_CONCURRENCY) await Promise.all(sessions.slice(index, index + SCOPE_COUNT_CONCURRENCY).map(async ({ id, threadId }) => {
|
|
406
|
+
let title;
|
|
407
|
+
try {
|
|
408
|
+
title = (await this.deps.threadTitle?.(threadId))?.trim();
|
|
409
|
+
} catch {}
|
|
410
|
+
names.set(id, title || `session ${threadId.slice(0, 8)}`);
|
|
411
|
+
}));
|
|
412
|
+
return names;
|
|
413
|
+
}
|
|
192
414
|
/** Reserved `pinned` node ids at the active view's rungs (one exact-scope lookup per rung). */
|
|
193
415
|
async #pinnedNodeIds(view) {
|
|
194
416
|
const out = [];
|
|
@@ -222,18 +444,383 @@ var KnowledgeRoutes = class extends Route {
|
|
|
222
444
|
}
|
|
223
445
|
routes() {
|
|
224
446
|
return [
|
|
225
|
-
registerApiRoute("/web/factory/projects/:id/knowledge/
|
|
447
|
+
registerApiRoute("/web/factory/projects/:id/knowledge/scopes", {
|
|
448
|
+
method: "GET",
|
|
449
|
+
requiresAuth: false,
|
|
450
|
+
handler: async (c) => {
|
|
451
|
+
const view = await this.#resolveView(loose(c));
|
|
452
|
+
if ("response" in view) return view.response;
|
|
453
|
+
await this.#vouchIdentityScopes(view);
|
|
454
|
+
let scopeNodes;
|
|
455
|
+
let nextCursor;
|
|
456
|
+
const childCursors = {};
|
|
457
|
+
let storedScopeNodes = [];
|
|
458
|
+
let scopesTruncated = false;
|
|
459
|
+
try {
|
|
460
|
+
({scopes: storedScopeNodes, truncated: scopesTruncated} = await listOrgScopeNodes(view.store, view.orgId, this.#limits.maxOrgScopePages));
|
|
461
|
+
const projectNodeId = storedScopeNodes.find((node) => node.address === `resource:${view.factoryProjectId}`)?.id;
|
|
462
|
+
const foreignSessions = await this.#hiddenSessionIds(view, storedScopeNodes, projectNodeId, { probe: false });
|
|
463
|
+
storedScopeNodes = storedScopeNodes.filter((node) => !foreignSessions.has(node.id));
|
|
464
|
+
const scopeNodeById = new Map(storedScopeNodes.map((node) => [node.id, node]));
|
|
465
|
+
const childScopeCountByParent = /* @__PURE__ */ new Map();
|
|
466
|
+
for (const node of storedScopeNodes) for (const parentId of node.parentIds) childScopeCountByParent.set(parentId, (childScopeCountByParent.get(parentId) ?? 0) + 1);
|
|
467
|
+
const parentId = loose(c).req.query("parentId");
|
|
468
|
+
const cursor = loose(c).req.query("cursor");
|
|
469
|
+
if (parentId && !UUID_RE.test(parentId)) return c.json({ error: "invalid_scope_parent" }, 400);
|
|
470
|
+
if (cursor && !UUID_RE.test(cursor)) return c.json({ error: "invalid_scope_cursor" }, 400);
|
|
471
|
+
if (parentId && !scopeNodeById.has(parentId)) return c.json({ error: "scope_not_found" }, 404);
|
|
472
|
+
const siblings = storedScopeNodes.filter((node) => parentId ? node.parentIds.includes(parentId) : node.parentIds.length === 0).sort(compareScopeNodes);
|
|
473
|
+
const cursorIndex = cursor ? siblings.findIndex((node) => node.id === cursor) : -1;
|
|
474
|
+
if (cursor && cursorIndex < 0) return c.json({ error: "invalid_scope_cursor" }, 400);
|
|
475
|
+
const pageStart = cursorIndex + 1;
|
|
476
|
+
const siblingPage = siblings.slice(pageStart, pageStart + SCOPE_PAGE_SIZE);
|
|
477
|
+
if (pageStart + siblingPage.length < siblings.length) nextCursor = siblingPage.at(-1)?.id;
|
|
478
|
+
const returnedById = new Map(siblingPage.map((node) => [node.id, node]));
|
|
479
|
+
if (!parentId) {
|
|
480
|
+
const addFirstChildPage = (parent) => {
|
|
481
|
+
const children = storedScopeNodes.filter((node) => node.parentIds.includes(parent.id)).sort(compareScopeNodes);
|
|
482
|
+
const returnedChildren = [];
|
|
483
|
+
for (const child of children.slice(0, SCOPE_PAGE_SIZE)) {
|
|
484
|
+
if (returnedById.size >= this.#limits.maxNodes) break;
|
|
485
|
+
returnedById.set(child.id, child);
|
|
486
|
+
returnedChildren.push(child);
|
|
487
|
+
}
|
|
488
|
+
if (returnedChildren.length < children.length) {
|
|
489
|
+
const lastChild = returnedChildren.at(-1);
|
|
490
|
+
if (lastChild) childCursors[parent.id] = lastChild.id;
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
for (const root of siblingPage) addFirstChildPage(root);
|
|
494
|
+
const identityAddresses = /* @__PURE__ */ new Set([
|
|
495
|
+
`org:${view.orgId}`,
|
|
496
|
+
`resource:${view.factoryProjectId}`,
|
|
497
|
+
...view.threadId ? [`thread:${view.threadId}`] : []
|
|
498
|
+
]);
|
|
499
|
+
const includeWithParents = (node) => {
|
|
500
|
+
if (returnedById.has(node.id) || returnedById.size >= this.#limits.maxNodes) return;
|
|
501
|
+
for (const ancestorId of node.parentIds) {
|
|
502
|
+
const ancestor = scopeNodeById.get(ancestorId);
|
|
503
|
+
if (ancestor) includeWithParents(ancestor);
|
|
504
|
+
}
|
|
505
|
+
returnedById.set(node.id, node);
|
|
506
|
+
};
|
|
507
|
+
for (const node of storedScopeNodes) if (identityAddresses.has(node.address)) includeWithParents(node);
|
|
508
|
+
const projectNode = projectNodeId ? scopeNodeById.get(projectNodeId) : void 0;
|
|
509
|
+
if (projectNode && returnedById.has(projectNode.id)) addFirstChildPage(projectNode);
|
|
510
|
+
}
|
|
511
|
+
const unreadableSessions = await this.#hiddenSessionIds(view, [...returnedById.values()], projectNodeId, { probe: true });
|
|
512
|
+
for (const id of unreadableSessions) returnedById.delete(id);
|
|
513
|
+
scopeNodes = [];
|
|
514
|
+
const returnedNodes = [...returnedById.values()].sort(compareScopeNodes);
|
|
515
|
+
for (let index = 0; index < returnedNodes.length; index += SCOPE_COUNT_CONCURRENCY) {
|
|
516
|
+
const batch = returnedNodes.slice(index, index + SCOPE_COUNT_CONCURRENCY);
|
|
517
|
+
scopeNodes.push(...await Promise.all(batch.map(async (node) => {
|
|
518
|
+
const members = await view.store.listScopeMembers({
|
|
519
|
+
scopeNodeId: node.id,
|
|
520
|
+
limit: this.#limits.maxNodes
|
|
521
|
+
});
|
|
522
|
+
return {
|
|
523
|
+
...node,
|
|
524
|
+
...node.address === `resource:${view.factoryProjectId}` ? { name: view.factoryProjectName } : {},
|
|
525
|
+
...directScopeMemberCounts(members, childScopeCountByParent.get(node.id) ?? 0, view.scope, this.#limits.maxNodes)
|
|
526
|
+
};
|
|
527
|
+
})));
|
|
528
|
+
}
|
|
529
|
+
const sessionNames = await this.#sessionNames(scopeNodes);
|
|
530
|
+
scopeNodes = scopeNodes.map((node) => ({
|
|
531
|
+
...node,
|
|
532
|
+
kind: scopeKindLabel(node.address, node.kind),
|
|
533
|
+
...sessionNames.has(node.id) ? { name: sessionNames.get(node.id) } : {}
|
|
534
|
+
}));
|
|
535
|
+
} catch (error) {
|
|
536
|
+
if (!(error instanceof KnowledgeUnsupportedCapabilityError)) throw error;
|
|
537
|
+
scopeNodes = void 0;
|
|
538
|
+
}
|
|
539
|
+
const scopeNodeByAddress = new Map(storedScopeNodes.map((node) => [node.address, node]));
|
|
540
|
+
const threadRoot = view.threadId ? scopeNodeByAddress.get(`thread:${view.threadId}`) : void 0;
|
|
541
|
+
const threadRootName = threadRoot ? (await this.#sessionNames([threadRoot])).get(threadRoot.id) : void 0;
|
|
542
|
+
const rungs = [
|
|
543
|
+
{
|
|
544
|
+
level: "org",
|
|
545
|
+
id: view.orgId,
|
|
546
|
+
available: true
|
|
547
|
+
},
|
|
548
|
+
{
|
|
549
|
+
level: "resource",
|
|
550
|
+
id: view.factoryProjectId,
|
|
551
|
+
available: true
|
|
552
|
+
},
|
|
553
|
+
...view.threadId ? [{
|
|
554
|
+
level: "thread",
|
|
555
|
+
id: view.threadId,
|
|
556
|
+
available: true
|
|
557
|
+
}] : []
|
|
558
|
+
];
|
|
559
|
+
return c.json({
|
|
560
|
+
roots: rungs.map((rung) => {
|
|
561
|
+
const match = scopeNodeByAddress.get(`${rung.level}:${rung.id}`);
|
|
562
|
+
const name = match?.address === `resource:${view.factoryProjectId}` ? view.factoryProjectName : match && rung.level === "thread" ? threadRootName : match?.name;
|
|
563
|
+
return match ? {
|
|
564
|
+
...rung,
|
|
565
|
+
scopeNodeId: match.id,
|
|
566
|
+
name
|
|
567
|
+
} : rung;
|
|
568
|
+
}),
|
|
569
|
+
defaultLevel: "resource",
|
|
570
|
+
...scopeNodes ? { scopeNodes } : {},
|
|
571
|
+
...nextCursor ? { nextCursor } : {},
|
|
572
|
+
...Object.keys(childCursors).length > 0 ? { childCursors } : {},
|
|
573
|
+
...scopesTruncated ? { truncated: true } : {}
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
}),
|
|
577
|
+
registerApiRoute("/web/factory/projects/:id/knowledge/search", {
|
|
226
578
|
method: "GET",
|
|
227
579
|
requiresAuth: false,
|
|
228
580
|
handler: async (c) => {
|
|
229
581
|
const view = await this.#resolveView(loose(c));
|
|
230
582
|
if ("response" in view) return view.response;
|
|
583
|
+
const query = (loose(c).req.query("q") ?? "").trim().slice(0, SEARCH_QUERY_LIMIT).toLocaleLowerCase();
|
|
584
|
+
if (query.length < 2) return c.json({
|
|
585
|
+
results: [],
|
|
586
|
+
truncated: false
|
|
587
|
+
});
|
|
588
|
+
const scanLimit = Math.min(SEARCH_SCAN_CAP, this.#limits.maxNodes);
|
|
589
|
+
const [prefixNodes, scannedNodes] = await Promise.all([view.store.listNodes({
|
|
590
|
+
scope: view.scope,
|
|
591
|
+
namePrefix: query,
|
|
592
|
+
limit: 21
|
|
593
|
+
}), view.store.listNodes({
|
|
594
|
+
scope: view.scope,
|
|
595
|
+
limit: scanLimit
|
|
596
|
+
})]);
|
|
597
|
+
const contentById = /* @__PURE__ */ new Map();
|
|
598
|
+
for (const node of [...prefixNodes, ...scannedNodes]) {
|
|
599
|
+
if (node.isScope || !Array.isArray(node.scope) || !withinViewBoundary(node.scope, view.scope) || !node.name.toLocaleLowerCase().includes(query)) continue;
|
|
600
|
+
contentById.set(node.id, node);
|
|
601
|
+
}
|
|
602
|
+
let structuralScopes = [];
|
|
603
|
+
let scopesTruncated = false;
|
|
604
|
+
try {
|
|
605
|
+
const orgScopes = await listOrgScopeNodes(view.store, view.orgId, this.#limits.maxOrgScopePages);
|
|
606
|
+
scopesTruncated = orgScopes.truncated;
|
|
607
|
+
structuralScopes = orgScopes.scopes.filter((node) => (!node.address.startsWith("thread:") || sessionThreadId(node) === view.threadId) && node.name.toLocaleLowerCase().includes(query));
|
|
608
|
+
} catch (error) {
|
|
609
|
+
if (!(error instanceof KnowledgeUnsupportedCapabilityError)) throw error;
|
|
610
|
+
}
|
|
611
|
+
const scopeSessionNames = await this.#sessionNames(structuralScopes);
|
|
612
|
+
const results = [...structuralScopes.map((scopeNode) => {
|
|
613
|
+
const rung = scopeNode.address === `org:${view.orgId}` ? "org" : scopeNode.address === `resource:${view.factoryProjectId}` ? "resource" : view.threadId && scopeNode.address === `thread:${view.threadId}` ? "thread" : null;
|
|
614
|
+
return {
|
|
615
|
+
id: scopeNode.id,
|
|
616
|
+
name: scopeNode.address === `resource:${view.factoryProjectId}` ? view.factoryProjectName : scopeSessionNames.get(scopeNode.id) ?? scopeNode.name,
|
|
617
|
+
kind: scopeKindLabel(scopeNode.address, scopeNode.kind),
|
|
618
|
+
type: "scope",
|
|
619
|
+
rung,
|
|
620
|
+
address: scopeNode.address,
|
|
621
|
+
...scopeNode.description ? { description: scopeNode.description } : {}
|
|
622
|
+
};
|
|
623
|
+
}), ...[...contentById.values()].map((node) => {
|
|
624
|
+
const rung = deepestRung(node.scope);
|
|
625
|
+
const threadId = rung === "thread" ? node.scope.find((address) => address.startsWith("thread:"))?.slice(7) : void 0;
|
|
626
|
+
return {
|
|
627
|
+
id: node.id,
|
|
628
|
+
name: node.name,
|
|
629
|
+
kind: node.kind,
|
|
630
|
+
type: "node",
|
|
631
|
+
rung,
|
|
632
|
+
...threadId ? { threadId } : {}
|
|
633
|
+
};
|
|
634
|
+
})].sort((a, b) => knowledgeSearchRank(a.name, query) - knowledgeSearchRank(b.name, query) || a.name.localeCompare(b.name) || a.id.localeCompare(b.id));
|
|
635
|
+
return c.json({
|
|
636
|
+
results: results.slice(0, SEARCH_RESULT_LIMIT),
|
|
637
|
+
truncated: scopesTruncated || results.length > SEARCH_RESULT_LIMIT || prefixNodes.length > SEARCH_RESULT_LIMIT || scannedNodes.length >= scanLimit
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
}),
|
|
641
|
+
registerApiRoute("/web/factory/projects/:id/knowledge/subgraph", {
|
|
642
|
+
method: "GET",
|
|
643
|
+
requiresAuth: false,
|
|
644
|
+
handler: async (c) => {
|
|
645
|
+
const resolved = await this.#resolveView(loose(c));
|
|
646
|
+
if ("response" in resolved) return resolved.response;
|
|
647
|
+
const scopeNodeId = loose(c).req.query("scopeNodeId");
|
|
648
|
+
if (scopeNodeId !== void 0) {
|
|
649
|
+
if (!UUID_RE.test(scopeNodeId)) return c.json({ error: "scope_not_found" }, 404);
|
|
650
|
+
const limits = this.#limits;
|
|
651
|
+
const { store } = resolved;
|
|
652
|
+
let storedRoot;
|
|
653
|
+
try {
|
|
654
|
+
storedRoot = await findOrgScopeNode(store, resolved.orgId, scopeNodeId);
|
|
655
|
+
} catch (error) {
|
|
656
|
+
if (error instanceof KnowledgeUnsupportedCapabilityError) return c.json({ error: "scope_not_found" }, 404);
|
|
657
|
+
throw error;
|
|
658
|
+
}
|
|
659
|
+
if (!storedRoot) return c.json({ error: "scope_not_found" }, 404);
|
|
660
|
+
const { scopes: scopeNodes, truncated: scopesTruncated } = await listOrgScopeNodes(store, resolved.orgId, limits.maxOrgScopePages);
|
|
661
|
+
const rootSessionName = (await this.#sessionNames([storedRoot])).get(storedRoot.id);
|
|
662
|
+
const root = storedRoot.address === `resource:${resolved.factoryProjectId}` ? {
|
|
663
|
+
...storedRoot,
|
|
664
|
+
name: resolved.factoryProjectName
|
|
665
|
+
} : rootSessionName ? {
|
|
666
|
+
...storedRoot,
|
|
667
|
+
name: rootSessionName
|
|
668
|
+
} : storedRoot;
|
|
669
|
+
if (storedRoot.address.startsWith("thread:")) {
|
|
670
|
+
const projectNodeId = scopeNodes.find((node) => node.address === `resource:${resolved.factoryProjectId}`)?.id;
|
|
671
|
+
if ((await this.#hiddenSessionIds(resolved, [storedRoot], projectNodeId, { probe: true })).has(storedRoot.id)) return c.json({ error: "scope_not_found" }, 404);
|
|
672
|
+
}
|
|
673
|
+
const otherSessionIds = new Set(scopeNodes.filter((node) => node.address.startsWith("thread:") && sessionThreadId(node) !== resolved.threadId).map((node) => node.id));
|
|
674
|
+
const fetched = await store.listScopeMembers({
|
|
675
|
+
scopeNodeId,
|
|
676
|
+
limit: limits.maxNodes
|
|
677
|
+
});
|
|
678
|
+
const bounded = fetched.members.filter((node) => node.isScope && !otherSessionIds.has(node.id) || !node.isScope && Array.isArray(node.scope) && withinViewBoundary(node.scope, resolved.scope));
|
|
679
|
+
let truncated = scopesTruncated || fetched.hasMore || bounded.length > limits.maxNodes;
|
|
680
|
+
const members = bounded.slice(0, limits.maxNodes);
|
|
681
|
+
const contentMembers = members.filter((node) => !node.isScope && Array.isArray(node.scope));
|
|
682
|
+
const childScopeCountByParent = /* @__PURE__ */ new Map();
|
|
683
|
+
for (const node of scopeNodes) for (const parentId of node.parentIds) childScopeCountByParent.set(parentId, (childScopeCountByParent.get(parentId) ?? 0) + 1);
|
|
684
|
+
const scopeCountsById = /* @__PURE__ */ new Map([[root.id, directScopeMemberCounts(fetched, childScopeCountByParent.get(root.id) ?? 0, resolved.scope, limits.maxNodes)]]);
|
|
685
|
+
const memberScopes = members.filter((node) => node.isScope);
|
|
686
|
+
for (let index = 0; index < memberScopes.length; index += SCOPE_COUNT_CONCURRENCY) {
|
|
687
|
+
const batch = memberScopes.slice(index, index + SCOPE_COUNT_CONCURRENCY);
|
|
688
|
+
await Promise.all(batch.map(async (node) => {
|
|
689
|
+
const directMembers = await store.listScopeMembers({
|
|
690
|
+
scopeNodeId: node.id,
|
|
691
|
+
limit: limits.maxNodes
|
|
692
|
+
});
|
|
693
|
+
scopeCountsById.set(node.id, directScopeMemberCounts(directMembers, childScopeCountByParent.get(node.id) ?? 0, resolved.scope, limits.maxNodes));
|
|
694
|
+
}));
|
|
695
|
+
}
|
|
696
|
+
const recordWindow = [];
|
|
697
|
+
for (const node of contentMembers) {
|
|
698
|
+
if (recordWindow.length > limits.maxRecords) break;
|
|
699
|
+
const { records } = await store.listKnowledgeAbout({
|
|
700
|
+
node: node.id,
|
|
701
|
+
scope: node.scope,
|
|
702
|
+
limit: limits.maxRecords + 1 - recordWindow.length
|
|
703
|
+
});
|
|
704
|
+
recordWindow.push(...records);
|
|
705
|
+
}
|
|
706
|
+
recordWindow.sort((a, b) => b.id.localeCompare(a.id));
|
|
707
|
+
if (recordWindow.length > limits.maxRecords) {
|
|
708
|
+
truncated = true;
|
|
709
|
+
recordWindow.length = limits.maxRecords;
|
|
710
|
+
}
|
|
711
|
+
const resolver = new WikilinkResolver(store, contentMembers, limits.maxFallbackLookups);
|
|
712
|
+
const edges = [];
|
|
713
|
+
const graphRecords = [];
|
|
714
|
+
const edgeSeen = /* @__PURE__ */ new Set();
|
|
715
|
+
const recordCounts = /* @__PURE__ */ new Map();
|
|
716
|
+
for (const record of recordWindow) {
|
|
717
|
+
recordCounts.set(record.node, (recordCounts.get(record.node) ?? 0) + 1);
|
|
718
|
+
const nodeIds = [record.node];
|
|
719
|
+
for (const name of parseKnowledgeWikilinks(record.text)) {
|
|
720
|
+
const target = await resolver.resolve(name, record.scope);
|
|
721
|
+
if (!target) continue;
|
|
722
|
+
if (target.id === record.node) continue;
|
|
723
|
+
if (!resolver.inWindowId(target.id)) continue;
|
|
724
|
+
if (!nodeIds.includes(target.id)) nodeIds.push(target.id);
|
|
725
|
+
const key = `${record.node}\u0000${target.id}`;
|
|
726
|
+
if (edgeSeen.has(key)) continue;
|
|
727
|
+
edgeSeen.add(key);
|
|
728
|
+
edges.push({
|
|
729
|
+
id: `wikilink:${record.node}:${target.id}`,
|
|
730
|
+
source: record.node,
|
|
731
|
+
target: target.id,
|
|
732
|
+
type: "wikilink",
|
|
733
|
+
recordId: record.id
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
graphRecords.push({
|
|
737
|
+
id: record.id,
|
|
738
|
+
nodeIds,
|
|
739
|
+
pinned: false,
|
|
740
|
+
text: truncateRecordText(record.text)
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
for (const member of members) edges.push({
|
|
744
|
+
id: `contains:${root.id}:${member.id}`,
|
|
745
|
+
source: root.id,
|
|
746
|
+
target: member.id,
|
|
747
|
+
type: "contains"
|
|
748
|
+
});
|
|
749
|
+
const scopeAddressById = new Map(scopeNodes.map((node) => [node.id, node.address]));
|
|
750
|
+
const memberSessionNames = await this.#sessionNames(members.flatMap((node) => {
|
|
751
|
+
const address = node.isScope ? scopeAddressById.get(node.id) : void 0;
|
|
752
|
+
return address ? [{
|
|
753
|
+
id: node.id,
|
|
754
|
+
address
|
|
755
|
+
}] : [];
|
|
756
|
+
}));
|
|
757
|
+
const rootRow = await store.getNode(root.id);
|
|
758
|
+
const activity = await store.listActivity({
|
|
759
|
+
scope: resolved.scope,
|
|
760
|
+
limit: 1
|
|
761
|
+
});
|
|
762
|
+
const payload = {
|
|
763
|
+
view: resolved.view,
|
|
764
|
+
...resolved.threadId ? { threadId: resolved.threadId } : {},
|
|
765
|
+
nodes: [{
|
|
766
|
+
id: root.id,
|
|
767
|
+
name: root.name,
|
|
768
|
+
kind: scopeKindLabel(root.address, root.kind),
|
|
769
|
+
...root.description ? { description: root.description } : {},
|
|
770
|
+
scope: null,
|
|
771
|
+
rung: null,
|
|
772
|
+
isScope: true,
|
|
773
|
+
pinned: false,
|
|
774
|
+
recordCount: 0,
|
|
775
|
+
...scopeCountsById.get(root.id),
|
|
776
|
+
...rootRow ? {
|
|
777
|
+
createdAt: rootRow.createdAt.toISOString(),
|
|
778
|
+
updatedAt: rootRow.updatedAt.toISOString()
|
|
779
|
+
} : {}
|
|
780
|
+
}, ...members.map((node) => {
|
|
781
|
+
const nodeScope = Array.isArray(node.scope) ? node.scope : null;
|
|
782
|
+
const address = node.isScope ? scopeAddressById.get(node.id) : void 0;
|
|
783
|
+
return {
|
|
784
|
+
id: node.id,
|
|
785
|
+
name: memberSessionNames.get(node.id) ?? node.name,
|
|
786
|
+
kind: node.isScope ? scopeKindLabel(address ?? "", node.kind) : node.kind,
|
|
787
|
+
...node.description ? { description: node.description } : {},
|
|
788
|
+
scope: nodeScope,
|
|
789
|
+
rung: nodeScope ? deepestRung(nodeScope) : null,
|
|
790
|
+
...node.isScope ? {
|
|
791
|
+
isScope: true,
|
|
792
|
+
...scopeCountsById.get(node.id)
|
|
793
|
+
} : {},
|
|
794
|
+
pinned: false,
|
|
795
|
+
recordCount: recordCounts.get(node.id) ?? 0,
|
|
796
|
+
createdAt: node.createdAt.toISOString(),
|
|
797
|
+
updatedAt: node.updatedAt.toISOString()
|
|
798
|
+
};
|
|
799
|
+
})],
|
|
800
|
+
edges,
|
|
801
|
+
records: graphRecords,
|
|
802
|
+
truncated,
|
|
803
|
+
outOfWindow: [...resolver.outOfWindow.values()],
|
|
804
|
+
unresolvedCapped: {
|
|
805
|
+
count: resolver.cappedCount,
|
|
806
|
+
names: resolver.cappedNames
|
|
807
|
+
},
|
|
808
|
+
pinCensus: {
|
|
809
|
+
resource: 0,
|
|
810
|
+
thread: null
|
|
811
|
+
},
|
|
812
|
+
version: activity[0]?.id ?? null
|
|
813
|
+
};
|
|
814
|
+
return c.json(payload);
|
|
815
|
+
}
|
|
816
|
+
const view = this.#selectedView(resolved, loose(c).req.query("scopeLevel"));
|
|
817
|
+
if (!view) return c.json({ error: "scope_not_found" }, 404);
|
|
231
818
|
const { store, scope } = view;
|
|
232
819
|
const limits = this.#limits;
|
|
233
|
-
const fetched = await store.listNodes({
|
|
820
|
+
const fetched = (await store.listNodes({
|
|
234
821
|
scope,
|
|
235
822
|
limit: limits.maxNodes + 1
|
|
236
|
-
});
|
|
823
|
+
})).filter((node) => Array.isArray(node.scope));
|
|
237
824
|
let truncated = fetched.length > limits.maxNodes;
|
|
238
825
|
const pinnedNodeIds = await this.#pinnedNodeIds(view);
|
|
239
826
|
const pinnedNodeIdSet = new Set(pinnedNodeIds.map((p) => p.id));
|
|
@@ -358,13 +945,15 @@ var KnowledgeRoutes = class extends Route {
|
|
|
358
945
|
method: "GET",
|
|
359
946
|
requiresAuth: false,
|
|
360
947
|
handler: async (c) => {
|
|
361
|
-
const
|
|
362
|
-
if ("response" in
|
|
363
|
-
const { store, scope } = view;
|
|
948
|
+
const resolved = await this.#resolveView(loose(c));
|
|
949
|
+
if ("response" in resolved) return resolved.response;
|
|
364
950
|
const nodeId = loose(c).req.param("nodeId");
|
|
365
951
|
if (!nodeId || nodeId.length > 512) return c.json({ error: "node_not_found" }, 404);
|
|
952
|
+
const view = this.#selectedView(resolved, loose(c).req.query("scopeLevel"));
|
|
953
|
+
if (!view) return c.json({ error: "scope_not_found" }, 404);
|
|
954
|
+
const { store, scope } = view;
|
|
366
955
|
const node = await store.getNode(nodeId);
|
|
367
|
-
if (!node || !isKnowledgeScopeVisible(node.scope, scope)) return c.json({ error: "node_not_found" }, 404);
|
|
956
|
+
if (!node?.scope || !isKnowledgeScopeVisible(node.scope, scope)) return c.json({ error: "node_not_found" }, 404);
|
|
368
957
|
const pinnedNodeIds = await this.#pinnedNodeIds(view);
|
|
369
958
|
const pinnedNodeIdSet = new Set(pinnedNodeIds.map((p) => p.id));
|
|
370
959
|
const [owned, mentioning] = await Promise.all([store.listKnowledgeAbout({
|
|
@@ -403,6 +992,7 @@ var KnowledgeRoutes = class extends Route {
|
|
|
403
992
|
name: node.name,
|
|
404
993
|
kind: node.kind,
|
|
405
994
|
content: node.content ?? "",
|
|
995
|
+
...node.description ? { description: node.description } : {},
|
|
406
996
|
scope: node.scope,
|
|
407
997
|
rung: deepestRung(node.scope),
|
|
408
998
|
createdAt: node.createdAt.toISOString(),
|
|
@@ -417,25 +1007,120 @@ var KnowledgeRoutes = class extends Route {
|
|
|
417
1007
|
method: "GET",
|
|
418
1008
|
requiresAuth: false,
|
|
419
1009
|
handler: async (c) => {
|
|
420
|
-
const
|
|
421
|
-
if ("response" in
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
1010
|
+
const resolved = await this.#resolveView(loose(c));
|
|
1011
|
+
if ("response" in resolved) return resolved.response;
|
|
1012
|
+
const scopeNodeId = loose(c).req.query("scopeNodeId");
|
|
1013
|
+
let view;
|
|
1014
|
+
let memberIds;
|
|
1015
|
+
let membersTruncated = false;
|
|
1016
|
+
if (scopeNodeId !== void 0) {
|
|
1017
|
+
if (!UUID_RE.test(scopeNodeId)) return c.json({ error: "scope_not_found" }, 404);
|
|
1018
|
+
let scopeNode;
|
|
1019
|
+
try {
|
|
1020
|
+
scopeNode = await findOrgScopeNode(resolved.store, resolved.orgId, scopeNodeId);
|
|
1021
|
+
} catch (error) {
|
|
1022
|
+
if (error instanceof KnowledgeUnsupportedCapabilityError) return c.json({ error: "scope_not_found" }, 404);
|
|
1023
|
+
throw error;
|
|
1024
|
+
}
|
|
1025
|
+
if (!scopeNode) return c.json({ error: "scope_not_found" }, 404);
|
|
1026
|
+
const { members, hasMore } = await resolved.store.listScopeMembers({
|
|
1027
|
+
scopeNodeId,
|
|
1028
|
+
limit: this.#limits.maxNodes
|
|
1029
|
+
});
|
|
1030
|
+
membersTruncated = hasMore || members.length > this.#limits.maxNodes;
|
|
1031
|
+
memberIds = new Set(members.slice(0, this.#limits.maxNodes).filter((member) => !member.isScope).map((member) => member.id));
|
|
1032
|
+
view = resolved;
|
|
1033
|
+
} else {
|
|
1034
|
+
const selected = this.#selectedView(resolved, loose(c).req.query("scopeLevel"));
|
|
1035
|
+
if (!selected) return c.json({ error: "scope_not_found" }, 404);
|
|
1036
|
+
view = selected;
|
|
1037
|
+
}
|
|
1038
|
+
const cursor = loose(c).req.query("cursor");
|
|
1039
|
+
if (cursor && cursor.length > 128) return c.json({ error: "invalid_activity_cursor" }, 400);
|
|
1040
|
+
const { items: activity, scanCursor } = await this.#visibleActivity(view, memberIds, cursor, 26);
|
|
1041
|
+
const visiblePage = activity.slice(0, ACTIVITY_PAGE_SIZE);
|
|
1042
|
+
const lastVisible = visiblePage.at(-1);
|
|
1043
|
+
const payload = {
|
|
1044
|
+
events: visiblePage.map(({ event, node }) => {
|
|
1045
|
+
const rung = deepestRung(node.scope);
|
|
1046
|
+
const threadId = rung === "thread" ? node.scope.find((address) => address.startsWith("thread:"))?.slice(7) : void 0;
|
|
1047
|
+
return {
|
|
1048
|
+
id: event.id,
|
|
1049
|
+
action: event.action,
|
|
1050
|
+
recordType: event.recordType,
|
|
1051
|
+
...event.recordType === "record" ? { recordId: event.recordId } : {},
|
|
1052
|
+
scope: event.scope,
|
|
1053
|
+
node: {
|
|
1054
|
+
id: node.id,
|
|
1055
|
+
name: node.name,
|
|
1056
|
+
rung,
|
|
1057
|
+
...threadId ? { threadId } : {}
|
|
1058
|
+
},
|
|
1059
|
+
createdAt: event.createdAt.toISOString()
|
|
1060
|
+
};
|
|
1061
|
+
}),
|
|
1062
|
+
...activity.length > ACTIVITY_PAGE_SIZE && lastVisible ? { nextCursor: lastVisible.event.id } : scanCursor ? { nextCursor: scanCursor } : {},
|
|
1063
|
+
...membersTruncated ? { truncated: true } : {}
|
|
1064
|
+
};
|
|
1065
|
+
return c.json(payload);
|
|
435
1066
|
}
|
|
436
1067
|
})
|
|
437
1068
|
];
|
|
438
1069
|
}
|
|
1070
|
+
/**
|
|
1071
|
+
* Newest-first activity page where EVERY returned event targets a node
|
|
1072
|
+
* visible in the view. Visibility is decided BEFORE window shaping: hidden
|
|
1073
|
+
* rows are skipped entirely (no action/type/id/time metadata leaks) and a
|
|
1074
|
+
* hidden backlog can never displace visible events from the window.
|
|
1075
|
+
* When the scan cap stops the walk before the stream ends, `scanCursor` is
|
|
1076
|
+
* the last scanned event so the caller can continue past hidden rows.
|
|
1077
|
+
*/
|
|
1078
|
+
async #visibleActivity(view, memberIds, cursor, limit) {
|
|
1079
|
+
const out = [];
|
|
1080
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
1081
|
+
const getNode = (id) => {
|
|
1082
|
+
let node = nodes.get(id);
|
|
1083
|
+
if (!node) {
|
|
1084
|
+
node = view.store.getNode(id);
|
|
1085
|
+
nodes.set(id, node);
|
|
1086
|
+
}
|
|
1087
|
+
return node;
|
|
1088
|
+
};
|
|
1089
|
+
let after = cursor;
|
|
1090
|
+
let scanned = 0;
|
|
1091
|
+
let exhausted = false;
|
|
1092
|
+
while (out.length < limit && scanned < ACTIVITY_SCAN_CAP) {
|
|
1093
|
+
const batch = await view.store.listActivity({
|
|
1094
|
+
scope: view.scope,
|
|
1095
|
+
after,
|
|
1096
|
+
limit: ACTIVITY_BATCH
|
|
1097
|
+
});
|
|
1098
|
+
if (batch.length === 0) {
|
|
1099
|
+
exhausted = true;
|
|
1100
|
+
break;
|
|
1101
|
+
}
|
|
1102
|
+
for (const event of batch) {
|
|
1103
|
+
after = event.id;
|
|
1104
|
+
scanned += 1;
|
|
1105
|
+
let targetNode;
|
|
1106
|
+
if (event.recordType === "node") targetNode = await getNode(event.recordId);
|
|
1107
|
+
else {
|
|
1108
|
+
const record = await view.store.getKnowledge({ id: event.recordId });
|
|
1109
|
+
targetNode = record ? await getNode(record.node) : null;
|
|
1110
|
+
if (record && !isKnowledgeScopeVisible(record.scope, view.scope)) targetNode = null;
|
|
1111
|
+
}
|
|
1112
|
+
if (targetNode?.scope && isKnowledgeScopeVisible(targetNode.scope, view.scope) && (memberIds === void 0 || memberIds.has(targetNode.id))) out.push({
|
|
1113
|
+
event,
|
|
1114
|
+
node: targetNode
|
|
1115
|
+
});
|
|
1116
|
+
if (out.length >= limit || scanned >= ACTIVITY_SCAN_CAP) break;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
return {
|
|
1120
|
+
items: out,
|
|
1121
|
+
...!exhausted && out.length < limit && scanned >= ACTIVITY_SCAN_CAP && after ? { scanCursor: after } : {}
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
439
1124
|
};
|
|
440
1125
|
//#endregion
|
|
441
1126
|
export { KnowledgeRoutes };
|