@opengeni/worker-bundle 2.3.1 → 2.3.2-canary.37090756480001

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 (66) hide show
  1. package/dist/activities/agent-turn/claude-usage-observer.d.ts +30 -6
  2. package/dist/activities/agent-turn/credential-leases.d.ts +5 -32
  3. package/dist/activities/agent-turn/errors.d.ts +3 -0
  4. package/dist/activities/agent-turn/finalization-monitor.d.ts +1 -1
  5. package/dist/activities/agent-turn/finalization.d.ts +2 -0
  6. package/dist/activities/agent-turn/quiescence.d.ts +3 -4
  7. package/dist/activities/agent-turn/subscription-lease.d.ts +38 -0
  8. package/dist/activities/agent-turn/turn-context.d.ts +1 -1
  9. package/dist/activities/model-call-fact-reconciler.d.ts +34 -0
  10. package/dist/activities/session-state.d.ts +2 -2
  11. package/dist/activities/types.d.ts +7 -0
  12. package/dist/{activities-control-3HAH4FMG.js → activities-control-3BY4IJKL.js} +166 -35
  13. package/dist/activities-control-3BY4IJKL.js.map +1 -0
  14. package/dist/activities-control.d.ts +6 -4
  15. package/dist/{activities-turn-KU25J3VQ.js → activities-turn-TTNAVGKN.js} +263 -308
  16. package/dist/activities-turn-TTNAVGKN.js.map +1 -0
  17. package/dist/activities.d.ts +6 -4
  18. package/dist/{chunk-3K3ULWEE.js → chunk-PA7Z55P4.js} +242 -17
  19. package/dist/chunk-PA7Z55P4.js.map +1 -0
  20. package/dist/{chunk-YG27NEMX.js → chunk-YOI3Z3GF.js} +9 -1
  21. package/dist/chunk-YOI3Z3GF.js.map +1 -0
  22. package/dist/http.d.ts +1 -1
  23. package/dist/index.d.ts +6 -2
  24. package/dist/index.js +82 -16
  25. package/dist/index.js.map +1 -1
  26. package/dist/observability-metrics.d.ts +6 -1
  27. package/dist/sandbox-resume.d.ts +3 -0
  28. package/dist/session-event-fanout.d.ts +6 -0
  29. package/dist/worker-service-lifecycle.d.ts +12 -0
  30. package/dist/workflow-bundle.js +3 -2
  31. package/package.json +21 -21
  32. package/src/activities/agent-turn/claim.ts +13 -0
  33. package/src/activities/agent-turn/claude-usage-observer.ts +99 -8
  34. package/src/activities/agent-turn/credential-leases.ts +86 -213
  35. package/src/activities/agent-turn/errors.ts +61 -1
  36. package/src/activities/agent-turn/failure-settlement.ts +59 -5
  37. package/src/activities/agent-turn/finalization-monitor.ts +3 -4
  38. package/src/activities/agent-turn/finalization.ts +8 -3
  39. package/src/activities/agent-turn/quiescence.ts +3 -4
  40. package/src/activities/agent-turn/run.ts +24 -20
  41. package/src/activities/agent-turn/sandbox-establish.ts +2 -0
  42. package/src/activities/agent-turn/sandbox-runtime.ts +5 -1
  43. package/src/activities/agent-turn/skill-selection.ts +2 -0
  44. package/src/activities/agent-turn/subscription-lease.ts +115 -0
  45. package/src/activities/agent-turn/turn-context.ts +1 -4
  46. package/src/activities/codex-rotation.ts +2 -12
  47. package/src/activities/model-call-fact-reconciler.ts +112 -0
  48. package/src/activities/sandbox-lease.ts +59 -8
  49. package/src/activities/session-state.ts +29 -4
  50. package/src/activities/types.ts +7 -0
  51. package/src/activities-control.ts +2 -0
  52. package/src/activities-turn.ts +5 -0
  53. package/src/activities.ts +7 -0
  54. package/src/http.ts +4 -2
  55. package/src/index.ts +39 -9
  56. package/src/observability-metrics.ts +27 -0
  57. package/src/sandbox-resume.ts +9 -0
  58. package/src/sandbox-routing.ts +77 -0
  59. package/src/session-event-fanout.ts +23 -0
  60. package/src/worker-service-lifecycle.ts +53 -0
  61. package/src/workflows/file-upload-reaper.ts +2 -0
  62. package/src/workflows/session.ts +6 -1
  63. package/dist/activities-control-3HAH4FMG.js.map +0 -1
  64. package/dist/activities-turn-KU25J3VQ.js.map +0 -1
  65. package/dist/chunk-3K3ULWEE.js.map +0 -1
  66. package/dist/chunk-YG27NEMX.js.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/worker-bundle",
3
- "version": "2.3.1",
3
+ "version": "2.3.2-canary.37090756480001",
4
4
  "description": "OpenGeni worker entry and reusable embedded lifecycle, shipped with a release-coherent pre-bundled Temporal workflow artifact.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -54,26 +54,26 @@
54
54
  },
55
55
  "dependencies": {
56
56
  "@llamaindex/liteparse": "2.14.2",
57
- "@opengeni/agent-proto": "^0.6.1",
58
- "@opengeni/capabilities": "^0.3.5",
59
- "@opengeni/codemode": "^0.6.8",
60
- "@opengeni/codex": "^0.2.31",
61
- "@opengeni/config": "^3.2.1",
62
- "@opengeni/contracts": "^5.6.0",
63
- "@opengeni/core": "^5.1.1",
64
- "@opengeni/db": "^6.5.0",
65
- "@opengeni/documents": "^0.8.40",
66
- "@opengeni/events": "^0.4.38",
67
- "@opengeni/github": "^0.8.2",
68
- "@opengeni/interaction": "^0.4.45",
69
- "@opengeni/jev": "^0.2.1",
70
- "@opengeni/network": "^0.3.1",
71
- "@opengeni/observability": "^0.8.37",
72
- "@opengeni/runtime": "^4.6.0",
73
- "@opengeni/sdk": "^7.6.1",
74
- "@opengeni/storage": "^0.2.138",
75
- "@opengeni/tool-gateway": "^0.1.18",
76
- "@opengeni/xai-subscription": "^0.1.5",
57
+ "@opengeni/agent-proto": "^0.6.1-canary.37090756480001",
58
+ "@opengeni/capabilities": "^0.3.6-canary.37090756480001",
59
+ "@opengeni/codemode": "^0.7.0-canary.37090756480001",
60
+ "@opengeni/codex": "^0.2.32-canary.37090756480001",
61
+ "@opengeni/config": "^3.3.0-canary.37090756480001",
62
+ "@opengeni/contracts": "^5.7.0-canary.37090756480001",
63
+ "@opengeni/core": "^5.2.0-canary.37090756480001",
64
+ "@opengeni/db": "^6.6.0-canary.37090756480001",
65
+ "@opengeni/documents": "^0.8.41-canary.37090756480001",
66
+ "@opengeni/events": "^0.4.39-canary.37090756480001",
67
+ "@opengeni/github": "^0.8.3-canary.37090756480001",
68
+ "@opengeni/interaction": "^0.4.46-canary.37090756480001",
69
+ "@opengeni/jev": "^0.2.1-canary.37090756480001",
70
+ "@opengeni/network": "^0.3.1-canary.37090756480001",
71
+ "@opengeni/observability": "^0.8.38-canary.37090756480001",
72
+ "@opengeni/runtime": "^4.7.0-canary.37090756480001",
73
+ "@opengeni/sdk": "^7.7.0-canary.37090756480001",
74
+ "@opengeni/storage": "^0.2.139-canary.37090756480001",
75
+ "@opengeni/tool-gateway": "^0.1.19-canary.37090756480001",
76
+ "@opengeni/xai-subscription": "^0.1.5-canary.37090756480001",
77
77
  "@temporalio/activity": "^1.17.0",
78
78
  "@temporalio/client": "^1.17.0",
79
79
  "@temporalio/worker": "^1.17.0",
@@ -1,3 +1,5 @@
1
+ import { withDirectModelProviders } from "@opengeni/config";
2
+ import { loadDirectModelProviderConnection } from "@opengeni/db";
1
3
  import { FILESYSTEM_DISCONTINUITY_PROTOCOL } from "./recovery-warning";
2
4
  import {
3
5
  applySessionTurnSettlement,
@@ -288,6 +290,17 @@ export async function claimTurnAttempt(deps: ClaimTurnDeps): Promise<ClaimTurnOu
288
290
  workspaceProviderSettings,
289
291
  claimedPolicy.kind === "valid" ? claimedPolicy.policy.productModelId : turn.model,
290
292
  );
293
+ const selectedDirectConnection = await loadDirectModelProviderConnection(
294
+ db,
295
+ capabilitySettings,
296
+ input.workspaceId,
297
+ claimedPolicy.kind === "valid" ? claimedPolicy.policy.productModelId : (turn.model ?? ""),
298
+ );
299
+ // Execution only needs the selected customer connection. Ordinary turns
300
+ // must not load or install unrelated workspace provider configurations.
301
+ if (selectedDirectConnection) {
302
+ capabilitySettings = withDirectModelProviders(capabilitySettings, [selectedDirectConnection]);
303
+ }
291
304
  const codexAppsCredentialId = capabilitySettings.codexConnectedAppsEnabled
292
305
  ? await resolveCodexAppsCredentialIdForRun(db, input.workspaceId)
293
306
  : null;
@@ -1,3 +1,4 @@
1
+ import { createHash } from "node:crypto";
1
2
  import {
2
3
  emptyClaudeUsage,
3
4
  mergeClaudeUsage,
@@ -7,7 +8,22 @@ import {
7
8
  } from "@opengeni/config";
8
9
 
9
10
  type Scope = "workspace" | "organization";
11
+ type ClaudeRequestCredential = {
12
+ token: string;
13
+ connectionId: string;
14
+ credentialVersion: number;
15
+ };
16
+ export class ClaudeSubscriptionConnectionUnavailable extends Error {
17
+ readonly status = 409;
18
+ readonly code = "claude_subscription_connection_changed";
19
+ constructor() {
20
+ super(
21
+ "Claude connection changed or was disconnected. Start a new turn with the current connection.",
22
+ );
23
+ }
24
+ }
10
25
  export type CapturedClaudeUsage = {
26
+ scope: Scope;
11
27
  token: string;
12
28
  expectedConnectionId: string;
13
29
  expectedCredentialVersion: number;
@@ -18,13 +34,22 @@ export type CapturedClaudeUsage = {
18
34
  /** Capture the exact credential before requests; replacement fences late responses. */
19
35
  export async function createClaudeUsageObserver(
20
36
  providers: ReturnType<typeof parseModelProvidersJson>,
21
- latest: Map<Scope, CapturedClaudeUsage>,
37
+ latest: Map<string, CapturedClaudeUsage>,
22
38
  readCredential: (scope: Scope) => Promise<{
23
39
  token: string;
24
40
  connectionId: string;
25
41
  credentialVersion: number;
26
42
  } | null>,
27
43
  ) {
44
+ const managedProviderIds = new Set(
45
+ providers
46
+ .filter(
47
+ (provider) =>
48
+ provider.kind === "claude-subscription-workspace" ||
49
+ provider.kind === "claude-subscription-organization",
50
+ )
51
+ .map((provider) => provider.id),
52
+ );
28
53
  const bindings = await Promise.all(
29
54
  providers
30
55
  .filter(
@@ -60,12 +85,27 @@ export async function createClaudeUsageObserver(
60
85
  }),
61
86
  );
62
87
  const captured = new Map(bindings.filter((binding) => binding !== null));
63
- const observe = (providerId: string, response: Response) => {
64
- const binding = captured.get(providerId);
88
+ const observeBinding = (
89
+ binding:
90
+ | Pick<
91
+ CapturedClaudeUsage,
92
+ "scope" | "token" | "expectedConnectionId" | "expectedCredentialVersion"
93
+ >
94
+ | undefined,
95
+ response: Response,
96
+ upstreamModelId?: string,
97
+ requestToken?: string | null,
98
+ ) => {
99
+ if (requestToken === null) return;
65
100
  if (!binding) return;
66
- const { scope, ...identity } = binding;
67
- const previous = latest.get(scope);
68
- let observation = parseClaudeUsageHeaders(response.headers);
101
+ const { scope, ...capturedIdentity } = binding;
102
+ const identity = { ...capturedIdentity, token: requestToken ?? capturedIdentity.token };
103
+ // Same-generation OAuth renewal can overlap an older request. Never attach
104
+ // its authentication failure to the newly renewed token or merge the two.
105
+ const tokenKey = createHash("sha256").update(identity.token).digest("hex");
106
+ const captureKey = `${scope}:${identity.expectedConnectionId}:${identity.expectedCredentialVersion}:${tokenKey}`;
107
+ const previous = latest.get(captureKey);
108
+ let observation = parseClaudeUsageHeaders(response.headers, new Date(), upstreamModelId);
69
109
  if (observation && previous?.observation) {
70
110
  const merged = mergeClaudeUsage(
71
111
  mergeClaudeUsage(emptyClaudeUsage(binding.expectedCredentialVersion), previous.observation),
@@ -75,10 +115,13 @@ export async function createClaudeUsageObserver(
75
115
  windows: merged.windows,
76
116
  observedAt: merged.observedAt!,
77
117
  source: merged.source!,
118
+ requestStatus: merged.requestStatus ?? null,
119
+ requestRestrictions: merged.requestRestrictions ?? [],
78
120
  };
79
121
  }
80
122
  if (observation || response.status === 401)
81
- latest.set(scope, {
123
+ latest.set(captureKey, {
124
+ scope,
82
125
  ...identity,
83
126
  ...(previous?.observation && !observation ? { observation: previous.observation } : {}),
84
127
  ...(observation ? { observation } : {}),
@@ -87,9 +130,57 @@ export async function createClaudeUsageObserver(
87
130
  : {}),
88
131
  });
89
132
  };
133
+ const observe = (
134
+ providerId: string,
135
+ response: Response,
136
+ upstreamModelId?: string,
137
+ requestToken?: string | null,
138
+ ) => observeBinding(captured.get(providerId), response, upstreamModelId, requestToken);
139
+ const prepareRequestWithObserver = async (
140
+ providerId: string,
141
+ headers: Headers,
142
+ resolve: (binding: {
143
+ scope: Scope;
144
+ expectedConnectionId: string;
145
+ expectedCredentialVersion: number;
146
+ }) => Promise<ClaudeRequestCredential | null>,
147
+ ) => {
148
+ if (!managedProviderIds.has(providerId)) return { headers, observe };
149
+ const binding = captured.get(providerId);
150
+ if (!binding) throw new ClaudeSubscriptionConnectionUnavailable();
151
+ const credential = await resolve({
152
+ scope: binding.scope,
153
+ expectedConnectionId: binding.expectedConnectionId,
154
+ expectedCredentialVersion: binding.expectedCredentialVersion,
155
+ });
156
+ if (
157
+ !credential ||
158
+ credential.connectionId !== binding.expectedConnectionId ||
159
+ credential.credentialVersion !== binding.expectedCredentialVersion
160
+ )
161
+ throw new ClaudeSubscriptionConnectionUnavailable();
162
+ const dispatched = Object.freeze({ ...binding, token: credential.token });
163
+ captured.set(providerId, dispatched);
164
+ headers.set("authorization", `Bearer ${credential.token}`);
165
+ headers.delete("x-api-key");
166
+ return {
167
+ headers,
168
+ observe: (
169
+ _providerId: string,
170
+ response: Response,
171
+ upstreamModelId?: string,
172
+ requestToken?: string | null,
173
+ ) => observeBinding(dispatched, response, upstreamModelId, requestToken),
174
+ };
175
+ };
90
176
  return Object.assign(observe, {
177
+ prepareRequestWithObserver,
178
+ async prepareRequest(...args: Parameters<typeof prepareRequestWithObserver>) {
179
+ return (await prepareRequestWithObserver(...args)).headers;
180
+ },
91
181
  binding(providerId: string) {
92
- return captured.get(providerId);
182
+ const binding = captured.get(providerId);
183
+ return binding ? { ...binding } : undefined;
93
184
  },
94
185
  renew(
95
186
  providerId: string,
@@ -5,12 +5,14 @@ import {
5
5
  heartbeatXaiCredentialLeaseUntil,
6
6
  } from "@opengeni/db";
7
7
  import type { SharedActivityServices } from "../types";
8
- import { codexCredentialLeaseDeadlineExpired } from "./codex";
8
+ import {
9
+ SubscriptionTurnLease,
10
+ type LeaseRenewReason,
11
+ type LeaseLossReason,
12
+ } from "./subscription-lease";
13
+ export type { LeaseRenewReason, LeaseLossReason } from "./subscription-lease";
9
14
  import { safeErrorDiagnostic } from "./errors";
10
15
 
11
- export type LeaseRenewReason = "timer" | "runtime_event" | "model_usage";
12
- export type LeaseLossReason = "deadline" | "not_found";
13
-
14
16
  export class CodexCredentialLeaseLostError extends Error {
15
17
  readonly code = "codex_credential_lease_lost";
16
18
 
@@ -34,223 +36,94 @@ export type TurnCredentialLeaseDeps = {
34
36
  * cross-replica fairness primitive; the heartbeat here only extends its short
35
37
  * TTL. A killed worker stops heartbeating and the holder self-expires.
36
38
  */
37
- export class CodexTurnLease {
38
- held = false;
39
- lost = false;
40
- lossReason: LeaseLossReason | null = null;
41
- holderId: string | null = null;
42
- generation: number | null = null;
43
- // Monotonic worker deadline, not a comparison between the Postgres and
44
- // worker wall clocks. It is advanced only after a database renewal confirms,
45
- // from the request START + TTL; slow queries therefore shorten (never extend)
46
- // the conservative ownership window.
47
- confirmedUntilMs: number | null = null;
48
- private heartbeatInFlight = false;
49
- private heartbeatTimer: ReturnType<typeof setInterval> | undefined;
50
-
51
- constructor(private readonly deps: TurnCredentialLeaseDeps) {}
52
-
53
- markLost = (reason: LeaseLossReason): void => {
54
- if (this.lost) return;
55
- this.lost = true;
56
- this.lossReason = reason;
57
- this.deps.observability.incrementCounter({
58
- name: "opengeni_codex_lease_renewals_total",
59
- help: "Codex lease renewal checkpoints by outcome and reason.",
60
- labels: { workspace_key: this.deps.codexWorkspaceKey, outcome: "lost", reason },
61
- });
62
- this.deps.observability.warn("Codex credential lease was lost during an active turn", {
63
- workspaceId: this.deps.workspaceId,
64
- turnId: this.deps.getTurnId(),
65
- reason,
66
- });
67
- };
68
-
69
- /** Fail closed at the last worker-confirmed lease deadline before dispatch. */
70
- assertUsable = (): void => {
71
- if (this.lost) {
72
- throw new CodexCredentialLeaseLostError(this.lossReason ?? "not_found");
73
- }
74
- if (
75
- !this.held ||
76
- this.holderId === null ||
77
- this.generation === null ||
78
- codexCredentialLeaseDeadlineExpired(this.confirmedUntilMs)
79
- ) {
80
- this.markLost("deadline");
81
- throw new CodexCredentialLeaseLostError("deadline");
82
- }
83
- };
84
-
85
- renew = async (reason: LeaseRenewReason): Promise<void> => {
86
- const turnId = this.deps.getTurnId();
87
- if (!turnId || !this.held || !this.holderId || this.generation === null || this.lost) {
88
- return;
89
- }
90
- if (codexCredentialLeaseDeadlineExpired(this.confirmedUntilMs)) {
91
- this.markLost("deadline");
92
- return;
93
- }
94
- if (this.heartbeatInFlight) return;
95
- this.heartbeatInFlight = true;
96
- const priorConfirmedUntilMs = this.confirmedUntilMs;
97
- const renewalStartedAtMs = performance.now();
98
- try {
99
- const renewedUntil = await heartbeatCodexCredentialLeaseUntil(
100
- this.deps.db,
101
- this.deps.accountId,
102
- this.deps.workspaceId,
103
- turnId,
104
- this.holderId,
105
- this.generation,
106
- CODEX_CREDENTIAL_LEASE_TTL_MS,
107
- );
108
- if (!renewedUntil) {
109
- this.markLost("not_found");
110
- } else if (
111
- codexCredentialLeaseDeadlineExpired(priorConfirmedUntilMs) ||
112
- codexCredentialLeaseDeadlineExpired(renewalStartedAtMs + CODEX_CREDENTIAL_LEASE_TTL_MS)
113
- ) {
114
- // The database call may have waited on a lock until the prior lease
115
- // expired. A late successful UPDATE is not proof that this worker
116
- // owned the credential continuously; do not accept or extend it.
117
- this.markLost("deadline");
118
- } else {
119
- this.confirmedUntilMs = renewalStartedAtMs + CODEX_CREDENTIAL_LEASE_TTL_MS;
120
- this.deps.observability.incrementCounter({
39
+ export class CodexTurnLease extends SubscriptionTurnLease {
40
+ constructor(deps: TurnCredentialLeaseDeps) {
41
+ super({
42
+ ttlMs: CODEX_CREDENTIAL_LEASE_TTL_MS,
43
+ getTurnId: deps.getTurnId,
44
+ heartbeat: ({ turnId, holderId, generation }) =>
45
+ heartbeatCodexCredentialLeaseUntil(
46
+ deps.db,
47
+ deps.accountId,
48
+ deps.workspaceId,
49
+ turnId,
50
+ holderId,
51
+ generation,
52
+ CODEX_CREDENTIAL_LEASE_TTL_MS,
53
+ ),
54
+ lostError: (reason) => new CodexCredentialLeaseLostError(reason),
55
+ onLost: (reason) => {
56
+ deps.observability.incrementCounter({
121
57
  name: "opengeni_codex_lease_renewals_total",
122
58
  help: "Codex lease renewal checkpoints by outcome and reason.",
123
- labels: {
124
- workspace_key: this.deps.codexWorkspaceKey,
125
- outcome: "completed",
126
- reason,
127
- },
59
+ labels: { workspace_key: deps.codexWorkspaceKey, outcome: "lost", reason },
128
60
  });
129
- }
130
- } catch (error) {
131
- if (codexCredentialLeaseDeadlineExpired(this.confirmedUntilMs)) {
132
- this.markLost("deadline");
133
- return;
134
- }
135
- this.deps.observability.warn("Codex credential lease heartbeat failed", {
136
- workspaceId: this.deps.workspaceId,
137
- turnId,
138
- reason,
139
- ...safeErrorDiagnostic(error),
140
- });
141
- this.deps.observability.incrementCounter({
142
- name: "opengeni_codex_lease_renewals_total",
143
- help: "Codex lease renewal checkpoints by outcome and reason.",
144
- labels: {
145
- workspace_key: this.deps.codexWorkspaceKey,
146
- outcome: "error",
61
+ deps.observability.warn("Codex credential lease was lost during an active turn", {
62
+ workspaceId: deps.workspaceId,
63
+ turnId: deps.getTurnId(),
147
64
  reason,
148
- },
149
- });
150
- } finally {
151
- this.heartbeatInFlight = false;
152
- }
153
- };
154
-
155
- startHeartbeat = (): void => {
156
- if (!this.deps.getTurnId() || this.heartbeatTimer) return;
157
- this.heartbeatTimer = setInterval(() => {
158
- void this.renew("timer");
159
- }, 60_000);
160
- this.heartbeatTimer.unref?.();
161
- };
162
-
163
- stopHeartbeat = (): void => {
164
- if (!this.heartbeatTimer) return;
165
- clearInterval(this.heartbeatTimer);
166
- this.heartbeatTimer = undefined;
167
- };
65
+ });
66
+ },
67
+ onRenewed: (reason) =>
68
+ deps.observability.incrementCounter({
69
+ name: "opengeni_codex_lease_renewals_total",
70
+ help: "Codex lease renewal checkpoints by outcome and reason.",
71
+ labels: { workspace_key: deps.codexWorkspaceKey, outcome: "completed", reason },
72
+ }),
73
+ onError: (error, reason) => {
74
+ deps.observability.warn("Codex credential lease heartbeat failed", {
75
+ workspaceId: deps.workspaceId,
76
+ turnId: deps.getTurnId(),
77
+ reason,
78
+ ...safeErrorDiagnostic(error),
79
+ });
80
+ deps.observability.incrementCounter({
81
+ name: "opengeni_codex_lease_renewals_total",
82
+ help: "Codex lease renewal checkpoints by outcome and reason.",
83
+ labels: { workspace_key: deps.codexWorkspaceKey, outcome: "error", reason },
84
+ });
85
+ },
86
+ });
87
+ }
168
88
  }
169
89
 
170
- /** The xAI/SuperGrok credential holder for one running turn; same shape. */
171
- export class XaiTurnLease {
172
- held = false;
173
- lost = false;
90
+ /** SuperGrok uses the same deadline and holder fences as Codex. */
91
+ export class XaiTurnLease extends SubscriptionTurnLease {
174
92
  subjectId: string | null = null;
175
- holderId: string | null = null;
176
- generation: number | null = null;
177
- confirmedUntilMs: number | null = null;
178
- private heartbeatInFlight = false;
179
- private heartbeatTimer: ReturnType<typeof setInterval> | undefined;
180
-
181
- constructor(private readonly deps: TurnCredentialLeaseDeps) {}
182
-
183
- markLost = (reason: LeaseLossReason): void => {
184
- if (this.lost) return;
185
- this.lost = true;
186
- this.deps.observability.warn("xAI credential lease was lost during an active turn", {
187
- workspaceId: this.deps.workspaceId,
188
- turnId: this.deps.getTurnId(),
189
- reason,
93
+ constructor(deps: TurnCredentialLeaseDeps) {
94
+ super({
95
+ ttlMs: XAI_CREDENTIAL_LEASE_TTL_MS,
96
+ getTurnId: deps.getTurnId,
97
+ heartbeat: ({ turnId, holderId, generation }) =>
98
+ this.subjectId
99
+ ? heartbeatXaiCredentialLeaseUntil(deps.db, {
100
+ workspaceId: deps.workspaceId,
101
+ subjectId: this.subjectId,
102
+ turnId,
103
+ holderId,
104
+ generation,
105
+ leaseTtlMs: XAI_CREDENTIAL_LEASE_TTL_MS,
106
+ })
107
+ : Promise.resolve(null),
108
+ lostError: (reason) =>
109
+ Object.assign(new Error("SuperGrok credential lease is not usable for provider dispatch"), {
110
+ code: "xai_credential_lease_lost",
111
+ reason,
112
+ }),
113
+ onLost: (reason) =>
114
+ deps.observability.warn("xAI credential lease was lost during an active turn", {
115
+ workspaceId: deps.workspaceId,
116
+ turnId: deps.getTurnId(),
117
+ reason,
118
+ }),
119
+ onError: (error) =>
120
+ deps.observability.warn("xAI credential lease heartbeat failed", {
121
+ workspaceId: deps.workspaceId,
122
+ turnId: deps.getTurnId(),
123
+ ...safeErrorDiagnostic(error),
124
+ }),
190
125
  });
191
- };
192
-
193
- renew = async (): Promise<void> => {
194
- const turnId = this.deps.getTurnId();
195
- if (
196
- !turnId ||
197
- !this.held ||
198
- !this.subjectId ||
199
- !this.holderId ||
200
- this.generation === null ||
201
- this.lost
202
- ) {
203
- return;
204
- }
205
- if (codexCredentialLeaseDeadlineExpired(this.confirmedUntilMs)) {
206
- this.markLost("deadline");
207
- return;
208
- }
209
- if (this.heartbeatInFlight) return;
210
- this.heartbeatInFlight = true;
211
- const renewalStartedAtMs = performance.now();
212
- try {
213
- const renewedUntil = await heartbeatXaiCredentialLeaseUntil(this.deps.db, {
214
- workspaceId: this.deps.workspaceId,
215
- subjectId: this.subjectId,
216
- turnId,
217
- holderId: this.holderId,
218
- generation: this.generation,
219
- leaseTtlMs: XAI_CREDENTIAL_LEASE_TTL_MS,
220
- });
221
- if (!renewedUntil) {
222
- this.markLost("not_found");
223
- } else {
224
- this.confirmedUntilMs = renewalStartedAtMs + XAI_CREDENTIAL_LEASE_TTL_MS;
225
- }
226
- } catch (error) {
227
- if (codexCredentialLeaseDeadlineExpired(this.confirmedUntilMs)) {
228
- this.markLost("deadline");
229
- return;
230
- }
231
- this.deps.observability.warn("xAI credential lease heartbeat failed", {
232
- workspaceId: this.deps.workspaceId,
233
- turnId,
234
- ...safeErrorDiagnostic(error),
235
- });
236
- } finally {
237
- this.heartbeatInFlight = false;
238
- }
239
- };
240
-
241
- startHeartbeat = (): void => {
242
- if (!this.deps.getTurnId() || this.heartbeatTimer) return;
243
- this.heartbeatTimer = setInterval(() => {
244
- void this.renew();
245
- }, 60_000);
246
- this.heartbeatTimer.unref?.();
247
- };
248
-
249
- stopHeartbeat = (): void => {
250
- if (!this.heartbeatTimer) return;
251
- clearInterval(this.heartbeatTimer);
252
- this.heartbeatTimer = undefined;
253
- };
126
+ }
254
127
  }
255
128
 
256
129
  /** Both serving-credential leases for one turn attempt plus their composite views. */