@opengeni/core 0.12.10 → 0.14.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 (39) hide show
  1. package/dist/access/index.d.ts +22 -0
  2. package/dist/application/new-session-drafts.d.ts +14 -0
  3. package/dist/application/session-commands.d.ts +107 -0
  4. package/dist/billing/limits.d.ts +29 -0
  5. package/dist/dependencies.d.ts +137 -0
  6. package/dist/domain/capabilities.d.ts +62 -0
  7. package/dist/domain/environments.d.ts +33 -0
  8. package/dist/domain/insights.d.ts +11 -0
  9. package/dist/domain/packs.d.ts +27 -0
  10. package/dist/domain/resources.d.ts +32 -0
  11. package/dist/domain/scheduled-tasks.d.ts +72 -0
  12. package/dist/domain/session-tool-policy.d.ts +31 -0
  13. package/dist/domain/sessions.d.ts +256 -0
  14. package/dist/domain/slack-bot.d.ts +19 -0
  15. package/dist/domain/workspace-members.d.ts +34 -0
  16. package/dist/index.d.ts +23 -1199
  17. package/dist/index.js +693 -53
  18. package/dist/index.js.map +1 -1
  19. package/dist/managed-auth-type.d.ts +2 -0
  20. package/dist/rigs/index.d.ts +57 -0
  21. package/dist/sandbox/fleet.d.ts +197 -0
  22. package/dist/sandbox/routing.d.ts +55 -0
  23. package/dist/sandbox-types.d.ts +52 -0
  24. package/dist/session-authorization.d.ts +36 -0
  25. package/dist/transcription.d.ts +71 -0
  26. package/dist/workflow-wake-contract.d.ts +4 -0
  27. package/package.json +11 -11
  28. package/src/access/index.ts +73 -2
  29. package/src/application/new-session-drafts.ts +3 -0
  30. package/src/application/session-commands.ts +3 -1
  31. package/src/dependencies.ts +5 -0
  32. package/src/domain/insights.ts +480 -0
  33. package/src/domain/session-tool-policy.ts +17 -25
  34. package/src/domain/sessions.ts +75 -4
  35. package/src/domain/slack-bot.ts +2 -4
  36. package/src/index.ts +2 -0
  37. package/src/sandbox/fleet.ts +96 -33
  38. package/src/sandbox/routing.ts +29 -7
  39. package/src/transcription.ts +142 -0
@@ -16,7 +16,7 @@ import type {
16
16
  WorkspaceInferenceControlRequest,
17
17
  WorkspaceInferenceControlResponse,
18
18
  } from "@opengeni/contracts";
19
- import { reasoningEffortForMetadata } from "@opengeni/contracts";
19
+ import { latencyModeForMetadata, reasoningEffortForMetadata } from "@opengeni/contracts";
20
20
  import {
21
21
  deleteSessionQueueItemInTransaction,
22
22
  editQueuedTurnInTransaction,
@@ -401,6 +401,7 @@ function composerDraft(
401
401
  resources: row.resources as ComposerDraft["resources"],
402
402
  model: row.model,
403
403
  reasoningEffort: row.reasoningEffort as ComposerDraft["reasoningEffort"],
404
+ latencyMode: row.latencyMode as ComposerDraft["latencyMode"],
404
405
  sourceTurnId: row.sourceTurnId,
405
406
  sourceTurnVersion: row.sourceTurnVersion,
406
407
  updatedAt: row.updatedAt.toISOString(),
@@ -663,6 +664,7 @@ export async function getHumanComposerDraft(
663
664
  resources: [],
664
665
  model: session.model,
665
666
  reasoningEffort: reasoningEffortForMetadata(session.metadata, "medium"),
667
+ latencyMode: latencyModeForMetadata(session.metadata, "standard"),
666
668
  sourceTurnId: null,
667
669
  sourceTurnVersion: null,
668
670
  updatedAt: null,
@@ -14,6 +14,7 @@ import type { Observability } from "@opengeni/observability";
14
14
  import type { createObjectStorage } from "@opengeni/storage";
15
15
  import type { ManagedAuth } from "./managed-auth-type";
16
16
  import type { ApiSandboxClient, ResumeBoxByIdInput, ResumedSandboxSession } from "./sandbox-types";
17
+ import type { TranscriptionService } from "./transcription";
17
18
 
18
19
  export type SessionWorkflowClient = {
19
20
  signalUserMessage: (input: {
@@ -110,6 +111,10 @@ export type AppDependencies = {
110
111
  codexFetch?: typeof fetch;
111
112
  /** Injectable Slack Web API transport for deterministic bot-connection tests. */
112
113
  slackFetch?: typeof fetch;
114
+ /** Injectable Google OAuth/Drive transport for deterministic connector tests. */
115
+ googleDriveFetch?: typeof fetch;
116
+ /** Optional host-owned voice-input transcription service. */
117
+ transcription?: TranscriptionService | null;
113
118
  // The API process's OWN agent-loop-free sandbox client (constructed from
114
119
  // settings via @opengeni/runtime/sandbox). Undefined when sandboxBackend=none.
115
120
  // This is the foundation of the API-direct control plane: the API resumes
@@ -0,0 +1,480 @@
1
+ import { configuredStaticUsageLimits, type Settings } from "@opengeni/config";
2
+ import {
3
+ WorkspaceInsightsSnapshot,
4
+ type InsightsRange,
5
+ type WorkspaceInsightsResponse,
6
+ } from "@opengeni/contracts";
7
+ import {
8
+ aggregateModelCallFacts,
9
+ aggregateModelCallFactsByDay,
10
+ aggregateRootSessionDrivers,
11
+ aggregateScheduleFacts,
12
+ aggregateSessionDepth,
13
+ aggregateWarmSecondsByGroup,
14
+ countOnlineMachines,
15
+ countScheduledTaskFires,
16
+ countSessionsAttachedToGroups,
17
+ enumerateUtcDays,
18
+ listFloorSessions,
19
+ listLiveWarmLeases,
20
+ listModelCallFacets,
21
+ listScheduledTasks,
22
+ requireWorkspace,
23
+ sumUsageQuantity,
24
+ sumUsageQuantityByDay,
25
+ sumUsageQuantityInRange,
26
+ type Database,
27
+ } from "@opengeni/db";
28
+
29
+ const MACHINE_HEARTBEAT_FRESH_MS = 120_000;
30
+
31
+ export type GetWorkspaceInsightsInput = {
32
+ workspaceId: string;
33
+ range: InsightsRange;
34
+ provider?: string | null;
35
+ model?: string | null;
36
+ now?: Date;
37
+ };
38
+
39
+ function microsToUsd(micros: number): number {
40
+ return Math.round((micros / 1_000_000) * 100) / 100;
41
+ }
42
+
43
+ function cacheHitPct(cached: number, input: number): number {
44
+ if (input <= 0) return 0;
45
+ return Math.min(100, Math.max(0, Math.round((cached / input) * 100)));
46
+ }
47
+
48
+ function startOfUtcDay(date: Date): Date {
49
+ return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
50
+ }
51
+
52
+ function startOfUtcMonth(date: Date): Date {
53
+ return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 1));
54
+ }
55
+
56
+ function startOfUtcYear(date: Date): Date {
57
+ return new Date(Date.UTC(date.getUTCFullYear(), 0, 1));
58
+ }
59
+
60
+ function resolveRangeWindow(
61
+ range: InsightsRange,
62
+ now: Date,
63
+ ): {
64
+ since: Date;
65
+ until: Date;
66
+ priorSince: Date;
67
+ priorUntil: Date;
68
+ rangeLabel: string;
69
+ priorLabel: string;
70
+ seriesLabel: string;
71
+ cacheSeriesLabel: string;
72
+ } {
73
+ const until = now;
74
+ let since: Date;
75
+ let rangeLabel: string;
76
+ let priorLabel: string;
77
+ let seriesLabel: string;
78
+ let cacheSeriesLabel: string;
79
+ switch (range) {
80
+ case "today":
81
+ since = startOfUtcDay(now);
82
+ rangeLabel = "Today (UTC)";
83
+ priorLabel = "Prior equal window";
84
+ seriesLabel = "Credit $ (UTC day)";
85
+ cacheSeriesLabel = "Cache hit %";
86
+ break;
87
+ case "week":
88
+ since = new Date(startOfUtcDay(now).getTime() - 6 * 24 * 60 * 60 * 1000);
89
+ rangeLabel = "Last 7 days (UTC)";
90
+ priorLabel = "Prior 7 days";
91
+ seriesLabel = "Credit $ / day";
92
+ cacheSeriesLabel = "Cache hit % / day";
93
+ break;
94
+ case "month":
95
+ since = startOfUtcMonth(now);
96
+ rangeLabel = "This month (UTC)";
97
+ priorLabel = "Prior equal window";
98
+ seriesLabel = "Credit $ / day";
99
+ cacheSeriesLabel = "Cache hit % / day";
100
+ break;
101
+ case "ytd":
102
+ since = startOfUtcYear(now);
103
+ rangeLabel = "Year to date (UTC)";
104
+ priorLabel = "Prior equal window";
105
+ seriesLabel = "Credit $ / day";
106
+ cacheSeriesLabel = "Cache hit % / day";
107
+ break;
108
+ default: {
109
+ const _exhaustive: never = range;
110
+ throw new Error(`Unknown insights range: ${_exhaustive}`);
111
+ }
112
+ }
113
+ const durationMs = until.getTime() - since.getTime();
114
+ const priorUntil = since;
115
+ const priorSince = new Date(priorUntil.getTime() - Math.max(durationMs, 1));
116
+ return {
117
+ since,
118
+ until,
119
+ priorSince,
120
+ priorUntil,
121
+ rangeLabel,
122
+ priorLabel,
123
+ seriesLabel,
124
+ cacheSeriesLabel,
125
+ };
126
+ }
127
+
128
+ function ageLabel(updatedAt: Date, now: Date): string {
129
+ const ms = Math.max(0, now.getTime() - updatedAt.getTime());
130
+ const minutes = Math.floor(ms / 60_000);
131
+ if (minutes < 1) return "just now";
132
+ if (minutes < 60) return `${minutes}m`;
133
+ const hours = Math.floor(minutes / 60);
134
+ if (hours < 48) return `${hours}h`;
135
+ return `${Math.floor(hours / 24)}d`;
136
+ }
137
+
138
+ function floorState(input: {
139
+ status: string;
140
+ directControlState: string;
141
+ }): "running" | "paused" | "failed" | "idle" | "compacting" | "waiting" {
142
+ if (input.directControlState === "paused") return "paused";
143
+ switch (input.status) {
144
+ case "running":
145
+ return "running";
146
+ case "failed":
147
+ return "failed";
148
+ case "requires_action":
149
+ case "waiting":
150
+ return "waiting";
151
+ case "compacting":
152
+ return "compacting";
153
+ case "completed":
154
+ case "idle":
155
+ case "queued":
156
+ return "idle";
157
+ default:
158
+ return "idle";
159
+ }
160
+ }
161
+
162
+ function billingPathOf(value: string): "opengeni_credits" | "external" {
163
+ return value === "external" ? "external" : "opengeni_credits";
164
+ }
165
+
166
+ export async function getWorkspaceInsights(
167
+ db: Database,
168
+ settings: Settings,
169
+ input: GetWorkspaceInsightsInput,
170
+ ): Promise<WorkspaceInsightsResponse> {
171
+ await requireWorkspace(db, input.workspaceId);
172
+ const now = input.now ?? new Date();
173
+ const window = resolveRangeWindow(input.range, now);
174
+ const provider = input.provider?.trim() || null;
175
+ const model = input.model?.trim() || null;
176
+ const modelFilterActive = Boolean(provider || model);
177
+ const filter = { provider, model };
178
+
179
+ const [
180
+ workspaceCreditMicros,
181
+ priorWorkspaceCreditMicros,
182
+ warmSeconds,
183
+ priorWarmSeconds,
184
+ modelRows,
185
+ priorModelRows,
186
+ factDays,
187
+ warmDays,
188
+ costDays,
189
+ warmGroups,
190
+ liveWarm,
191
+ rootDrivers,
192
+ scheduleFacts,
193
+ tasks,
194
+ depth,
195
+ floorRows,
196
+ machinesOnline,
197
+ billableTokensUsed,
198
+ agentRunsUsed,
199
+ facets,
200
+ ] = await Promise.all([
201
+ sumUsageQuantityInRange(db, {
202
+ workspaceId: input.workspaceId,
203
+ eventType: "model.cost",
204
+ since: window.since,
205
+ until: window.until,
206
+ }),
207
+ sumUsageQuantityInRange(db, {
208
+ workspaceId: input.workspaceId,
209
+ eventType: "model.cost",
210
+ since: window.priorSince,
211
+ until: window.priorUntil,
212
+ }),
213
+ sumUsageQuantityInRange(db, {
214
+ workspaceId: input.workspaceId,
215
+ eventType: "sandbox.warm_seconds",
216
+ since: window.since,
217
+ until: window.until,
218
+ }),
219
+ sumUsageQuantityInRange(db, {
220
+ workspaceId: input.workspaceId,
221
+ eventType: "sandbox.warm_seconds",
222
+ since: window.priorSince,
223
+ until: window.priorUntil,
224
+ }),
225
+ aggregateModelCallFacts(db, {
226
+ workspaceId: input.workspaceId,
227
+ since: window.since,
228
+ until: window.until,
229
+ ...filter,
230
+ }),
231
+ aggregateModelCallFacts(db, {
232
+ workspaceId: input.workspaceId,
233
+ since: window.priorSince,
234
+ until: window.priorUntil,
235
+ ...filter,
236
+ }),
237
+ aggregateModelCallFactsByDay(db, {
238
+ workspaceId: input.workspaceId,
239
+ since: window.since,
240
+ until: window.until,
241
+ ...filter,
242
+ }),
243
+ sumUsageQuantityByDay(db, {
244
+ workspaceId: input.workspaceId,
245
+ eventType: "sandbox.warm_seconds",
246
+ since: window.since,
247
+ until: window.until,
248
+ }),
249
+ sumUsageQuantityByDay(db, {
250
+ workspaceId: input.workspaceId,
251
+ eventType: "model.cost",
252
+ since: window.since,
253
+ until: window.until,
254
+ }),
255
+ aggregateWarmSecondsByGroup(db, {
256
+ workspaceId: input.workspaceId,
257
+ since: window.since,
258
+ until: window.until,
259
+ limit: 24,
260
+ }),
261
+ listLiveWarmLeases(db, input.workspaceId),
262
+ aggregateRootSessionDrivers(db, {
263
+ workspaceId: input.workspaceId,
264
+ since: window.since,
265
+ until: window.until,
266
+ ...filter,
267
+ limit: 8,
268
+ }),
269
+ aggregateScheduleFacts(db, {
270
+ workspaceId: input.workspaceId,
271
+ since: window.since,
272
+ until: window.until,
273
+ ...filter,
274
+ }),
275
+ listScheduledTasks(db, input.workspaceId, 100),
276
+ aggregateSessionDepth(db, input.workspaceId),
277
+ listFloorSessions(db, input.workspaceId, 24),
278
+ settings.sandboxSelfhostedEnabled
279
+ ? countOnlineMachines(db, input.workspaceId, MACHINE_HEARTBEAT_FRESH_MS)
280
+ : Promise.resolve(0),
281
+ sumUsageQuantity(db, {
282
+ workspaceId: input.workspaceId,
283
+ eventType: "model.tokens",
284
+ since: startOfUtcMonth(now),
285
+ }),
286
+ sumUsageQuantity(db, {
287
+ workspaceId: input.workspaceId,
288
+ eventType: "agent_run.created",
289
+ since: startOfUtcMonth(now),
290
+ }),
291
+ listModelCallFacets(db, {
292
+ workspaceId: input.workspaceId,
293
+ since: window.since,
294
+ until: window.until,
295
+ }),
296
+ ]);
297
+
298
+ // Exact prior costs for the current top drivers — never a separate top-N page that
299
+ // drops roots and invents +$full as "new" spend.
300
+ const priorRootDrivers = await aggregateRootSessionDrivers(db, {
301
+ workspaceId: input.workspaceId,
302
+ since: window.priorSince,
303
+ until: window.priorUntil,
304
+ ...filter,
305
+ rootSessionIds: rootDrivers.map((row) => row.rootSessionId),
306
+ });
307
+
308
+ const attached = await countSessionsAttachedToGroups(
309
+ db,
310
+ input.workspaceId,
311
+ warmGroups.map((group) => group.groupId),
312
+ );
313
+ const backendByGroup = new Map(liveWarm.map((lease) => [lease.groupId, lease.backend]));
314
+ const warmSecondsByGroup = new Map(warmGroups.map((group) => [group.groupId, group.warmSeconds]));
315
+
316
+ const models = modelRows
317
+ .map((row) => ({
318
+ id: `${row.provider}:${row.model}:${row.billingPath}`,
319
+ model: row.model,
320
+ provider: row.provider,
321
+ billing: billingPathOf(row.billingPath),
322
+ calls: row.calls,
323
+ inputTokens: row.inputTokens,
324
+ outputTokens: row.outputTokens,
325
+ cachedTokens: row.cachedTokens,
326
+ cacheWriteTokens: row.cacheWriteTokens,
327
+ reasoningTokens: row.reasoningTokens,
328
+ creditUsd: microsToUsd(row.pricedCostMicros),
329
+ }))
330
+ .sort((a, b) => b.inputTokens - a.inputTokens);
331
+
332
+ const creditMicros = modelRows.reduce((sum, row) => sum + row.pricedCostMicros, 0);
333
+ const priorCreditMicros = priorModelRows.reduce((sum, row) => sum + row.pricedCostMicros, 0);
334
+ const priorInputTokens = priorModelRows.reduce((sum, row) => sum + row.inputTokens, 0);
335
+ const priorCachedTokens = priorModelRows.reduce((sum, row) => sum + row.cachedTokens, 0);
336
+ const priorCalls = priorModelRows.reduce((sum, row) => sum + row.calls, 0);
337
+
338
+ const days = enumerateUtcDays(window.since, window.until);
339
+ const series = days.map((day) => {
340
+ const facts = factDays.get(day) ?? {
341
+ costMicros: 0,
342
+ inputTokens: 0,
343
+ cachedTokens: 0,
344
+ calls: 0,
345
+ };
346
+ const modelCostMicros = modelFilterActive
347
+ ? facts.costMicros
348
+ : (costDays.get(day) ?? facts.costMicros);
349
+ return {
350
+ label: day.slice(5),
351
+ modelCostUsd: microsToUsd(modelCostMicros),
352
+ warmSeconds: warmDays.get(day) ?? 0,
353
+ inputTokens: facts.inputTokens,
354
+ cachedTokens: facts.cachedTokens,
355
+ cacheHitPct: cacheHitPct(facts.cachedTokens, facts.inputTokens),
356
+ calls: facts.calls,
357
+ };
358
+ });
359
+
360
+ const priorDriverByRoot = new Map(
361
+ priorRootDrivers.map((row) => [row.rootSessionId, row.pricedCostMicros]),
362
+ );
363
+ const creditUsdForPct = Math.max(microsToUsd(creditMicros), 0.01);
364
+ const drivers = rootDrivers.map((row) => {
365
+ const creditUsd = microsToUsd(row.pricedCostMicros);
366
+ const priorUsd = microsToUsd(priorDriverByRoot.get(row.rootSessionId) ?? 0);
367
+ return {
368
+ id: `root:${row.rootSessionId}`,
369
+ groupBy: "root_session" as const,
370
+ label: row.title?.trim() || row.rootSessionId.slice(0, 8),
371
+ creditUsd,
372
+ tokens: row.inputTokens,
373
+ cacheHitPct: cacheHitPct(row.cachedTokens, row.inputTokens),
374
+ pctOfCreditUsd: Math.min(100, Math.round((creditUsd / creditUsdForPct) * 100)),
375
+ deltaUsdVsPrior: Math.round((creditUsd - priorUsd) * 100) / 100,
376
+ };
377
+ });
378
+
379
+ const fireCounts = await countScheduledTaskFires(db, {
380
+ workspaceId: input.workspaceId,
381
+ since: window.since,
382
+ until: window.until,
383
+ taskIds: tasks.map((task) => task.id),
384
+ });
385
+ const scheduleFactById = new Map(scheduleFacts.map((row) => [row.scheduledTaskId, row] as const));
386
+ const schedules = tasks.map((task) => {
387
+ const fact = scheduleFactById.get(task.id);
388
+ return {
389
+ id: task.id,
390
+ name: task.name,
391
+ fires: fireCounts.get(task.id) ?? 0,
392
+ creditUsd: fact ? microsToUsd(fact.pricedCostMicros) : null,
393
+ tokens: fact ? fact.inputTokens : null,
394
+ cacheHitPct: fact ? cacheHitPct(fact.cachedTokens, fact.inputTokens) : null,
395
+ billing: fact ? billingPathOf(fact.billingPath) : null,
396
+ };
397
+ });
398
+
399
+ const limits = configuredStaticUsageLimits(settings);
400
+ // Floor sessions only carry product model, not provider. Filter when an exact
401
+ // model is selected; otherwise leave the list workspace-wide (captioned in UI).
402
+ const floor = floorRows
403
+ .filter((row) => {
404
+ if (!model) return true;
405
+ return row.model === model;
406
+ })
407
+ .map((row) => ({
408
+ id: row.id,
409
+ title: row.title?.trim() || "Untitled session",
410
+ state: floorState(row),
411
+ depth: row.nestedAgentDepth,
412
+ model: row.model,
413
+ provider: null,
414
+ ageLabel: ageLabel(row.updatedAt, now),
415
+ cacheHitPct: null,
416
+ route: row.sandboxBackend,
417
+ }));
418
+
419
+ const snapshot = WorkspaceInsightsSnapshot.parse({
420
+ range: input.range,
421
+ rangeLabel: window.rangeLabel,
422
+ priorLabel: window.priorLabel,
423
+ seriesLabel: window.seriesLabel,
424
+ cacheSeriesLabel: window.cacheSeriesLabel,
425
+ timezone: "UTC",
426
+ models,
427
+ facets,
428
+ series,
429
+ depth: depth.buckets.map((bucket) => ({
430
+ depth: bucket.depth,
431
+ sessions: bucket.sessions,
432
+ })),
433
+ drivers,
434
+ schedules,
435
+ warmSeconds,
436
+ priorWarmSeconds,
437
+ warmGroups: warmGroups.map((group) => ({
438
+ id: group.groupId,
439
+ groupId: group.groupId,
440
+ label: group.groupId.slice(0, 8),
441
+ backend: backendByGroup.get(group.groupId) ?? null,
442
+ warmSeconds: group.warmSeconds,
443
+ sessionsAttached: attached.get(group.groupId) ?? 0,
444
+ })),
445
+ liveWarm: liveWarm.map((lease) => ({
446
+ id: lease.id,
447
+ groupId: lease.groupId,
448
+ backend: lease.backend,
449
+ turnHolders: lease.turnHolders,
450
+ viewerHolders: lease.viewerHolders,
451
+ warmForLabel: lease.turnHolders > 0 ? "in use" : "idle warm",
452
+ warmSeconds: warmSecondsByGroup.get(lease.groupId) ?? 0,
453
+ })),
454
+ floor,
455
+ selfhostedEnabled: settings.sandboxSelfhostedEnabled,
456
+ machinesOnline,
457
+ workspaceCreditUsd: microsToUsd(workspaceCreditMicros),
458
+ priorWorkspaceCreditUsd: microsToUsd(priorWorkspaceCreditMicros),
459
+ creditUsd: microsToUsd(creditMicros),
460
+ priorCreditUsd: microsToUsd(priorCreditMicros),
461
+ priorInputTokens,
462
+ priorCacheHitPct: cacheHitPct(priorCachedTokens, priorInputTokens),
463
+ priorCalls,
464
+ goalsActive: depth.goalsActive,
465
+ goalsCompleted: depth.goalsCompleted,
466
+ sessionsTouched: depth.sessionsTouched,
467
+ rootSessions: depth.rootSessions,
468
+ deepestDepth: depth.deepestDepth,
469
+ deepestSessionTitle: depth.deepestSessionTitle || "",
470
+ avgDepth: Math.round(depth.avgDepth * 10) / 10,
471
+ warmIdleNow: liveWarm.filter((lease) => lease.turnHolders === 0).length,
472
+ billableTokensUsed,
473
+ billableTokenCap: limits.maxMonthlyTokensPerWorkspace ?? null,
474
+ agentRunsUsed,
475
+ agentRunCap: limits.maxMonthlyAgentRunsPerWorkspace ?? null,
476
+ modelFilterActive,
477
+ });
478
+
479
+ return { snapshot };
480
+ }
@@ -10,7 +10,6 @@ import {
10
10
  } from "@opengeni/contracts";
11
11
  import type { Database } from "@opengeni/db";
12
12
  import { settingsWithEnabledCapabilityMcpServers } from "./capabilities";
13
- import { enabledCapabilityMcpToolRefs } from "./resources";
14
13
 
15
14
  const MANDATORY_SESSION_MCP_SERVER_IDS = ["opengeni"] as const;
16
15
  const PROJECTABLE_REGISTRY_ID = /^[A-Za-z0-9_-]+$/;
@@ -32,6 +31,12 @@ function sortedIds(ids: Iterable<string>): string[] {
32
31
  return [...new Set(ids)].sort();
33
32
  }
34
33
 
34
+ /** Every configured runtime MCP defaults on; mandatory carrier IDs are separate. */
35
+ export function defaultSessionMcpServerIds(servers: Iterable<{ id: string }>): string[] {
36
+ const mandatory = new Set<string>(MANDATORY_SESSION_MCP_SERVER_IDS);
37
+ return sortedIds([...servers].map((server) => server.id).filter((id) => !mandatory.has(id)));
38
+ }
39
+
35
40
  function projectIds(ids: readonly string[]): { ids: string[]; truncated: boolean } {
36
41
  const projectable = ids.filter(
37
42
  (id) =>
@@ -49,13 +54,11 @@ function projectIds(ids: readonly string[]): { ids: string[]; truncated: boolean
49
54
  * Resolve the same ID-only policy used by API projections and worker turns.
50
55
  * This function never receives endpoint URLs, credentials, schemas, or live
51
56
  * probe results. `availableMcpServerIds` is the resolved runtime registry;
52
- * `defaultMcpServerIds` is the capability-only omitted-tools set.
57
+ * `defaultMcpServerIds` is the current configured omitted-tools default.
53
58
  */
54
59
  export function resolveSessionToolPolicy(input: SessionToolPolicyInput): ResolvedSessionToolPolicy {
55
60
  const policy = input.toolPolicy;
56
61
  const availableIds = new Set(input.availableMcpServerIds);
57
- // Never infer omitted-tools defaults from the full runtime registry: static
58
- // MCPs are explicit-only unless they are capability-derived defaults.
59
62
  const defaultIds = new Set(input.defaultMcpServerIds ?? []);
60
63
  const mandatoryIds: string[] = MANDATORY_SESSION_MCP_SERVER_IDS.filter((id) =>
61
64
  availableIds.has(id),
@@ -64,12 +67,14 @@ export function resolveSessionToolPolicy(input: SessionToolPolicyInput): Resolve
64
67
  const selectedRefs = mergeToolRefs([], input.sessionTools);
65
68
  const tracksWorkspaceDefaults = policy.mode === "workspace_default";
66
69
 
67
- // Optional capability refs are a historical materialization of a
68
- // workspace-default selection. They may outlive an installation or its
69
- // credentials; do not hand an unavailable optional ref to runtime, where it
70
- // would otherwise be an unknown MCP id. Strict historical refs intentionally
71
- // remain so their fail-loud compatibility contract is preserved.
72
- let toolRefs = selectedRefs.filter((tool) => tool.optional !== true || availableIds.has(tool.id));
70
+ // Persisted refs may outlive a capability installation, deployment config,
71
+ // or its credentials. Admission remains strict for newly requested refs, but
72
+ // turn-time materialization must not hand any no-longer-registered id to the
73
+ // runtime router: doing so fails before the model can respond and traps the
74
+ // session in an "Unknown MCP server id" loop. Keep the stale selection in the
75
+ // effective-policy projection below, while executable refs contain only the
76
+ // registry that is available for this exact turn.
77
+ let toolRefs = selectedRefs.filter((tool) => availableIds.has(tool.id));
73
78
  if (tracksWorkspaceDefaults) {
74
79
  toolRefs = mergeToolRefs(
75
80
  toolRefs,
@@ -159,19 +164,6 @@ export function resolveSessionToolPolicy(input: SessionToolPolicyInput): Resolve
159
164
  };
160
165
  }
161
166
 
162
- /**
163
- * Native provider tools that belong to the workspace-default capability set
164
- * follow the same omission/narrowing fence as deferred MCP tools. A durable
165
- * workspace-default policy receives them; fixed historical policies and an
166
- * explicit per-turn replacement do not. Provider support remains a separate
167
- * runtime gate and must also be true before a native tool is attached.
168
- */
169
- export function sessionToolPolicyAllowsDefaultNativeTools(
170
- policy: SessionEffectiveToolPolicy,
171
- ): boolean {
172
- return policy.mode === "workspace_default" && policy.lazyRouter.state === "required";
173
- }
174
-
175
167
  /** Current full runtime registry IDs, including configured static servers. */
176
168
  export async function workspaceSessionToolPolicyServerIds(
177
169
  db: Database,
@@ -182,14 +174,14 @@ export async function workspaceSessionToolPolicyServerIds(
182
174
  return sortedIds(runtimeSettings.mcpServers.map((server) => server.id));
183
175
  }
184
176
 
185
- /** Current omitted-tools defaults; this preserves capability-first behavior. */
177
+ /** Current omitted-tools defaults: every configured runtime MCP is on. */
186
178
  export async function workspaceSessionToolPolicyDefaultServerIds(
187
179
  db: Database,
188
180
  workspaceId: string,
189
181
  settings: Settings,
190
182
  ): Promise<string[]> {
191
183
  const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(db, workspaceId, settings);
192
- return sortedIds(enabledCapabilityMcpToolRefs(settings, runtimeSettings).map((tool) => tool.id));
184
+ return defaultSessionMcpServerIds(runtimeSettings.mcpServers);
193
185
  }
194
186
 
195
187
  /** Add a bounded, secret-safe effective projection to a session response. */