@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
@@ -1,12 +1,82 @@
1
1
  import { afterAll, expect, mock, test } from "bun:test";
2
+ import type { Settings } from "@opengeni/config";
3
+ import type { KnowledgeContext } from "@opengeni/db";
4
+ const serviceContext: KnowledgeContext = {
5
+ accountId: "account",
6
+ workspaceId: "workspace",
7
+ actor: {
8
+ kind: "service",
9
+ principalKind: "service",
10
+ subjectId: "service:knowledge",
11
+ writeScopes: ["workspace"],
12
+ settingsScopes: [],
13
+ review: false,
14
+ },
15
+ };
2
16
  const list = mock(
3
- async (_db: unknown, _context: unknown, _request: unknown, _embedding?: unknown) => ({
4
- entries: [],
5
- nextCursor: null,
6
- }),
17
+ async (_db: unknown, _context: unknown, _request: unknown, _embedding?: unknown) => {
18
+ if (failNextRetrieval) {
19
+ failNextRetrieval = false;
20
+ throw new Error("retrieval unavailable");
21
+ }
22
+ return { entries: [], nextCursor: null };
23
+ },
24
+ );
25
+ let balance = 0;
26
+ let usedChunks = 0;
27
+ let queryRateBytes = 0;
28
+ let queryRateMicros = 0;
29
+ let failNextRetrieval = false;
30
+ let failSettlement = false;
31
+ let allowanceRefusal: Record<string, unknown> | null = null;
32
+ let creditPolicy: { allowCreditModels: boolean } | null = null;
33
+ const allowanceChecks = mock(async (_db: unknown, _input: unknown) => allowanceRefusal);
34
+ const debits = mock(async (_db: unknown, input: { amountMicros: number }) => {
35
+ if (failSettlement) throw new Error("settlement unavailable");
36
+ balance -= input.amountMicros;
37
+ return { balance: { balanceMicros: balance }, debitedMicros: input.amountMicros };
38
+ });
39
+ const usage = mock(
40
+ async (_db: unknown, _input: { quantity: number; eventType: string }) => undefined,
7
41
  );
8
- mock.module("@opengeni/db", () => ({ listKnowledgeEntries: list }));
42
+ mock.module("@opengeni/db", () => ({
43
+ listKnowledgeEntries: list,
44
+ getSpendableCreditBalance: async () => ({ balanceMicros: balance }),
45
+ applyCreditDebitAfterUse: debits,
46
+ checkWorkspaceAllowance: allowanceChecks,
47
+ creditDebitAttributionForTurn: async (_db: unknown, input: { turnId: string }) => ({
48
+ kind: "turn",
49
+ turnId: input.turnId,
50
+ initiatingHumanSubjectId: "human:root",
51
+ }),
52
+ recordUsageEvent: usage,
53
+ withKnowledgeQueryAccountLock: async (
54
+ _db: unknown,
55
+ _account: unknown,
56
+ _workspace: unknown,
57
+ fn: (db: unknown) => Promise<unknown>,
58
+ ) => fn(_db),
59
+ isCodexBilledTurn: async () => false,
60
+ SUBSCRIPTION_CORE_XAI_PROVIDER: "xai",
61
+ // Knowledge queries and document indexing carry no inference model, so the
62
+ // imported billing helper must never consult subscription-route funding.
63
+ readSubscriptionCoreRouteFunding: async () => {
64
+ throw new Error("Unexpected subscription-route funding lookup in knowledge fixture");
65
+ },
66
+ getWorkspaceModelPolicy: async () => creditPolicy,
67
+ sumUsageQuantity: async (_db: unknown, input: { eventType: string }) =>
68
+ input.eventType === "document.query_embedding_bytes"
69
+ ? queryRateBytes
70
+ : input.eventType === "document.query_embedding_cost"
71
+ ? queryRateMicros
72
+ : usedChunks,
73
+ countScheduledTasksForWorkspace: async () => 0,
74
+ countWorkspacesForAccount: async () => 0,
75
+ countActiveApiKeysForWorkspace: async () => 0,
76
+ countActiveOrganizationApiKeysForAccount: async () => 0,
77
+ }));
9
78
  const { searchKnowledgeEntries } = await import("./knowledge-search");
79
+ const { checkLimit } = await import("../billing/limits");
10
80
  afterAll(() => mock.restore());
11
81
  const unavailable = () => {
12
82
  throw new Error("embedding unavailable");
@@ -18,15 +88,18 @@ test("hybrid fallback keeps lexical recall and passes the exact scope and cursor
18
88
  {
19
89
  query: "Acme renewal renew contract expiration renewal date",
20
90
  scope: "workspace",
91
+ createdSince: "2026-10-01T00:00:00Z",
21
92
  cursor: "page-2",
22
93
  },
23
94
  unavailable,
24
95
  );
25
96
  expect(result.searchMode).toBe("keyword");
97
+ expect(result.fallbackReason).toBe("provider_unavailable");
26
98
  expect(list.mock.calls.at(-1)?.[2]).toMatchObject({
27
99
  query: '"Acme" OR "renewal" OR "renew" OR "contract" OR "expiration" OR "date"',
28
100
  mode: "keyword",
29
101
  scope: "workspace",
102
+ createdSince: "2026-10-01T00:00:00Z",
30
103
  cursor: "page-2",
31
104
  });
32
105
  });
@@ -43,3 +116,542 @@ test("keyword and explicit search syntax remain unchanged", async () => {
43
116
  );
44
117
  expect(list.mock.calls.at(-1)?.[2]).toMatchObject({ query: "Acme renewal", mode: "keyword" });
45
118
  });
119
+
120
+ test("paid vector queries check funding, debit post-use, and preserve hybrid keyword fallback", async () => {
121
+ const settings = {
122
+ documentEmbeddingProvider: "openai",
123
+ documentEmbeddingBillingMode: "credits",
124
+ documentEmbeddingRateMicrosPerMillionBytes: 1_000_000,
125
+ } as Settings;
126
+ const context = serviceContext;
127
+ let embedded = 0;
128
+ const provider = () =>
129
+ ({
130
+ model: "test",
131
+ dimensions: 3,
132
+ embedQuery: async () => {
133
+ embedded++;
134
+ return [1, 0, 0];
135
+ },
136
+ }) as never;
137
+ balance = 0;
138
+ const unfunded = await searchKnowledgeEntries(
139
+ {} as never,
140
+ context,
141
+ { query: "paid" },
142
+ provider,
143
+ settings,
144
+ );
145
+ expect(unfunded.searchMode).toBe("keyword");
146
+ expect(unfunded.fallbackReason).toBe("awaiting_funding");
147
+ await expect(
148
+ searchKnowledgeEntries(
149
+ {} as never,
150
+ context,
151
+ { query: "paid", mode: "vector" },
152
+ provider,
153
+ settings,
154
+ ),
155
+ ).rejects.toMatchObject({ code: "knowledge_vector_funding_required" });
156
+ expect(embedded).toBe(0);
157
+ balance = 1;
158
+ const paid = await searchKnowledgeEntries(
159
+ {} as never,
160
+ context,
161
+ { query: "paid", mode: "vector" },
162
+ provider,
163
+ settings,
164
+ );
165
+ expect(paid.searchMode).toBe("vector");
166
+ expect(embedded).toBe(1);
167
+ expect(debits.mock.calls.at(-1)?.[1]).toMatchObject({
168
+ amountMicros: 4,
169
+ sourceType: "knowledge_query",
170
+ });
171
+ expect(balance).toBe(-3);
172
+ const priorUsage = usage.mock.calls.length;
173
+ await searchKnowledgeEntries(
174
+ {} as never,
175
+ context,
176
+ { query: "paid", mode: "keyword" },
177
+ provider,
178
+ settings,
179
+ );
180
+ expect(embedded).toBe(1);
181
+ expect(usage.mock.calls.length).toBe(priorUsage);
182
+ });
183
+
184
+ test("credits turned off falls back to keyword search and refuses vector before the provider", async () => {
185
+ const settings = {
186
+ documentEmbeddingProvider: "openai",
187
+ documentEmbeddingBillingMode: "credits",
188
+ documentEmbeddingRateMicrosPerMillionBytes: 1_000_000,
189
+ } as Settings;
190
+ let embedded = 0;
191
+ const provider = () =>
192
+ ({
193
+ model: "test",
194
+ dimensions: 3,
195
+ embedQuery: async () => {
196
+ embedded++;
197
+ return [1, 0, 0];
198
+ },
199
+ }) as never;
200
+ balance = 1_000_000;
201
+ creditPolicy = { allowCreditModels: false };
202
+ const priorDebits = debits.mock.calls.length;
203
+ const priorUsage = usage.mock.calls.length;
204
+ try {
205
+ const hybrid = await searchKnowledgeEntries(
206
+ {} as never,
207
+ serviceContext,
208
+ { query: "paid" },
209
+ provider,
210
+ settings,
211
+ );
212
+ expect(hybrid.searchMode).toBe("keyword");
213
+ expect(hybrid.fallbackReason).toBe("credits_disabled");
214
+ await expect(
215
+ searchKnowledgeEntries(
216
+ {} as never,
217
+ serviceContext,
218
+ { query: "paid", mode: "vector" },
219
+ provider,
220
+ settings,
221
+ ),
222
+ ).rejects.toMatchObject({ code: "knowledge_vector_credits_disabled" });
223
+ // Keyword pagination keeps working instead of the paid-cursor rejection.
224
+ const nextPage = await searchKnowledgeEntries(
225
+ {} as never,
226
+ serviceContext,
227
+ { query: "paid", cursor: "keyword-cursor" },
228
+ provider,
229
+ settings,
230
+ );
231
+ expect(nextPage.fallbackReason).toBe("credits_disabled");
232
+ expect(embedded).toBe(0);
233
+ expect(debits.mock.calls.length).toBe(priorDebits);
234
+ expect(usage.mock.calls.length).toBe(priorUsage);
235
+ creditPolicy = { allowCreditModels: true };
236
+ const paid = await searchKnowledgeEntries(
237
+ {} as never,
238
+ serviceContext,
239
+ { query: "paid", mode: "vector" },
240
+ provider,
241
+ settings,
242
+ );
243
+ expect(paid.searchMode).toBe("vector");
244
+ expect(embedded).toBe(1);
245
+ expect(debits.mock.calls.length).toBe(priorDebits + 1);
246
+ } finally {
247
+ creditPolicy = null;
248
+ }
249
+ });
250
+
251
+ test("credits turned off leaves unpriced query embeddings untouched", async () => {
252
+ creditPolicy = { allowCreditModels: false };
253
+ try {
254
+ const result = await searchKnowledgeEntries(
255
+ {} as never,
256
+ serviceContext,
257
+ { query: "free", mode: "vector" },
258
+ () => ({ model: "test", dimensions: 3, embedQuery: async () => [1, 0, 0] }) as never,
259
+ {
260
+ documentEmbeddingProvider: "deterministic",
261
+ documentEmbeddingBillingMode: "credits",
262
+ } as Settings,
263
+ );
264
+ expect(result.searchMode).toBe("vector");
265
+ } finally {
266
+ creditPolicy = null;
267
+ }
268
+ });
269
+
270
+ test("shadow query usage meters bytes without checking or consuming credits", async () => {
271
+ const settings = {
272
+ documentEmbeddingProvider: "openai",
273
+ documentEmbeddingBillingMode: "shadow",
274
+ documentEmbeddingRateMicrosPerMillionBytes: 1_000_000,
275
+ } as Settings;
276
+ balance = 0;
277
+ const before = debits.mock.calls.length;
278
+ const found = await searchKnowledgeEntries(
279
+ {} as never,
280
+ serviceContext,
281
+ { query: "é", mode: "vector" },
282
+ () => ({ model: "test", dimensions: 3, embedQuery: async () => [1, 0, 0] }) as never,
283
+ settings,
284
+ );
285
+ expect(found.searchMode).toBe("vector");
286
+ expect(usage.mock.calls.at(-1)?.[1]).toMatchObject({
287
+ quantity: 2,
288
+ eventType: "document.query_embedding_bytes",
289
+ });
290
+ expect(debits.mock.calls.length).toBe(before);
291
+ });
292
+
293
+ test("paid queries attribute exact turns or verified humans, never service/API-key subjects", async () => {
294
+ const settings = {
295
+ documentEmbeddingProvider: "openai",
296
+ documentEmbeddingBillingMode: "credits",
297
+ documentEmbeddingRateMicrosPerMillionBytes: 1_000_000,
298
+ } as Settings;
299
+ const actors: KnowledgeContext["actor"][] = [
300
+ {
301
+ kind: "agent",
302
+ sessionId: crypto.randomUUID(),
303
+ turnId: crypto.randomUUID(),
304
+ attemptId: crypto.randomUUID(),
305
+ executionGeneration: 1,
306
+ },
307
+ {
308
+ kind: "human",
309
+ principalKind: "human_session",
310
+ subjectId: "human:frozen",
311
+ writeScopes: ["workspace"],
312
+ settingsScopes: ["workspace"],
313
+ review: true,
314
+ },
315
+ {
316
+ ...serviceContext.actor,
317
+ principalKind: "api_key",
318
+ subjectId: "api-key:not-human",
319
+ } as KnowledgeContext["actor"],
320
+ serviceContext.actor,
321
+ ];
322
+ for (const actor of actors) {
323
+ balance = 100;
324
+ await searchKnowledgeEntries(
325
+ {} as never,
326
+ { ...serviceContext, actor },
327
+ { query: "paid", mode: "vector" },
328
+ () => ({ model: "test", dimensions: 3, embedQuery: async () => [1, 0, 0] }) as never,
329
+ settings,
330
+ );
331
+ const debit = debits.mock.calls.at(-1)?.[1] as unknown as {
332
+ metadata: Record<string, unknown>;
333
+ };
334
+ expect(debit.metadata).toEqual({
335
+ model: "test",
336
+ bytes: 4,
337
+ ...(actor.kind === "agent"
338
+ ? { turnId: actor.turnId }
339
+ : actor.kind === "human"
340
+ ? { initiatingHumanSubjectId: actor.subjectId }
341
+ : {}),
342
+ });
343
+ const receipt = usage.mock.calls.at(-1)?.[1] as unknown as {
344
+ eventType: string;
345
+ sourceResourceId: string;
346
+ initiatorContext: { creditDebitAttribution: Record<string, unknown> };
347
+ };
348
+ expect(receipt.eventType).toBe("document.query_embedding_cost");
349
+ expect(receipt.initiatorContext.creditDebitAttribution).toEqual(
350
+ actor.kind === "agent"
351
+ ? { kind: "turn", turnId: actor.turnId, initiatingHumanSubjectId: "human:root" }
352
+ : actor.kind === "human"
353
+ ? { kind: "human", initiatingHumanSubjectId: actor.subjectId }
354
+ : { kind: "service" },
355
+ );
356
+ }
357
+ });
358
+
359
+ test("paid query checks the frozen root human allowance before provider use", async () => {
360
+ const settings = {
361
+ documentEmbeddingProvider: "openai",
362
+ documentEmbeddingBillingMode: "credits",
363
+ documentEmbeddingRateMicrosPerMillionBytes: 1_000_000,
364
+ } as Settings;
365
+ let calls = 0;
366
+ const context: KnowledgeContext = {
367
+ ...serviceContext,
368
+ actor: {
369
+ kind: "agent",
370
+ sessionId: crypto.randomUUID(),
371
+ turnId: crypto.randomUUID(),
372
+ attemptId: crypto.randomUUID(),
373
+ executionGeneration: 1,
374
+ },
375
+ };
376
+ balance = 100;
377
+ const before = debits.mock.calls.length;
378
+ allowanceRefusal = {
379
+ code: "allowance_exhausted",
380
+ scope: "member",
381
+ subjectId: "human:root",
382
+ resetsAt: null,
383
+ message: "Member allowance exhausted",
384
+ };
385
+ const provider = () =>
386
+ ({
387
+ model: "test",
388
+ dimensions: 3,
389
+ embedQuery: async () => {
390
+ calls++;
391
+ return [1, 0, 0];
392
+ },
393
+ }) as never;
394
+ try {
395
+ const hybrid = await searchKnowledgeEntries(
396
+ {} as never,
397
+ context,
398
+ { query: "paid" },
399
+ provider,
400
+ settings,
401
+ );
402
+ expect(hybrid).toMatchObject({ searchMode: "keyword", fallbackReason: "quota" });
403
+ await expect(
404
+ searchKnowledgeEntries(
405
+ {} as never,
406
+ context,
407
+ { query: "paid", mode: "vector" },
408
+ provider,
409
+ settings,
410
+ ),
411
+ ).rejects.toMatchObject({ code: "allowance_exhausted", subjectId: "human:root" });
412
+ expect(allowanceChecks.mock.calls.at(-1)?.[1]).toMatchObject({ subjectId: "human:root" });
413
+ expect(calls).toBe(0);
414
+ expect(debits.mock.calls.length).toBe(before);
415
+ } finally {
416
+ allowanceRefusal = null;
417
+ }
418
+ });
419
+
420
+ test("paid query does not debit when vector retrieval fails after embedding", async () => {
421
+ const settings = {
422
+ documentEmbeddingProvider: "openai",
423
+ documentEmbeddingBillingMode: "credits",
424
+ documentEmbeddingRateMicrosPerMillionBytes: 1_000_000,
425
+ } as Settings;
426
+ balance = 1;
427
+ const debitsBefore = debits.mock.calls.length;
428
+ failNextRetrieval = true;
429
+ await expect(
430
+ searchKnowledgeEntries(
431
+ {} as never,
432
+ serviceContext,
433
+ { query: "paid", mode: "vector" },
434
+ () => ({ model: "test", dimensions: 3, embedQuery: async () => [1, 0, 0] }) as never,
435
+ settings,
436
+ ),
437
+ ).rejects.toThrow("retrieval unavailable");
438
+ expect(debits.mock.calls.length).toBe(debitsBefore);
439
+ });
440
+
441
+ test("query debit preserves initiating human when its caller context changes during provider work", async () => {
442
+ const context: KnowledgeContext = {
443
+ ...serviceContext,
444
+ actor: {
445
+ kind: "human",
446
+ principalKind: "human_session",
447
+ subjectId: "human:original",
448
+ writeScopes: ["workspace"],
449
+ settingsScopes: ["workspace"],
450
+ review: true,
451
+ },
452
+ };
453
+ balance = 100;
454
+ await searchKnowledgeEntries(
455
+ {} as never,
456
+ context,
457
+ { query: "paid", mode: "vector" },
458
+ () =>
459
+ ({
460
+ model: "test",
461
+ dimensions: 3,
462
+ embedQuery: async () => {
463
+ context.actor = serviceContext.actor;
464
+ return [1, 0, 0];
465
+ },
466
+ }) as never,
467
+ {
468
+ documentEmbeddingProvider: "openai",
469
+ documentEmbeddingBillingMode: "credits",
470
+ documentEmbeddingRateMicrosPerMillionBytes: 1_000_000,
471
+ } as Settings,
472
+ );
473
+ expect(debits.mock.calls.at(-1)?.[1]).toMatchObject({
474
+ metadata: { initiatingHumanSubjectId: "human:original" },
475
+ });
476
+ expect(usage.mock.calls.at(-1)?.[1]).toMatchObject({
477
+ initiatorContext: {
478
+ creditDebitAttribution: { kind: "human", initiatingHumanSubjectId: "human:original" },
479
+ },
480
+ });
481
+ });
482
+
483
+ test("paid semantic pagination fails closed before provider use", async () => {
484
+ let embedded = 0;
485
+ const before = debits.mock.calls.length;
486
+ await expect(
487
+ searchKnowledgeEntries(
488
+ {} as never,
489
+ serviceContext,
490
+ { query: "paid", mode: "vector", cursor: "next-page" },
491
+ () =>
492
+ ({
493
+ model: "test",
494
+ dimensions: 3,
495
+ embedQuery: async () => {
496
+ embedded++;
497
+ return [1, 0, 0];
498
+ },
499
+ }) as never,
500
+ {
501
+ documentEmbeddingProvider: "openai",
502
+ documentEmbeddingBillingMode: "credits",
503
+ documentEmbeddingRateMicrosPerMillionBytes: 1_000_000,
504
+ } as Settings,
505
+ ),
506
+ ).rejects.toThrow("pagination is unavailable");
507
+ expect(embedded).toBe(0);
508
+ expect(debits.mock.calls.length).toBe(before);
509
+ });
510
+
511
+ test("paid query rate ceiling falls back for hybrid and rejects vector without using the provider", async () => {
512
+ queryRateBytes = 64 * 1024;
513
+ balance = 1;
514
+ let embedded = 0;
515
+ const provider = () =>
516
+ ({
517
+ model: "test",
518
+ dimensions: 3,
519
+ embedQuery: async () => {
520
+ embedded++;
521
+ return [1, 0, 0];
522
+ },
523
+ }) as never;
524
+ const settings = {
525
+ documentEmbeddingProvider: "openai",
526
+ documentEmbeddingBillingMode: "credits",
527
+ documentEmbeddingRateMicrosPerMillionBytes: 1_000_000,
528
+ } as Settings;
529
+ try {
530
+ expect(
531
+ (
532
+ await searchKnowledgeEntries(
533
+ {} as never,
534
+ serviceContext,
535
+ { query: "paid" },
536
+ provider,
537
+ settings,
538
+ )
539
+ ).searchMode,
540
+ ).toBe("keyword");
541
+ await expect(
542
+ searchKnowledgeEntries(
543
+ {} as never,
544
+ serviceContext,
545
+ { query: "paid", mode: "vector" },
546
+ provider,
547
+ settings,
548
+ ),
549
+ ).rejects.toThrow("rate limit reached");
550
+ expect(embedded).toBe(0);
551
+ } finally {
552
+ queryRateBytes = 0;
553
+ }
554
+ });
555
+
556
+ test("paid micro-cost ceiling blocks repeated tiny queries before embedding", async () => {
557
+ balance = 1;
558
+ queryRateMicros = 10_000;
559
+ let embedded = 0;
560
+ try {
561
+ const result = await searchKnowledgeEntries(
562
+ {} as never,
563
+ serviceContext,
564
+ { query: "tiny" },
565
+ () =>
566
+ ({
567
+ model: "test",
568
+ dimensions: 3,
569
+ embedQuery: async () => {
570
+ embedded++;
571
+ return [1, 0, 0];
572
+ },
573
+ }) as never,
574
+ {
575
+ documentEmbeddingProvider: "openai",
576
+ documentEmbeddingBillingMode: "credits",
577
+ documentEmbeddingRateMicrosPerMillionBytes: 1_000_000,
578
+ } as Settings,
579
+ );
580
+ expect(result.searchMode).toBe("keyword");
581
+ expect(embedded).toBe(0);
582
+ } finally {
583
+ queryRateMicros = 0;
584
+ }
585
+ });
586
+
587
+ test("paid settlement failure propagates without a fallback", async () => {
588
+ balance = 1;
589
+ failSettlement = true;
590
+ const before = list.mock.calls.length;
591
+ try {
592
+ await expect(
593
+ searchKnowledgeEntries(
594
+ {} as never,
595
+ serviceContext,
596
+ { query: "paid" },
597
+ () => ({ model: "test", dimensions: 3, embedQuery: async () => [1, 0, 0] }) as never,
598
+ {
599
+ documentEmbeddingProvider: "openai",
600
+ documentEmbeddingBillingMode: "credits",
601
+ documentEmbeddingRateMicrosPerMillionBytes: 1_000_000,
602
+ } as Settings,
603
+ ),
604
+ ).rejects.toThrow("settlement unavailable");
605
+ expect(list.mock.calls.length).toBe(before + 1);
606
+ } finally {
607
+ failSettlement = false;
608
+ }
609
+ });
610
+
611
+ test("deterministic embeddings remain unpriced even with credits mode selected", async () => {
612
+ balance = 0;
613
+ const before = debits.mock.calls.length;
614
+ const found = await searchKnowledgeEntries(
615
+ {} as never,
616
+ serviceContext,
617
+ { query: "local", mode: "vector" },
618
+ () => ({ model: "local", dimensions: 3, embedQuery: async () => [1, 0, 0] }) as never,
619
+ {
620
+ documentEmbeddingProvider: "deterministic",
621
+ documentEmbeddingBillingMode: "credits",
622
+ documentEmbeddingRateMicrosPerMillionBytes: 1_000_000,
623
+ } as Settings,
624
+ );
625
+ expect(found.searchMode).toBe("vector");
626
+ expect(debits.mock.calls.length).toBe(before);
627
+ });
628
+
629
+ test("unpriced document source admission ignores zero credits but preserves the monthly chunk quota", async () => {
630
+ balance = 0;
631
+ usedChunks = 1;
632
+ const deps = {
633
+ db: {} as never,
634
+ settings: {
635
+ billingMode: "stripe",
636
+ usageLimitsMode: "static",
637
+ staticUsageLimitsJson: JSON.stringify({ maxDocumentIndexedChunksPerWorkspace: 2 }),
638
+ } as Settings,
639
+ };
640
+ expect(
641
+ await checkLimit(deps, {
642
+ accountId: "account",
643
+ workspaceId: "workspace",
644
+ action: "document:index",
645
+ quantity: 0,
646
+ }),
647
+ ).toMatchObject({ allowed: true });
648
+ usedChunks = 2;
649
+ expect(
650
+ await checkLimit(deps, {
651
+ accountId: "account",
652
+ workspaceId: "workspace",
653
+ action: "document:index",
654
+ quantity: 1,
655
+ }),
656
+ ).toMatchObject({ allowed: false, code: "max_document_indexed_chunks_per_workspace" });
657
+ });