@opengeni/api-router 2.5.0 → 2.6.4-canary.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.d.ts +2 -1
- package/dist/app.js +3 -1
- package/dist/auth/managed-auth-attempt-context.d.ts +5 -1
- package/dist/auth/managed-auth.d.ts +24 -1
- package/dist/{chunk-QESX7HDK.js → chunk-UOI7KAP3.js} +3240 -791
- package/dist/chunk-UOI7KAP3.js.map +1 -0
- package/dist/http/sse.d.ts +9 -0
- package/dist/index.js +117 -5
- package/dist/index.js.map +1 -1
- package/dist/interaction-metrics.d.ts +2 -0
- package/dist/mcp/company-brain-governed-writes.d.ts +1 -1
- package/dist/mcp/company-profile-agent-admin.d.ts +6 -6
- package/dist/mcp/remember.d.ts +2 -2
- package/dist/mcp/scheduled-task-view.d.ts +3 -3
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/session-view.d.ts +1 -0
- package/dist/mcp/session-wait.d.ts +19 -0
- package/dist/model-catalog.d.ts +5 -31
- package/dist/routes/api-keys.d.ts +2 -0
- package/dist/routes/browser-sessions.d.ts +1 -0
- package/dist/routes/computer-sessions.d.ts +12 -0
- package/dist/routes/managed-auth-session-sets.d.ts +7 -0
- package/dist/routes/workspaces.d.ts +1 -1
- package/dist/sandbox/metrics-ingestion.d.ts +16 -0
- package/dist/workspace-delete-observability.d.ts +10 -0
- package/package.json +19 -19
- package/src/app.ts +206 -29
- package/src/auth/managed-auth-attempt-context.ts +40 -3
- package/src/auth/managed-auth-session-adapter.ts +1 -0
- package/src/auth/managed-auth.ts +164 -4
- package/src/http/sse.ts +279 -45
- package/src/index.ts +13 -1
- package/src/integrations/oauth-client.ts +8 -1
- package/src/integrations/provider-oauth.ts +12 -2
- package/src/integrations/slack-interactions.ts +77 -37
- package/src/interaction-metrics.ts +30 -0
- package/src/mcp/company-brain-governed-writes.ts +38 -23
- package/src/mcp/company-profile-agent-admin.ts +7 -7
- package/src/mcp/remember.ts +19 -8
- package/src/mcp/server.ts +328 -27
- package/src/mcp/session-wait.ts +56 -8
- package/src/model-catalog.ts +45 -337
- package/src/routes/api-integrations.ts +2 -2
- package/src/routes/api-keys.ts +149 -7
- package/src/routes/automations.ts +186 -19
- package/src/routes/browser-sessions.ts +10 -2
- package/src/routes/capabilities.ts +3 -3
- package/src/routes/codex.ts +483 -74
- package/src/routes/company-profile.ts +64 -0
- package/src/routes/computer-sessions.ts +103 -1
- package/src/routes/connections.ts +271 -16
- package/src/routes/github.ts +0 -8
- package/src/routes/integration-facets.ts +8 -5
- package/src/routes/interaction-resources.ts +7 -1
- package/src/routes/managed-auth-session-sets.ts +199 -2
- package/src/routes/organization-memberships.ts +28 -8
- package/src/routes/packs.ts +5 -5
- package/src/routes/plugins.ts +2 -2
- package/src/routes/pr-review.ts +80 -4
- package/src/routes/scheduled-tasks.ts +47 -11
- package/src/routes/sessions.ts +75 -53
- package/src/routes/skills.ts +3 -3
- package/src/routes/supergrok.ts +3 -2
- package/src/routes/workspaces.ts +665 -56
- package/src/sandbox/channel-a.ts +10 -4
- package/src/sandbox/machines.ts +13 -6
- package/src/sandbox/metrics-ingestion.ts +157 -3
- package/src/sandbox/viewer.ts +20 -1
- package/src/workspace-delete-observability.ts +75 -0
- package/dist/chunk-QESX7HDK.js.map +0 -1
package/src/sandbox/channel-a.ts
CHANGED
|
@@ -86,6 +86,8 @@ import {
|
|
|
86
86
|
type RoutingSandboxSession,
|
|
87
87
|
} from "@opengeni/runtime/sandbox";
|
|
88
88
|
import {
|
|
89
|
+
managedSessionGroupBackend,
|
|
90
|
+
managedSessionGroupOs,
|
|
89
91
|
providerSettingsForSessionSandboxRuntime,
|
|
90
92
|
relayConfigFromSettings,
|
|
91
93
|
resolveSessionSandboxRuntime,
|
|
@@ -643,15 +645,19 @@ async function withChannelAOperation<T>(
|
|
|
643
645
|
// A machine-home session with no selected machine uses the deployment's
|
|
644
646
|
// managed group box, exactly like the worker turn path. Keep the durable
|
|
645
647
|
// home label honest; only this request's effective backend changes.
|
|
646
|
-
|
|
648
|
+
const groupBackend = managedSessionGroupBackend(
|
|
649
|
+
settings.sandboxBackend,
|
|
650
|
+
session.sandboxBackend,
|
|
651
|
+
);
|
|
652
|
+
if (groupBackend) {
|
|
647
653
|
return await withChannelAOperation(
|
|
648
654
|
services,
|
|
649
655
|
{
|
|
650
656
|
...ctx,
|
|
651
657
|
session: {
|
|
652
658
|
...session,
|
|
653
|
-
sandboxBackend:
|
|
654
|
-
sandboxOs:
|
|
659
|
+
sandboxBackend: groupBackend,
|
|
660
|
+
sandboxOs: managedSessionGroupOs(session.sandboxBackend, session.sandboxOs),
|
|
655
661
|
},
|
|
656
662
|
},
|
|
657
663
|
readOnly,
|
|
@@ -659,7 +665,7 @@ async function withChannelAOperation<T>(
|
|
|
659
665
|
);
|
|
660
666
|
}
|
|
661
667
|
throw new HTTPException(409, {
|
|
662
|
-
message: "machine-home session has no active Connected Machine",
|
|
668
|
+
message: "machine-home session has no active Connected Machine or managed sandbox",
|
|
663
669
|
});
|
|
664
670
|
}
|
|
665
671
|
const sandbox = await getSandbox(
|
package/src/sandbox/machines.ts
CHANGED
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
// heartbeat), projected to the contract's MetricSample;
|
|
10
10
|
// * sharedSessionCount — the lease refcount (how many sessions share this one
|
|
11
11
|
// whole machine, the maxSandboxes:1 disclosure).
|
|
12
|
-
// PLUS, when a session context is supplied, the session's synthetic
|
|
13
|
-
// box (isSessionGroup:true) + the active-sandbox pointer
|
|
12
|
+
// PLUS, when a session context is supplied, the session's synthetic managed
|
|
13
|
+
// group box (isSessionGroup:true) + the active-sandbox pointer
|
|
14
|
+
// (activeSandboxId/Epoch).
|
|
14
15
|
//
|
|
15
16
|
// This is workspace-scoped (perm enrollments:read) and flag-gated upstream
|
|
16
17
|
// (sandboxSelfhostedEnabled). It deliberately does NOT depend on a FleetContext
|
|
@@ -32,6 +33,7 @@ import {
|
|
|
32
33
|
type MachineMetricsRow,
|
|
33
34
|
} from "@opengeni/db";
|
|
34
35
|
import { MachineView, MetricSample, type MachinesResponse } from "@opengeni/contracts";
|
|
36
|
+
import { managedSessionGroupBackend } from "@opengeni/core";
|
|
35
37
|
import { selfhostedHeartbeatLiveness } from "@opengeni/runtime/sandbox";
|
|
36
38
|
|
|
37
39
|
export type MachinesServices = {
|
|
@@ -244,10 +246,15 @@ export async function listMachines(
|
|
|
244
246
|
}
|
|
245
247
|
|
|
246
248
|
const machines: MachineView[] = [];
|
|
249
|
+
const groupBackend = session
|
|
250
|
+
? managedSessionGroupBackend(services.settings.sandboxBackend, session.sandboxBackend)
|
|
251
|
+
: null;
|
|
247
252
|
|
|
248
|
-
// The session's own group box (synthetic): the
|
|
249
|
-
//
|
|
250
|
-
|
|
253
|
+
// The session's own managed group box (synthetic): the sandbox a null active
|
|
254
|
+
// pointer routes to. A machine-home session uses the deployment's managed
|
|
255
|
+
// backend only after an explicit switch; none/selfhosted-only deployments do
|
|
256
|
+
// not invent a group they cannot establish.
|
|
257
|
+
if (session && groupBackend) {
|
|
251
258
|
const groupActive = activeSandboxId === null;
|
|
252
259
|
const groupLease = await readLease(db, workspaceId, session.sandboxGroupId);
|
|
253
260
|
machines.push(
|
|
@@ -255,7 +262,7 @@ export async function listMachines(
|
|
|
255
262
|
sandboxId: session.sandboxGroupId,
|
|
256
263
|
enrollmentId: null,
|
|
257
264
|
name: "session sandbox",
|
|
258
|
-
kind:
|
|
265
|
+
kind: groupBackend === "opensandbox" ? "opensandbox" : "modal",
|
|
259
266
|
state: "online",
|
|
260
267
|
active: groupActive,
|
|
261
268
|
isSessionGroup: true,
|
|
@@ -28,7 +28,11 @@
|
|
|
28
28
|
// Both consumers are BEST-EFFORT and fail-soft: a decode/DB error for one message
|
|
29
29
|
// is logged + swallowed (the bus subscription already swallows handler throws) so
|
|
30
30
|
// a metrics blip / a display-refresh write failure never tears down the consumer,
|
|
31
|
-
// back-pressures the agent, or breaks its connect.
|
|
31
|
+
// back-pressures the agent, or breaks its connect. Event ingestion drains the NATS
|
|
32
|
+
// subscription immediately into exact-subject queues: different runner connections
|
|
33
|
+
// progress concurrently, one runner's ordering is preserved, and consecutive queued
|
|
34
|
+
// heartbeats collapse latest-wins. A delayed heartbeat backlog therefore cannot keep
|
|
35
|
+
// renewing a dead runner's short connection lease one stale message at a time.
|
|
32
36
|
|
|
33
37
|
import {
|
|
34
38
|
clearEnrollmentWentOffline,
|
|
@@ -74,6 +78,141 @@ export const AGENT_EVENTS_SUBJECT = "agent.*.*.connection.*.events";
|
|
|
74
78
|
/** The wildcard subject the agent publishes its connect Hello on. */
|
|
75
79
|
export const AGENT_HELLO_SUBJECT = "agent.*.*.connection.*.hello";
|
|
76
80
|
|
|
81
|
+
type QueuedAgentEvent = {
|
|
82
|
+
payload: Uint8Array;
|
|
83
|
+
subject: string;
|
|
84
|
+
heartbeat: boolean;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
type AgentEventQueueState = {
|
|
88
|
+
scheduled: boolean;
|
|
89
|
+
pending: QueuedAgentEvent[];
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
type ScheduledAgentEventQueue = {
|
|
93
|
+
subject: string;
|
|
94
|
+
state: AgentEventQueueState;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export type AgentEventIngestionScheduler = {
|
|
98
|
+
enqueue: (payload: Uint8Array, subject: string) => void;
|
|
99
|
+
whenIdle: () => Promise<void>;
|
|
100
|
+
close: () => void;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
function payloadIsHeartbeat(payload: Uint8Array): boolean {
|
|
104
|
+
try {
|
|
105
|
+
return AgentEvent.decode(payload).event?.$case === "heartbeat";
|
|
106
|
+
} catch {
|
|
107
|
+
// Preserve the normal decoder/logging path for malformed events. They are
|
|
108
|
+
// barriers rather than heartbeat-coalescing candidates.
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Drain agent events without making every Connected Machine wait behind one
|
|
115
|
+
* deployment-wide serial DB queue. Each exact process subject retains FIFO order;
|
|
116
|
+
* separate subjects drain concurrently. Consecutive pending heartbeats for one
|
|
117
|
+
* subject are latest-wins because they are point-in-time liveness/metrics samples,
|
|
118
|
+
* while GoingOffline and update-progress events remain ordered barriers.
|
|
119
|
+
*/
|
|
120
|
+
export function createAgentEventIngestionScheduler(
|
|
121
|
+
handler: (payload: Uint8Array, subject: string) => void | Promise<void>,
|
|
122
|
+
options: { maxConcurrentSubjects?: number; onHeartbeatCoalesced?: () => void } = {},
|
|
123
|
+
): AgentEventIngestionScheduler {
|
|
124
|
+
const maxConcurrentSubjects = options.maxConcurrentSubjects ?? 32;
|
|
125
|
+
if (!Number.isSafeInteger(maxConcurrentSubjects) || maxConcurrentSubjects <= 0) {
|
|
126
|
+
throw new RangeError("maxConcurrentSubjects must be a positive safe integer");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const states = new Map<string, AgentEventQueueState>();
|
|
130
|
+
const readySubjects: ScheduledAgentEventQueue[] = [];
|
|
131
|
+
const idleWaiters = new Set<() => void>();
|
|
132
|
+
let accepting = true;
|
|
133
|
+
let activeSubjects = 0;
|
|
134
|
+
|
|
135
|
+
const resolveIdleWaiters = (): void => {
|
|
136
|
+
if (states.size !== 0) return;
|
|
137
|
+
for (const resolve of idleWaiters) resolve();
|
|
138
|
+
idleWaiters.clear();
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
async function drain(subject: string, state: AgentEventQueueState): Promise<void> {
|
|
142
|
+
while (true) {
|
|
143
|
+
const event = state.pending.shift();
|
|
144
|
+
if (!event) break;
|
|
145
|
+
try {
|
|
146
|
+
await handler(event.payload, event.subject);
|
|
147
|
+
} catch {
|
|
148
|
+
// Agent-event ingestion is best-effort. One bad event must not strand the
|
|
149
|
+
// exact-subject queue or prevent later heartbeats from restoring liveness.
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
activeSubjects -= 1;
|
|
154
|
+
state.scheduled = false;
|
|
155
|
+
if (state.pending.length > 0) {
|
|
156
|
+
schedule(subject, state);
|
|
157
|
+
} else {
|
|
158
|
+
if (states.get(subject) === state) states.delete(subject);
|
|
159
|
+
resolveIdleWaiters();
|
|
160
|
+
}
|
|
161
|
+
pump();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function pump(): void {
|
|
165
|
+
while (activeSubjects < maxConcurrentSubjects) {
|
|
166
|
+
const next = readySubjects.shift();
|
|
167
|
+
if (!next) return;
|
|
168
|
+
if (states.get(next.subject) !== next.state || !next.state.scheduled) continue;
|
|
169
|
+
activeSubjects += 1;
|
|
170
|
+
void drain(next.subject, next.state);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function schedule(subject: string, state: AgentEventQueueState): void {
|
|
175
|
+
if (state.scheduled) return;
|
|
176
|
+
state.scheduled = true;
|
|
177
|
+
readySubjects.push({ subject, state });
|
|
178
|
+
pump();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
enqueue(payload, subject) {
|
|
183
|
+
if (!accepting) return;
|
|
184
|
+
const heartbeat = payloadIsHeartbeat(payload);
|
|
185
|
+
let state = states.get(subject);
|
|
186
|
+
if (!state) {
|
|
187
|
+
state = { scheduled: false, pending: [] };
|
|
188
|
+
states.set(subject, state);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const queued = { payload, subject, heartbeat };
|
|
192
|
+
const lastIndex = state.pending.length - 1;
|
|
193
|
+
if (heartbeat && lastIndex >= 0 && state.pending[lastIndex]!.heartbeat) {
|
|
194
|
+
state.pending[lastIndex] = queued;
|
|
195
|
+
options.onHeartbeatCoalesced?.();
|
|
196
|
+
} else {
|
|
197
|
+
state.pending.push(queued);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
schedule(subject, state);
|
|
201
|
+
},
|
|
202
|
+
async whenIdle() {
|
|
203
|
+
if (states.size === 0) return;
|
|
204
|
+
await new Promise<void>((resolve) => idleWaiters.add(resolve));
|
|
205
|
+
},
|
|
206
|
+
close() {
|
|
207
|
+
accepting = false;
|
|
208
|
+
for (const state of states.values()) {
|
|
209
|
+
state.pending.length = 0;
|
|
210
|
+
}
|
|
211
|
+
resolveIdleWaiters();
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
77
216
|
/**
|
|
78
217
|
* Parse `agent.<ws>.<id>.connection.<instance>.<tail>` into its exact authority,
|
|
79
218
|
* expected tail token. Returns null for a subject that does not match the shape
|
|
@@ -521,9 +660,24 @@ export function startMetricsIngestion(deps: {
|
|
|
521
660
|
bus: EventBus;
|
|
522
661
|
observability?: Observability;
|
|
523
662
|
}): () => void {
|
|
524
|
-
|
|
525
|
-
|
|
663
|
+
const scheduler = createAgentEventIngestionScheduler(
|
|
664
|
+
(payload, subject) =>
|
|
665
|
+
handleAgentEventPayload(deps.db, deps.observability, payload, subject, deps.bus),
|
|
666
|
+
{
|
|
667
|
+
onHeartbeatCoalesced: () =>
|
|
668
|
+
deps.observability?.incrementCounter({
|
|
669
|
+
name: "opengeni_machine_heartbeat_coalesced_total",
|
|
670
|
+
help: "Connected Machine heartbeats collapsed while an exact runner event queue was busy.",
|
|
671
|
+
}),
|
|
672
|
+
},
|
|
526
673
|
);
|
|
674
|
+
const unsubscribe = deps.bus.subscribeAgentEvents(AGENT_EVENTS_SUBJECT, (payload, subject) => {
|
|
675
|
+
scheduler.enqueue(payload, subject);
|
|
676
|
+
});
|
|
677
|
+
return () => {
|
|
678
|
+
unsubscribe();
|
|
679
|
+
scheduler.close();
|
|
680
|
+
};
|
|
527
681
|
}
|
|
528
682
|
|
|
529
683
|
// ── Connect-Hello display refresh ─────────────────────────────────────────────
|
package/src/sandbox/viewer.ts
CHANGED
|
@@ -81,6 +81,8 @@ import {
|
|
|
81
81
|
type NatsRequestConnection,
|
|
82
82
|
} from "@opengeni/runtime/sandbox";
|
|
83
83
|
import {
|
|
84
|
+
managedSessionGroupBackend,
|
|
85
|
+
managedSessionGroupOs,
|
|
84
86
|
providerSettingsForSessionSandboxRuntime,
|
|
85
87
|
relayConfigFromSettings,
|
|
86
88
|
resolveSessionSandboxRuntime,
|
|
@@ -246,7 +248,24 @@ export async function attachViewer(
|
|
|
246
248
|
},
|
|
247
249
|
): Promise<ViewerAttachResult> {
|
|
248
250
|
const { db, settings } = services;
|
|
249
|
-
const { accountId, workspaceId
|
|
251
|
+
const { accountId, workspaceId } = input;
|
|
252
|
+
const groupBackend = managedSessionGroupBackend(
|
|
253
|
+
settings.sandboxBackend,
|
|
254
|
+
input.session.sandboxBackend,
|
|
255
|
+
);
|
|
256
|
+
if (!groupBackend) {
|
|
257
|
+
throw new HTTPException(409, {
|
|
258
|
+
message: "session has no managed sandbox group to attach",
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
const session =
|
|
262
|
+
groupBackend === input.session.sandboxBackend
|
|
263
|
+
? input.session
|
|
264
|
+
: {
|
|
265
|
+
...input.session,
|
|
266
|
+
sandboxBackend: groupBackend,
|
|
267
|
+
sandboxOs: managedSessionGroupOs(input.session.sandboxBackend, input.session.sandboxOs),
|
|
268
|
+
};
|
|
250
269
|
const viewerId = input.viewerId ?? crypto.randomUUID();
|
|
251
270
|
const attachSubjectId = claimableSubjectId(input.viewerSubjectId ?? null);
|
|
252
271
|
const attachAuthorityEpoch = attachSubjectId
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { WorkspaceDeleteObservation, WorkspaceDeleteObserver } from "@opengeni/db";
|
|
2
|
+
import type { Observability } from "@opengeni/observability";
|
|
3
|
+
|
|
4
|
+
const DURATION_BUCKETS = [
|
|
5
|
+
0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30, 60, 120, 300, 600, 1_200, 2_400,
|
|
6
|
+
3_600,
|
|
7
|
+
];
|
|
8
|
+
const INVENTORY_BUCKETS = [0, 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1_000, 5_000, 10_000];
|
|
9
|
+
|
|
10
|
+
function recordWorkspaceDeleteObservation(
|
|
11
|
+
observability: Observability,
|
|
12
|
+
identity: { accountId: string; workspaceId: string },
|
|
13
|
+
observation: WorkspaceDeleteObservation,
|
|
14
|
+
): void {
|
|
15
|
+
const labels = { phase: observation.phase, outcome: observation.outcome };
|
|
16
|
+
observability.observeHistogram({
|
|
17
|
+
name: "opengeni_workspace_delete_phase_seconds",
|
|
18
|
+
help: "Workspace deletion transaction and bounded internal phase duration in seconds.",
|
|
19
|
+
labels,
|
|
20
|
+
buckets: DURATION_BUCKETS,
|
|
21
|
+
value: Math.max(0, observation.durationSeconds),
|
|
22
|
+
});
|
|
23
|
+
if (observation.phase === "transaction") {
|
|
24
|
+
observability.incrementCounter({
|
|
25
|
+
name: "opengeni_workspace_delete_attempts_total",
|
|
26
|
+
help: "Workspace deletion attempts by terminal database outcome.",
|
|
27
|
+
labels: { outcome: observation.outcome },
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
for (const [kind, count] of Object.entries(observation.inventory ?? {})) {
|
|
31
|
+
observability.observeHistogram({
|
|
32
|
+
name: "opengeni_workspace_delete_inventory_rows",
|
|
33
|
+
help: "Rows or live owners observed by workspace deletion preflight inventory class.",
|
|
34
|
+
labels: { kind },
|
|
35
|
+
buckets: INVENTORY_BUCKETS,
|
|
36
|
+
value: Math.max(0, Math.floor(count ?? 0)),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
observability.info("Workspace deletion phase", {
|
|
40
|
+
accountId: identity.accountId,
|
|
41
|
+
workspaceId: identity.workspaceId,
|
|
42
|
+
phase: observation.phase,
|
|
43
|
+
outcome: observation.outcome,
|
|
44
|
+
durationSeconds: observation.durationSeconds,
|
|
45
|
+
inventoryJson: JSON.stringify(observation.inventory ?? {}),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Retain exact workspace deletion timing and inventory without putting tenant
|
|
51
|
+
* identifiers in metric labels. Observer failures never change deletion truth.
|
|
52
|
+
*/
|
|
53
|
+
export function workspaceDeleteObserver(
|
|
54
|
+
observability: Observability | null | undefined,
|
|
55
|
+
identity: { accountId: string; workspaceId: string },
|
|
56
|
+
): WorkspaceDeleteObserver | undefined {
|
|
57
|
+
if (!observability) return undefined;
|
|
58
|
+
return {
|
|
59
|
+
onPhase: (observation) => {
|
|
60
|
+
try {
|
|
61
|
+
recordWorkspaceDeleteObservation(observability, identity, observation);
|
|
62
|
+
} catch {
|
|
63
|
+
try {
|
|
64
|
+
observability.incrementCounter({
|
|
65
|
+
name: "opengeni_observability_observer_errors_total",
|
|
66
|
+
help: "Observability observer failures isolated from product execution.",
|
|
67
|
+
labels: { observer: "workspace_delete" },
|
|
68
|
+
});
|
|
69
|
+
} catch {
|
|
70
|
+
// The metrics registry itself is unhealthy. Deletion remains authoritative.
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|