@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.
Files changed (83) hide show
  1. package/dist/auth.d.ts +7 -0
  2. package/dist/auth.d.ts.map +1 -1
  3. package/dist/auth.js +12 -1
  4. package/dist/auth.js.map +1 -1
  5. package/dist/factory.d.ts +9 -0
  6. package/dist/factory.d.ts.map +1 -1
  7. package/dist/factory.js +56 -23
  8. package/dist/factory.js.map +1 -1
  9. package/dist/integrations/github/routes.js +1 -0
  10. package/dist/integrations/github/routes.js.map +1 -1
  11. package/dist/integrations/linear/issue-reconciler.d.ts +3 -1
  12. package/dist/integrations/linear/issue-reconciler.d.ts.map +1 -1
  13. package/dist/integrations/linear/issue-reconciler.js +1 -1
  14. package/dist/integrations/linear/issue-reconciler.js.map +1 -1
  15. package/dist/integrations/platform/linear/integration.d.ts.map +1 -1
  16. package/dist/integrations/platform/linear/integration.js +10 -9
  17. package/dist/integrations/platform/linear/integration.js.map +1 -1
  18. package/dist/integrations/slack/integration.d.ts.map +1 -1
  19. package/dist/integrations/slack/integration.js +0 -1
  20. package/dist/integrations/slack/integration.js.map +1 -1
  21. package/dist/integrations/slack/slack.d.ts +0 -17
  22. package/dist/integrations/slack/slack.d.ts.map +1 -1
  23. package/dist/integrations/slack/slack.js +8 -40
  24. package/dist/integrations/slack/slack.js.map +1 -1
  25. package/dist/routes/config.d.ts +15 -36
  26. package/dist/routes/config.d.ts.map +1 -1
  27. package/dist/routes/config.js +72 -123
  28. package/dist/routes/config.js.map +1 -1
  29. package/dist/routes/knowledge.d.ts +115 -21
  30. package/dist/routes/knowledge.d.ts.map +1 -1
  31. package/dist/routes/knowledge.js +715 -30
  32. package/dist/routes/knowledge.js.map +1 -1
  33. package/dist/routes/oauth.d.ts +0 -3
  34. package/dist/routes/oauth.d.ts.map +1 -1
  35. package/dist/routes/oauth.js +2 -10
  36. package/dist/routes/oauth.js.map +1 -1
  37. package/dist/routes/provider-credentials.d.ts.map +1 -1
  38. package/dist/routes/provider-credentials.js +2 -1
  39. package/dist/routes/provider-credentials.js.map +1 -1
  40. package/dist/routes/surface.d.ts +1 -0
  41. package/dist/routes/surface.d.ts.map +1 -1
  42. package/dist/routes/surface.js +6 -3
  43. package/dist/routes/surface.js.map +1 -1
  44. package/dist/rules/binding-context.d.ts +19 -0
  45. package/dist/rules/binding-context.d.ts.map +1 -1
  46. package/dist/rules/binding-context.js +39 -1
  47. package/dist/rules/binding-context.js.map +1 -1
  48. package/dist/rules/dispatcher.d.ts +2 -13
  49. package/dist/rules/dispatcher.d.ts.map +1 -1
  50. package/dist/rules/dispatcher.js +0 -6
  51. package/dist/rules/dispatcher.js.map +1 -1
  52. package/dist/rules/processor.d.ts +35 -1
  53. package/dist/rules/processor.d.ts.map +1 -1
  54. package/dist/rules/processor.js +80 -4
  55. package/dist/rules/processor.js.map +1 -1
  56. package/dist/rules/start-coordinator.d.ts +1 -2
  57. package/dist/rules/start-coordinator.d.ts.map +1 -1
  58. package/dist/rules/start-coordinator.js +2 -6
  59. package/dist/rules/start-coordinator.js.map +1 -1
  60. package/dist/session/factory-session.d.ts +5 -43
  61. package/dist/session/factory-session.d.ts.map +1 -1
  62. package/dist/session/factory-session.js +6 -47
  63. package/dist/session/factory-session.js.map +1 -1
  64. package/dist/session/memory-settings-hydration.d.ts +17 -89
  65. package/dist/session/memory-settings-hydration.d.ts.map +1 -1
  66. package/dist/session/memory-settings-hydration.js +25 -111
  67. package/dist/session/memory-settings-hydration.js.map +1 -1
  68. package/dist/storage/domains/memory-settings/base.d.ts +5 -17
  69. package/dist/storage/domains/memory-settings/base.d.ts.map +1 -1
  70. package/dist/storage/domains/memory-settings/base.js +8 -15
  71. package/dist/storage/domains/memory-settings/base.js.map +1 -1
  72. package/dist/supervisor/session.d.ts +2 -4
  73. package/dist/supervisor/session.d.ts.map +1 -1
  74. package/dist/supervisor/session.js +3 -5
  75. package/dist/supervisor/session.js.map +1 -1
  76. package/dist/workspace.d.ts.map +1 -1
  77. package/dist/workspace.js +2 -2
  78. package/dist/workspace.js.map +1 -1
  79. package/package.json +7 -7
  80. package/dist/routes/om-seed.d.ts +0 -22
  81. package/dist/routes/om-seed.d.ts.map +0 -1
  82. package/dist/routes/om-seed.js +0 -40
  83. package/dist/routes/om-seed.js.map +0 -1
@@ -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
- if (!await projects.get({
239
+ const project = await projects.get({
147
240
  orgId: tenant.orgId,
148
241
  id: projectId
149
- })) return { response: c.json({ error: "Project not found" }, 404) };
150
- const store = await this.deps.knowledge();
151
- if (!store) return { response: c.json({
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 knowledge storage domain is not configured."
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/graph", {
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 view = await this.#resolveView(loose(c));
362
- if ("response" in view) return view.response;
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 view = await this.#resolveView(loose(c));
421
- if ("response" in view) return view.response;
422
- const events = await view.store.listActivity({
423
- scope: view.scope,
424
- limit: 100
425
- });
426
- return c.json({ events: events.map((event) => ({
427
- id: event.id,
428
- action: event.action,
429
- recordType: event.recordType,
430
- recordId: event.recordId,
431
- scope: event.scope,
432
- ...event.sourceThreadId ? { sourceThreadId: event.sourceThreadId } : {},
433
- createdAt: event.createdAt.toISOString()
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 };