@opengeni/core 3.0.1 → 3.1.2

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 (257) hide show
  1. package/README.md +4 -4
  2. package/dist/access/index.d.ts +131 -7
  3. package/dist/access/service-initiator.d.ts +3 -0
  4. package/dist/application/archived-session-imports.d.ts +6 -0
  5. package/dist/application/composer-submit.d.ts +1 -1
  6. package/dist/application/connect-operation.d.ts +6 -0
  7. package/dist/application/external-workspace-members.d.ts +13 -1
  8. package/dist/application/modal-native-live-declaration-join.d.ts +37 -0
  9. package/dist/application/modal-native-original-configuration.d.ts +79 -0
  10. package/dist/application/modal-native-worker-host-transport.d.ts +297 -0
  11. package/dist/application/new-session-drafts.d.ts +29 -2
  12. package/dist/application/organization-privacy.d.ts +57 -0
  13. package/dist/application/prepared-mcp-connection.d.ts +31 -0
  14. package/dist/application/sandbox-recovery.d.ts +58 -0
  15. package/dist/application/session-commands.d.ts +3 -0
  16. package/dist/application/session-tenancy.d.ts +13 -4
  17. package/dist/billing/agent-run-admission.d.ts +21 -0
  18. package/dist/billing/limits.d.ts +27 -1
  19. package/dist/billing/model-call-admission.d.ts +34 -0
  20. package/dist/billing/model-usage-settlement.d.ts +112 -0
  21. package/dist/canonical-human-identities.js +1 -1
  22. package/dist/{chunk-VCZFFEPB.js → chunk-7T2SZ7BI.js} +16 -2
  23. package/dist/chunk-7T2SZ7BI.js.map +1 -0
  24. package/dist/{chunk-ZYCNLYGH.js → chunk-OMJFIIWK.js} +178 -12
  25. package/dist/chunk-OMJFIIWK.js.map +1 -0
  26. package/dist/{chunk-H7X52RI2.js → chunk-QI2M2B2K.js} +83 -8
  27. package/dist/chunk-QI2M2B2K.js.map +1 -0
  28. package/dist/{chunk-OF65T3PM.js → chunk-TTWTXQB2.js} +94 -6
  29. package/dist/chunk-TTWTXQB2.js.map +1 -0
  30. package/dist/codex-model-availability.d.ts +55 -0
  31. package/dist/default-session-model.d.ts +192 -0
  32. package/dist/dependencies.d.ts +41 -7
  33. package/dist/developer-setup-credential-ceiling.d.ts +10 -0
  34. package/dist/domain/agent-config-resolution.d.ts +49 -0
  35. package/dist/domain/automations.d.ts +8 -1
  36. package/dist/domain/capabilities.d.ts +112 -8
  37. package/dist/domain/connection-account-selection-error.d.ts +8 -0
  38. package/dist/domain/connector-tool-permissions.d.ts +62 -0
  39. package/dist/domain/direct-model-provider.d.ts +3 -0
  40. package/dist/domain/editable-artifacts/agent-application.d.ts +10 -1
  41. package/dist/domain/editable-artifacts/durable-export.d.ts +22 -1
  42. package/dist/domain/editable-artifacts/ports.d.ts +10 -0
  43. package/dist/domain/editable-artifacts/service.d.ts +3 -1
  44. package/dist/domain/editable-artifacts/types.d.ts +1 -1
  45. package/dist/domain/encryption-key.d.ts +12 -0
  46. package/dist/domain/insights-timing.d.ts +16 -0
  47. package/dist/domain/insights-usage.d.ts +27 -0
  48. package/dist/domain/insights.d.ts +21 -3
  49. package/dist/domain/knowledge-files.d.ts +1 -1
  50. package/dist/domain/knowledge-preparation.d.ts +10 -0
  51. package/dist/domain/knowledge-preparation.test.d.ts +1 -0
  52. package/dist/domain/knowledge-search.d.ts +15 -76
  53. package/dist/domain/mcp-account-bindings.d.ts +17 -0
  54. package/dist/domain/mcp-account-routes.d.ts +21 -0
  55. package/dist/domain/native-mcp-connection-admission.d.ts +3 -0
  56. package/dist/domain/organization-integration-catalog.d.ts +15 -0
  57. package/dist/domain/personal-connection-delegations.d.ts +73 -12
  58. package/dist/domain/pr-review.d.ts +121 -6
  59. package/dist/domain/realtime-voice-billing.d.ts +103 -0
  60. package/dist/domain/resources.d.ts +17 -1
  61. package/dist/domain/scheduled-task-access.d.ts +265 -0
  62. package/dist/domain/scheduled-task-account-audit.d.ts +27 -0
  63. package/dist/domain/scheduled-task-audience.d.ts +70 -0
  64. package/dist/domain/scheduled-tasks.d.ts +61 -18
  65. package/dist/domain/session-connection-accounts.d.ts +53 -0
  66. package/dist/domain/session-file-access.d.ts +4 -0
  67. package/dist/domain/session-native-connection-replacement.d.ts +14 -0
  68. package/dist/domain/session-start-timing.d.ts +7 -0
  69. package/dist/domain/session-tool-policy.d.ts +102 -4
  70. package/dist/domain/sessions.d.ts +179 -15
  71. package/dist/domain/site-audience.d.ts +52 -0
  72. package/dist/domain/skills.d.ts +29 -1
  73. package/dist/domain/slack-bot.d.ts +46 -1
  74. package/dist/domain/slack-publication-secret-safety.d.ts +1 -1
  75. package/dist/domain/vaults/index.d.ts +7 -0
  76. package/dist/domain/vaults/mapping.d.ts +25 -0
  77. package/dist/domain/vaults/provider-registry.d.ts +72 -0
  78. package/dist/domain/vaults/provider-sync.d.ts +176 -0
  79. package/dist/domain/vaults/provider-writes.d.ts +57 -0
  80. package/dist/domain/vaults/secret-resolver.d.ts +34 -0
  81. package/dist/domain/vaults/types.d.ts +24 -0
  82. package/dist/domain/vaults/vault-service.d.ts +35 -0
  83. package/dist/domain/voice-input-billing.d.ts +77 -0
  84. package/dist/domain/web-search-billing.d.ts +51 -0
  85. package/dist/domain/workspace-credits.d.ts +7 -0
  86. package/dist/editable-artifact-live/application.d.ts +2 -1
  87. package/dist/editable-artifact-live/server.d.ts +5 -1
  88. package/dist/editable-artifact-live/ticket.d.ts +2 -1
  89. package/dist/editable-artifact-live/types.d.ts +9 -0
  90. package/dist/editable-artifact-live.js +2 -2
  91. package/dist/editable-artifacts.js +10 -2
  92. package/dist/goal-admission.d.ts +627 -0
  93. package/dist/host-session-usage-limit.d.ts +15 -0
  94. package/dist/index.d.ts +43 -8
  95. package/dist/index.js +22682 -11743
  96. package/dist/index.js.map +1 -1
  97. package/dist/managed-session.d.ts +110 -7
  98. package/dist/model-call.d.ts +58 -0
  99. package/dist/model-catalog.d.ts +29 -3
  100. package/dist/model-provider-credentials.d.ts +43 -0
  101. package/dist/product-usage-metrics.d.ts +30 -0
  102. package/dist/rigs/index.d.ts +1 -13
  103. package/dist/sandbox/fleet.d.ts +58 -4
  104. package/dist/sandbox/routing.d.ts +42 -2
  105. package/dist/sandbox/runtime-settings.d.ts +27 -17
  106. package/dist/session-authorization.d.ts +10 -0
  107. package/dist/session-usage-limit-metrics.d.ts +60 -0
  108. package/dist/session-usage-limits.d.ts +89 -0
  109. package/dist/site-agent-clamp.d.ts +71 -0
  110. package/dist/site-session-origin.d.ts +39 -2
  111. package/dist/transcription.d.ts +120 -3
  112. package/dist/turn-surface.d.ts +20 -0
  113. package/dist/user-presence.d.ts +32 -0
  114. package/dist/workflow-wake-contract.d.ts +13 -1
  115. package/package.json +15 -22
  116. package/src/access/index.ts +961 -38
  117. package/src/access/service-initiator.ts +50 -0
  118. package/src/application/api-integration-servers.ts +9 -0
  119. package/src/application/archived-session-imports.ts +161 -0
  120. package/src/application/composer-submit.ts +3 -2
  121. package/src/application/connect-authority.ts +3 -3
  122. package/src/application/connect-operation.ts +37 -3
  123. package/src/application/external-continuation.ts +20 -3
  124. package/src/application/external-link-work-admission.ts +3 -0
  125. package/src/application/external-workspace-members.ts +51 -3
  126. package/src/application/modal-native-live-declaration-join.ts +196 -0
  127. package/src/application/modal-native-original-configuration.ts +261 -0
  128. package/src/application/modal-native-worker-host-transport.ts +263 -0
  129. package/src/application/new-session-drafts.ts +172 -55
  130. package/src/application/organization-privacy.ts +161 -0
  131. package/src/application/prepared-mcp-connection.ts +133 -0
  132. package/src/application/sandbox-recovery.ts +78 -0
  133. package/src/application/session-commands.ts +73 -1
  134. package/src/application/session-mcp-credential-rotation.ts +61 -18
  135. package/src/application/session-tenancy.ts +262 -44
  136. package/src/application/user-resource-grants.ts +1 -29
  137. package/src/billing/agent-run-admission.ts +117 -0
  138. package/src/billing/limits.ts +138 -14
  139. package/src/billing/model-call-admission.ts +118 -0
  140. package/src/billing/model-usage-settlement.ts +472 -0
  141. package/src/codex-model-availability.ts +264 -0
  142. package/src/default-session-model.ts +692 -0
  143. package/src/dependencies.ts +48 -6
  144. package/src/developer-setup-credential-ceiling.ts +60 -0
  145. package/src/domain/agent-config-resolution.ts +148 -0
  146. package/src/domain/automations.ts +76 -2
  147. package/src/domain/capabilities.ts +830 -181
  148. package/src/domain/connection-account-selection-error.ts +19 -0
  149. package/src/domain/connector-tool-permissions.ts +572 -0
  150. package/src/domain/direct-model-provider.ts +62 -0
  151. package/src/domain/editable-artifacts/agent-application.ts +44 -6
  152. package/src/domain/editable-artifacts/durable-export.ts +100 -5
  153. package/src/domain/editable-artifacts/in-memory.ts +6 -0
  154. package/src/domain/editable-artifacts/ports.ts +11 -0
  155. package/src/domain/editable-artifacts/service.ts +20 -0
  156. package/src/domain/editable-artifacts/types.ts +1 -1
  157. package/src/domain/encryption-key.ts +26 -0
  158. package/src/domain/file-owner.ts +16 -1
  159. package/src/domain/github-action-policies.ts +1 -1
  160. package/src/domain/github-skill-source.ts +3 -1
  161. package/src/domain/insights-timing.ts +95 -0
  162. package/src/domain/insights-usage.ts +67 -0
  163. package/src/domain/insights.ts +167 -43
  164. package/src/domain/knowledge-files.ts +10 -3
  165. package/src/domain/knowledge-messages.ts +1 -0
  166. package/src/domain/knowledge-preparation.test.ts +276 -0
  167. package/src/domain/knowledge-preparation.ts +147 -0
  168. package/src/domain/knowledge-search.test.ts +617 -5
  169. package/src/domain/knowledge-search.ts +240 -23
  170. package/src/domain/mcp-account-bindings.ts +169 -0
  171. package/src/domain/mcp-account-routes.ts +152 -0
  172. package/src/domain/native-mcp-connection-admission.ts +14 -0
  173. package/src/domain/organization-integration-catalog.ts +88 -0
  174. package/src/domain/personal-connection-delegations.ts +399 -257
  175. package/src/domain/pr-review.ts +24 -8
  176. package/src/domain/realtime-voice-billing.ts +425 -0
  177. package/src/domain/resources.ts +50 -4
  178. package/src/domain/scheduled-task-access.ts +1089 -0
  179. package/src/domain/scheduled-task-account-audit.ts +90 -0
  180. package/src/domain/scheduled-task-audience.ts +249 -0
  181. package/src/domain/scheduled-tasks.ts +1313 -374
  182. package/src/domain/session-connection-accounts.ts +239 -0
  183. package/src/domain/session-file-access.ts +47 -0
  184. package/src/domain/session-native-connection-replacement.ts +64 -0
  185. package/src/domain/session-start-timing.ts +60 -0
  186. package/src/domain/session-tool-policy.ts +489 -14
  187. package/src/domain/sessions.ts +2254 -717
  188. package/src/domain/site-audience.ts +130 -0
  189. package/src/domain/skills.ts +9 -0
  190. package/src/domain/slack-bot.ts +141 -4
  191. package/src/domain/slack-publication-secret-safety.ts +1 -1
  192. package/src/domain/vaults/index.ts +19 -0
  193. package/src/domain/vaults/mapping.ts +190 -0
  194. package/src/domain/vaults/provider-registry.ts +201 -0
  195. package/src/domain/vaults/provider-sync.ts +531 -0
  196. package/src/domain/vaults/provider-writes.ts +288 -0
  197. package/src/domain/vaults/secret-resolver.ts +464 -0
  198. package/src/domain/vaults/types.ts +26 -0
  199. package/src/domain/vaults/vault-service.ts +1316 -0
  200. package/src/domain/voice-input-billing.ts +330 -0
  201. package/src/domain/web-search-billing.ts +169 -0
  202. package/src/domain/workspace-credits.ts +13 -0
  203. package/src/editable-artifact-live/application.ts +8 -1
  204. package/src/editable-artifact-live/server.ts +154 -6
  205. package/src/editable-artifact-live/ticket.ts +86 -1
  206. package/src/editable-artifact-live/types.ts +10 -0
  207. package/src/goal-admission.ts +220 -0
  208. package/src/host-session-usage-limit.ts +25 -0
  209. package/src/index.ts +75 -11
  210. package/src/managed-session.ts +137 -6
  211. package/src/model-call.ts +79 -0
  212. package/src/model-catalog.ts +345 -91
  213. package/src/model-provider-credentials.ts +394 -0
  214. package/src/product-usage-metrics.ts +101 -0
  215. package/src/rigs/index.ts +23 -111
  216. package/src/sandbox/fleet.ts +253 -28
  217. package/src/sandbox/routing.ts +204 -43
  218. package/src/sandbox/runtime-settings.ts +159 -85
  219. package/src/session-authorization.ts +172 -23
  220. package/src/session-usage-limit-metrics.ts +95 -0
  221. package/src/session-usage-limits.ts +415 -0
  222. package/src/site-agent-clamp.ts +226 -0
  223. package/src/site-session-origin.ts +84 -5
  224. package/src/transcription.ts +123 -4
  225. package/src/turn-surface.ts +47 -0
  226. package/src/user-presence.ts +134 -0
  227. package/src/workflow-wake-contract.ts +14 -1
  228. package/dist/application/host-mcp-owner.d.ts +0 -6
  229. package/dist/application/host-mcp-resolvers.d.ts +0 -13
  230. package/dist/chunk-5WZYFL45.js +0 -266
  231. package/dist/chunk-5WZYFL45.js.map +0 -1
  232. package/dist/chunk-H7X52RI2.js.map +0 -1
  233. package/dist/chunk-OF65T3PM.js.map +0 -1
  234. package/dist/chunk-VCZFFEPB.js.map +0 -1
  235. package/dist/chunk-ZYCNLYGH.js.map +0 -1
  236. package/dist/domain/connection-authority.d.ts +0 -97
  237. package/dist/domain/connection-authority.js +0 -169
  238. package/dist/domain/connection-authority.js.map +0 -1
  239. package/dist/domain/environments.d.ts +0 -33
  240. package/dist/domain/host-mcp-authority-source-admission.d.ts +0 -10
  241. package/dist/domain/host-mcp-task-admission.d.ts +0 -18
  242. package/dist/domain/packs.d.ts +0 -59
  243. package/dist/domain/product-integration-pack.d.ts +0 -44
  244. package/dist/domain/product-integration-skill.gen.d.ts +0 -5
  245. package/dist/remote-mcp-credentials.d.ts +0 -23
  246. package/dist/remote-mcp-credentials.js +0 -11
  247. package/dist/remote-mcp-credentials.js.map +0 -1
  248. package/src/application/host-mcp-owner.ts +0 -41
  249. package/src/application/host-mcp-resolvers.ts +0 -84
  250. package/src/domain/connection-authority.ts +0 -329
  251. package/src/domain/environments.ts +0 -120
  252. package/src/domain/host-mcp-authority-source-admission.ts +0 -25
  253. package/src/domain/host-mcp-task-admission.ts +0 -121
  254. package/src/domain/packs.ts +0 -699
  255. package/src/domain/product-integration-pack.ts +0 -42
  256. package/src/domain/product-integration-skill.gen.ts +0 -52
  257. package/src/remote-mcp-credentials.ts +0 -361
@@ -11,6 +11,7 @@ import {
11
11
  countSessionsAttachedToGroups,
12
12
  enumerateUtcDays,
13
13
  enumerateUtcHours,
14
+ getSessionAccessProjection,
14
15
  listFloorSessions,
15
16
  listLiveWarmLeases,
16
17
  listScheduledTasks,
@@ -20,24 +21,42 @@ import {
20
21
  type Database,
21
22
  } from "@opengeni/db";
22
23
 
24
+ import { measureInsightsPhase, type InsightsPhaseObserver } from "./insights-timing";
25
+ export { measureInsightsPhase, workspaceInsightsPhaseMetricObserver } from "./insights-timing";
26
+ export type { InsightsPhaseObservation, InsightsPhaseObserver } from "./insights-timing";
23
27
  const MACHINE_HEARTBEAT_FRESH_MS = 120_000;
24
28
  export const WORKSPACE_INSIGHTS_PROVIDER_FILTER_MAX_UTF8_BYTES = 256;
25
29
  export const WORKSPACE_INSIGHTS_MODEL_FILTER_MAX_UTF8_BYTES = 512;
26
30
 
27
- export type WorkspaceInsightsFilterField = "provider" | "model";
31
+ export type WorkspaceInsightsFilterField = "provider" | "model" | "rootSessionId" | "sessionId";
28
32
 
29
33
  export class WorkspaceInsightsFilterValidationError extends Error {
30
34
  readonly field: WorkspaceInsightsFilterField;
31
35
  readonly maxUtf8Bytes: number;
32
36
 
33
- constructor(field: WorkspaceInsightsFilterField, maxUtf8Bytes: number) {
34
- super(`${field} must be at most ${maxUtf8Bytes} UTF-8 bytes`);
37
+ constructor(field: WorkspaceInsightsFilterField, maxUtf8Bytes: number, message?: string) {
38
+ super(message ?? `${field} must be at most ${maxUtf8Bytes} UTF-8 bytes`);
35
39
  this.name = "WorkspaceInsightsFilterValidationError";
36
40
  this.field = field;
37
41
  this.maxUtf8Bytes = maxUtf8Bytes;
38
42
  }
39
43
  }
40
44
 
45
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
46
+
47
+ /** Normalize a session drilldown scope; only exact UUIDs are accepted. */
48
+ export function normalizeWorkspaceInsightsSessionScope(
49
+ value: string | null | undefined,
50
+ field: "rootSessionId" | "sessionId",
51
+ ): string | null {
52
+ const normalized = value?.trim() || null;
53
+ if (normalized === null || normalized === "all") return null;
54
+ if (!UUID_PATTERN.test(normalized)) {
55
+ throw new WorkspaceInsightsFilterValidationError(field, 36, `${field} must be a UUID`);
56
+ }
57
+ return normalized.toLowerCase();
58
+ }
59
+
41
60
  /** Normalize the public filter sentinel and enforce the database authority envelope. */
42
61
  export function normalizeWorkspaceInsightsFilter(
43
62
  value: string | null | undefined,
@@ -60,6 +79,14 @@ export type GetWorkspaceInsightsInput = {
60
79
  range: InsightsRange;
61
80
  provider?: string | null;
62
81
  model?: string | null;
82
+ rootSessionId?: string | null;
83
+ sessionId?: string | null;
84
+ /**
85
+ * Resolves the reading person, only once the rollup runs. The per-schedule
86
+ * list leaves out other people's Only me schedules; their usage stays in the
87
+ * per-person private totals.
88
+ */
89
+ viewerSubjectId?: () => Promise<string | null>;
63
90
  now?: Date;
64
91
  };
65
92
 
@@ -206,46 +233,100 @@ export function insightsSessionLabel(input: {
206
233
  return `${(input.depth ?? 0) > 0 ? "Agent" : "Session"} ${input.id.slice(0, 8)}`;
207
234
  }
208
235
 
236
+ export function insightsProjectLabel(input: {
237
+ kind: "project" | "other" | "unfiled" | "unavailable" | "deleted";
238
+ name: string | null;
239
+ projects: number;
240
+ }): string {
241
+ switch (input.kind) {
242
+ case "project":
243
+ return input.name?.trim() || "Untitled project";
244
+ case "other":
245
+ return `${input.projects.toLocaleString("en-US")} other ${input.projects === 1 ? "project" : "projects"}`;
246
+ case "unfiled":
247
+ return "No project";
248
+ case "unavailable":
249
+ return "Root session not visible";
250
+ case "deleted":
251
+ return "Deleted chats";
252
+ }
253
+ }
254
+
209
255
  export async function getWorkspaceInsights(
210
256
  db: Database,
211
257
  settings: Settings,
212
258
  input: GetWorkspaceInsightsInput,
259
+ observePhase?: InsightsPhaseObserver,
213
260
  ): Promise<WorkspaceInsightsResponse> {
214
261
  const provider = normalizeWorkspaceInsightsFilter(input.provider, "provider");
215
262
  const model = normalizeWorkspaceInsightsFilter(input.model, "model");
216
- await requireWorkspace(db, input.workspaceId);
263
+ const rootSessionId = normalizeWorkspaceInsightsSessionScope(
264
+ input.rootSessionId,
265
+ "rootSessionId",
266
+ );
267
+ const sessionId = normalizeWorkspaceInsightsSessionScope(input.sessionId, "sessionId");
268
+ await measureInsightsPhase(observePhase, "require_workspace", () =>
269
+ requireWorkspace(db, input.workspaceId),
270
+ );
271
+ const [visibleRootScope, visibleSessionScope] = await Promise.all([
272
+ rootSessionId ? getSessionAccessProjection(db, input.workspaceId, rootSessionId) : null,
273
+ sessionId ? getSessionAccessProjection(db, input.workspaceId, sessionId) : null,
274
+ ]);
217
275
  const now = input.now ?? new Date();
218
276
  const window = resolveRangeWindow(input.range, now);
219
277
  const modelFilterActive = Boolean(provider || model);
220
- const filter = { provider, model };
278
+ const scopeActive = Boolean(rootSessionId || sessionId);
279
+ const filter = { provider, model, rootSessionId, sessionId };
221
280
 
222
281
  const [modelBundle, usageBundle, liveWarm, tasks, depth, floorRows, machinesOnline] =
223
282
  await Promise.all([
224
- readWorkspaceInsightsModelBundle(db, {
225
- workspaceId: input.workspaceId,
226
- since: window.since,
227
- until: window.until,
228
- priorSince: window.priorSince,
229
- priorUntil: window.priorUntil,
230
- granularity: input.range === "today" ? "hour" : "day",
231
- ...filter,
232
- }),
233
- readWorkspaceInsightsUsageBundle(db, {
234
- workspaceId: input.workspaceId,
235
- since: window.since,
236
- until: window.until,
237
- priorSince: window.priorSince,
238
- priorUntil: window.priorUntil,
239
- monthSince: startOfUtcMonth(now),
240
- granularity: input.range === "today" ? "hour" : "day",
241
- warmGroupLimit: 24,
242
- }),
243
- listLiveWarmLeases(db, input.workspaceId),
244
- listScheduledTasks(db, input.workspaceId, 100),
245
- aggregateSessionDepth(db, input.workspaceId),
246
- listFloorSessions(db, input.workspaceId, 24),
283
+ measureInsightsPhase(observePhase, "model_bundle", () =>
284
+ readWorkspaceInsightsModelBundle(db, {
285
+ workspaceId: input.workspaceId,
286
+ since: window.since,
287
+ until: window.until,
288
+ priorSince: window.priorSince,
289
+ priorUntil: window.priorUntil,
290
+ granularity: input.range === "today" ? "hour" : "day",
291
+ ...filter,
292
+ }),
293
+ ),
294
+ measureInsightsPhase(observePhase, "usage_bundle", () =>
295
+ readWorkspaceInsightsUsageBundle(db, {
296
+ workspaceId: input.workspaceId,
297
+ since: window.since,
298
+ until: window.until,
299
+ priorSince: window.priorSince,
300
+ priorUntil: window.priorUntil,
301
+ monthSince: startOfUtcMonth(now),
302
+ granularity: input.range === "today" ? "hour" : "day",
303
+ warmGroupLimit: 24,
304
+ }),
305
+ ),
306
+ measureInsightsPhase(observePhase, "live_warm", () =>
307
+ listLiveWarmLeases(db, input.workspaceId),
308
+ ),
309
+ measureInsightsPhase(observePhase, "scheduled_tasks", () =>
310
+ (async () =>
311
+ await listScheduledTasks(
312
+ db,
313
+ input.workspaceId,
314
+ 100,
315
+ 0,
316
+ undefined,
317
+ input.viewerSubjectId ? await input.viewerSubjectId() : null,
318
+ ))(),
319
+ ),
320
+ measureInsightsPhase(observePhase, "session_depth", () =>
321
+ aggregateSessionDepth(db, input.workspaceId),
322
+ ),
323
+ measureInsightsPhase(observePhase, "floor_sessions", () =>
324
+ listFloorSessions(db, input.workspaceId, 24),
325
+ ),
247
326
  settings.sandboxSelfhostedEnabled
248
- ? countOnlineMachines(db, input.workspaceId, MACHINE_HEARTBEAT_FRESH_MS, now)
327
+ ? measureInsightsPhase(observePhase, "online_machines", () =>
328
+ countOnlineMachines(db, input.workspaceId, MACHINE_HEARTBEAT_FRESH_MS, now),
329
+ )
249
330
  : Promise.resolve(0),
250
331
  ]);
251
332
 
@@ -255,10 +336,18 @@ export async function getWorkspaceInsights(
255
336
  factBuckets: factDays,
256
337
  rootDrivers,
257
338
  priorRootDrivers,
339
+ projects,
340
+ privateChats,
341
+ privateChatsTruncated,
258
342
  scheduleFacts,
259
343
  facets,
260
344
  recentCalls,
261
345
  promptContributions,
346
+ dataThrough,
347
+ driverGroups,
348
+ driversTruncated,
349
+ facetsTruncated,
350
+ recentCallsTruncated,
262
351
  } = modelBundle;
263
352
  const {
264
353
  workspaceCreditMicros,
@@ -271,17 +360,21 @@ export async function getWorkspaceInsights(
271
360
  agentRunsUsed,
272
361
  } = usageBundle;
273
362
  const [attached, fireCounts] = await Promise.all([
274
- countSessionsAttachedToGroups(
275
- db,
276
- input.workspaceId,
277
- warmGroups.map((group) => group.groupId),
363
+ measureInsightsPhase(observePhase, "attached_sessions", () =>
364
+ countSessionsAttachedToGroups(
365
+ db,
366
+ input.workspaceId,
367
+ warmGroups.map((group) => group.groupId),
368
+ ),
369
+ ),
370
+ measureInsightsPhase(observePhase, "scheduled_fires", () =>
371
+ countScheduledTaskFires(db, {
372
+ workspaceId: input.workspaceId,
373
+ since: window.since,
374
+ until: window.until,
375
+ taskIds: tasks.map((task) => task.id),
376
+ }),
278
377
  ),
279
- countScheduledTaskFires(db, {
280
- workspaceId: input.workspaceId,
281
- since: window.since,
282
- until: window.until,
283
- taskIds: tasks.map((task) => task.id),
284
- }),
285
378
  ]);
286
379
  const backendByGroup = new Map(liveWarm.map((lease) => [lease.groupId, lease.backend]));
287
380
  const warmSecondsByGroup = new Map(warmGroups.map((group) => [group.groupId, group.warmSeconds]));
@@ -345,6 +438,8 @@ export async function getWorkspaceInsights(
345
438
  0,
346
439
  );
347
440
  const modelCalls = modelRows.reduce((sum, row) => sum + row.calls, 0);
441
+ const cachedTokens = modelRows.reduce((sum, row) => sum + row.cachedTokens, 0);
442
+ const cacheInputTokens = modelRows.reduce((sum, row) => sum + row.cacheInputTokens, 0);
348
443
  const priorInputTokens = priorModelRows.reduce((sum, row) => sum + row.inputTokens, 0);
349
444
  const priorTotalTokens = priorModelRows.reduce((sum, row) => sum + row.totalTokens, 0);
350
445
  const priorCachedTokens = priorModelRows.reduce((sum, row) => sum + row.cachedTokens, 0);
@@ -373,9 +468,10 @@ export async function getWorkspaceInsights(
373
468
  cacheKnownCalls: 0,
374
469
  calls: 0,
375
470
  };
376
- const modelCostMicros = modelFilterActive
377
- ? facts.costMicros
378
- : (usageBuckets.get(bucket)?.costMicros ?? facts.costMicros);
471
+ const modelCostMicros =
472
+ modelFilterActive || scopeActive
473
+ ? facts.costMicros
474
+ : (usageBuckets.get(bucket)?.costMicros ?? facts.costMicros);
379
475
  return {
380
476
  label: input.range === "today" ? bucket.slice(11) : bucket.slice(5),
381
477
  modelCostUsd: microsToUsd(modelCostMicros),
@@ -428,6 +524,20 @@ export async function getWorkspaceInsights(
428
524
  };
429
525
  });
430
526
 
527
+ const projectRows = projects.map((row) => ({
528
+ id: row.kind === "project" && row.channelId ? `project:${row.channelId}` : row.kind,
529
+ kind: row.kind,
530
+ label: insightsProjectLabel(row),
531
+ projects: row.projects,
532
+ rootSessions: row.rootSessions,
533
+ calls: row.calls,
534
+ creditUsd: microsToUsd(row.pricedCostMicros),
535
+ estimatedProviderUsd: microsToUsd(row.estimatedProviderCostMicros),
536
+ estimatedProviderCostKnownCalls: row.estimatedProviderCostKnownCalls,
537
+ tokens: row.totalTokens,
538
+ cacheHitPct: cacheHitPct(row.cachedTokens, row.cacheInputTokens),
539
+ }));
540
+
431
541
  const scheduleFactById = new Map(scheduleFacts.map((row) => [row.scheduledTaskId, row] as const));
432
542
  const schedules = tasks.map((task) => {
433
543
  const fact = scheduleFactById.get(task.id);
@@ -488,6 +598,9 @@ export async function getWorkspaceInsights(
488
598
  sessions: bucket.sessions,
489
599
  })),
490
600
  drivers,
601
+ projects: projectRows,
602
+ privateChats,
603
+ privateChatsTruncated,
491
604
  schedules,
492
605
  recentCalls: recentCalls.map((row) => ({
493
606
  id: row.id,
@@ -558,6 +671,7 @@ export async function getWorkspaceInsights(
558
671
  priorInputTokens,
559
672
  priorTotalTokens,
560
673
  priorCacheHitPct: cacheHitPct(priorCachedTokens, priorCacheInputTokens),
674
+ priorCacheInputTokens,
561
675
  priorCalls,
562
676
  goalsActive: depth.goalsActive,
563
677
  goalsCompleted: depth.goalsCompleted,
@@ -577,7 +691,17 @@ export async function getWorkspaceInsights(
577
691
  billableTokenCap: limits.maxMonthlyTokensPerWorkspace ?? null,
578
692
  agentRunsUsed,
579
693
  agentRunCap: limits.maxMonthlyAgentRunsPerWorkspace ?? null,
580
- modelFilterActive,
694
+ modelFilterActive: modelFilterActive || scopeActive,
695
+ dataThrough: dataThrough?.toISOString() ?? null,
696
+ cacheHitPct: cacheHitPct(cachedTokens, cacheInputTokens),
697
+ scope: {
698
+ rootSessionId: visibleRootScope ? rootSessionId : null,
699
+ sessionId: visibleSessionScope ? sessionId : null,
700
+ },
701
+ driverGroups,
702
+ driversTruncated,
703
+ facetsTruncated,
704
+ recentCallsTruncated,
581
705
  });
582
706
 
583
707
  return { snapshot };
@@ -12,6 +12,7 @@ export async function prepareKnowledgeFile(
12
12
  deps: Pick<ApiRouteDeps, "db" | "objectStorage" | "getDocumentServices">,
13
13
  context: KnowledgeContext,
14
14
  fileId: string,
15
+ purpose: "evidence" | "reference" = "evidence",
15
16
  ) {
16
17
  const inspected = await inspectKnowledgeFilePreparation(deps.db, context, fileId);
17
18
  if (inspected.status !== "prepare") return inspected;
@@ -27,13 +28,19 @@ export async function prepareKnowledgeFile(
27
28
  ) {
28
29
  throw new Error("The original file no longer matches its retained metadata");
29
30
  }
30
- const parsed = await deps.getDocumentServices().parser.parse(object.bytes, inspected.file);
31
- // Empty extraction is a visible failure, not a searchable-ready receipt.
32
- if (!parsed.text.trim()) throw new Error("No searchable text could be extracted from this file");
31
+ // A screenshot/diagram can support a visual finding without containing text.
32
+ // Keep its exact original; OCR is neither required evidence nor the finding.
33
+ const originalOnly = purpose === "evidence" && inspected.file.contentType.startsWith("image/");
34
+ const parsed = originalOnly
35
+ ? { text: "" }
36
+ : await deps.getDocumentServices().parser.parse(object.bytes, inspected.file);
37
+ if (!originalOnly && !parsed.text.trim())
38
+ throw new Error("No searchable text could be extracted from this file");
33
39
  return completeKnowledgeFilePreparation(deps.db, context, {
34
40
  fileId,
35
41
  title: inspected.file.filename,
36
42
  sourceVersion,
37
43
  content: parsed.text,
44
+ purpose,
38
45
  });
39
46
  }
@@ -82,6 +82,7 @@ export async function retainKnowledgeMessage(
82
82
  content: text,
83
83
  source: {
84
84
  kind: "conversation",
85
+ purpose: "evidence",
85
86
  sessionId: event.sessionId,
86
87
  externalId: event.id,
87
88
  capturedAt: event.occurredAt,
@@ -0,0 +1,276 @@
1
+ import { expect, test } from "bun:test";
2
+ import type { KnowledgeEntryRecord, KnowledgeEntrySummary } from "@opengeni/contracts";
3
+ import { prepareKnowledgeSave } from "./knowledge-preparation";
4
+
5
+ const context = {
6
+ accountId: "account",
7
+ workspaceId: "workspace",
8
+ actor: { kind: "human", review: true },
9
+ } as never;
10
+ const db = {} as never;
11
+ const embedder = () => {
12
+ throw new Error("not used by fake search");
13
+ };
14
+ function group(title: string, parentIds: string[] = []): KnowledgeEntryRecord {
15
+ const id = crypto.randomUUID();
16
+ return {
17
+ id,
18
+ scope: "workspace",
19
+ version: 1,
20
+ publishedRevisionId: id,
21
+ latestRevisionId: id,
22
+ archived: false,
23
+ createdAt: "2026-09-14T00:00:00Z",
24
+ updatedAt: "2026-09-14T00:00:00Z",
25
+ revision: {
26
+ id,
27
+ entryId: id,
28
+ number: 1,
29
+ change: "upsert",
30
+ previousRevisionId: null,
31
+ restoredFromRevisionId: null,
32
+ createdAt: "2026-09-14T00:00:00Z",
33
+ createdBySessionId: null,
34
+ reviewBatchId: null,
35
+ outcome: "published",
36
+ entry: {
37
+ title,
38
+ kind: "group",
39
+ content: `Description of ${title}`,
40
+ groupIds: parentIds,
41
+ evidence: [],
42
+ relationships: [],
43
+ },
44
+ },
45
+ };
46
+ }
47
+ const summary = (record: KnowledgeEntryRecord) =>
48
+ ({
49
+ ...record,
50
+ revision: {
51
+ ...record.revision,
52
+ title: record.revision.entry.title,
53
+ kind: "group",
54
+ preview: "preview",
55
+ groupIds: record.revision.entry.groupIds,
56
+ sourceKind: null,
57
+ },
58
+ excerpts: [],
59
+ }) as KnowledgeEntrySummary;
60
+
61
+ test("one read returns the complete paginated hierarchy and separately labeled duplicate candidates", async () => {
62
+ const parent = group("Engineering");
63
+ const child = group("Product UI", [parent.id]);
64
+ const pending = group("Proposed collection");
65
+ pending.revision.outcome = "pending";
66
+ const revoked = group("Revoked");
67
+ const calls: unknown[] = [];
68
+ const result = await prepareKnowledgeSave(
69
+ db,
70
+ context,
71
+ { query: "plugin setup consistency" },
72
+ embedder,
73
+ {
74
+ list: async (actualDb, actualContext, request) => {
75
+ expect(actualDb).toBe(db);
76
+ expect(actualContext).toBe(context);
77
+ calls.push(request);
78
+ if (request?.view === "needs_review")
79
+ return { entries: [summary(pending)], nextCursor: null };
80
+ return request?.cursor
81
+ ? { entries: [summary(child), summary(revoked)], nextCursor: null }
82
+ : { entries: [summary(parent)], nextCursor: "page2" };
83
+ },
84
+ get: async (_db, _context, id, options) => {
85
+ expect(options?.revisionId).toBeUndefined();
86
+ return [parent, child, pending].find((record) => record.id === id) ?? null;
87
+ },
88
+ search: async (_db, actualContext, request) => {
89
+ expect(actualContext).toBe(context);
90
+ expect(request.query).toBe("plugin setup consistency");
91
+ expect(request.groupId).toBeUndefined();
92
+ expect(request.scope).toBeUndefined();
93
+ return {
94
+ entries: [summary(request.view === "needs_review" ? pending : child)],
95
+ nextCursor: null,
96
+ searchMode: "hybrid",
97
+ };
98
+ },
99
+ },
100
+ );
101
+ expect(calls).toHaveLength(3);
102
+ expect(result.collections.complete).toBe(true);
103
+ expect(result.collections.entries.map((entry) => entry.title)).toEqual([
104
+ "Engineering",
105
+ "Product UI",
106
+ "Proposed collection",
107
+ ]);
108
+ expect(result.collections.entries[1]?.parentIds).toEqual([parent.id]);
109
+ expect(result.collections.entries[0]?.description).toBe("Description of Engineering");
110
+ expect(result.collections.entries[2]?.view).toBe("needs_review");
111
+ expect(result.matches.published.entries[0]?.id).toBe(child.id);
112
+ expect(result.matches.needs_review.entries[0]?.id).toBe(pending.id);
113
+ });
114
+
115
+ test("large collection maps explicitly continue and long descriptions report loss", async () => {
116
+ const record = group("Large collection");
117
+ record.revision.entry.content = "x".repeat(3000);
118
+ let count = 0;
119
+ const services: NonNullable<Parameters<typeof prepareKnowledgeSave>[4]> = {
120
+ list: async (_db, _context, request) => {
121
+ if (request?.view === "needs_review") return { entries: [], nextCursor: null };
122
+ return { entries: [summary(record)], nextCursor: `page${++count}` };
123
+ },
124
+ get: async () => record,
125
+ search: async () => ({ entries: [], nextCursor: null, searchMode: "keyword" }),
126
+ };
127
+ const result = await prepareKnowledgeSave(db, context, { query: "subject" }, embedder, services);
128
+ expect(result.collections.complete).toBe(false);
129
+ expect(result.collections.nextCursors.published).toBeTruthy();
130
+ expect(result.collections.nextCursors.needs_review).toBeNull();
131
+ expect(result.collections.entries[0]?.descriptionTruncated).toBe(true);
132
+ expect(result.collections.entries[0]?.description).toHaveLength(2000);
133
+ let requestedCursor: string | undefined;
134
+ await prepareKnowledgeSave(
135
+ db,
136
+ context,
137
+ { query: "subject", collectionCursors: result.collections.nextCursors },
138
+ embedder,
139
+ {
140
+ ...services,
141
+ list: async (_db, _context, request) => {
142
+ expect(request?.view).toBe("published");
143
+ requestedCursor = request?.cursor;
144
+ return { entries: [], nextCursor: null };
145
+ },
146
+ },
147
+ );
148
+ expect(requestedCursor).toBe(result.collections.nextCursors.published!);
149
+ });
150
+
151
+ test("a catalog authorization failure is not disguised as an empty complete map", async () => {
152
+ await expect(
153
+ prepareKnowledgeSave(db, context, { query: "subject" }, embedder, {
154
+ list: async () => {
155
+ throw new Error("access revoked");
156
+ },
157
+ get: async () => null,
158
+ search: async () => ({ entries: [], nextCursor: null, searchMode: "keyword" }),
159
+ }),
160
+ ).rejects.toThrow("access revoked");
161
+ });
162
+
163
+ test("read-only principals without proposal access fail before searching or embedding", async () => {
164
+ let touched = false;
165
+ await expect(
166
+ prepareKnowledgeSave(
167
+ db,
168
+ {
169
+ accountId: "account",
170
+ workspaceId: "workspace",
171
+ actor: { kind: "service", review: false },
172
+ } as never,
173
+ { query: "subject" },
174
+ () => {
175
+ touched = true;
176
+ throw new Error("must not embed");
177
+ },
178
+ {
179
+ list: async () => {
180
+ touched = true;
181
+ return { entries: [], nextCursor: null };
182
+ },
183
+ get: async () => {
184
+ touched = true;
185
+ return null;
186
+ },
187
+ search: async () => {
188
+ touched = true;
189
+ return { entries: [], nextCursor: null, searchMode: "keyword" };
190
+ },
191
+ },
192
+ ),
193
+ ).rejects.toMatchObject({ code: "42501" });
194
+ expect(touched).toBe(false);
195
+ });
196
+
197
+ test("published and pending searches reuse one query embedding", async () => {
198
+ let calls = 0;
199
+ await prepareKnowledgeSave(
200
+ db,
201
+ context,
202
+ { query: "plugin dialog" },
203
+ () => ({
204
+ model: "test",
205
+ dimensions: 2,
206
+ embedMany: async () => [],
207
+ embedQuery: async () => {
208
+ calls++;
209
+ return [1, 0];
210
+ },
211
+ }),
212
+ {
213
+ list: async () => ({ entries: [], nextCursor: null }),
214
+ get: async () => null,
215
+ search: async (_db, _context, request, queryEmbedder) => {
216
+ expect(await queryEmbedder().embedQuery(request.query!)).toEqual([1, 0]);
217
+ return { entries: [], nextCursor: null, searchMode: "hybrid" };
218
+ },
219
+ },
220
+ );
221
+ expect(calls).toBe(1);
222
+ });
223
+
224
+ test("a keyword fallback in the first view never retries an unfunded provider in the review view", async () => {
225
+ const searches: Array<{ mode: string | undefined; settingsProvided: boolean }> = [];
226
+ await prepareKnowledgeSave(
227
+ db,
228
+ context,
229
+ { query: "Acme renewal" },
230
+ () => {
231
+ throw new Error("An unfunded provider was called");
232
+ },
233
+ {
234
+ list: async () => ({ entries: [], nextCursor: null }),
235
+ get: async () => null,
236
+ search: async (_db, _context, request, _embedder, settings) => {
237
+ searches.push({ mode: request.mode, settingsProvided: !!settings });
238
+ return { entries: [], nextCursor: null, searchMode: "keyword" };
239
+ },
240
+ },
241
+ { documentEmbeddingProvider: "openai", documentEmbeddingBillingMode: "credits" } as never,
242
+ );
243
+ expect(searches).toEqual([
244
+ { mode: "keyword", settingsProvided: false },
245
+ { mode: "keyword", settingsProvided: false },
246
+ ]);
247
+ });
248
+
249
+ test("catalog rechecks current metadata after a collection changes", async () => {
250
+ const old = group("Old name");
251
+ const current = {
252
+ ...old,
253
+ version: 2,
254
+ revision: {
255
+ ...old.revision,
256
+ id: crypto.randomUUID(),
257
+ entry: { ...old.revision.entry, title: "New name" },
258
+ },
259
+ };
260
+ const result = await prepareKnowledgeSave(db, context, { query: "subject" }, embedder, {
261
+ list: async (_db, _context, request) => ({
262
+ entries: request?.view === "published" ? [summary(old)] : [],
263
+ nextCursor: null,
264
+ }),
265
+ get: async (_db, _context, _id, options) => {
266
+ expect(options?.revisionId).toBeUndefined();
267
+ return current;
268
+ },
269
+ search: async () => ({ entries: [], nextCursor: null, searchMode: "keyword" }),
270
+ });
271
+ expect(result.collections.entries[0]).toMatchObject({
272
+ title: "New name",
273
+ version: 2,
274
+ revisionId: current.revision.id,
275
+ });
276
+ });