@opengeni/worker-bundle 1.0.2 → 1.2.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.
Files changed (47) hide show
  1. package/dist/activities/agent-run-admission.d.ts +1 -14
  2. package/dist/activities/agent-turn/admission.d.ts +3 -2
  3. package/dist/activities/agent-turn/compaction-prep.d.ts +1 -0
  4. package/dist/activities/agent-turn/errors.d.ts +3 -0
  5. package/dist/activities/agent-turn/finalization-monitor.d.ts +6 -1
  6. package/dist/activities/agent-turn/model-usage.d.ts +10 -0
  7. package/dist/activities/agent-turn/provider-recovery-metrics.d.ts +18 -0
  8. package/dist/activities/agent-turn/turn-context.d.ts +5 -0
  9. package/dist/activities/goals.d.ts +1 -17
  10. package/dist/activities/types.d.ts +1 -1
  11. package/dist/{activities-control-BNPTRTOE.js → activities-control-QSDPXLLD.js} +26 -10
  12. package/dist/activities-control-QSDPXLLD.js.map +1 -0
  13. package/dist/{activities-turn-5LGZOSVU.js → activities-turn-BMY3CDKL.js} +191 -35
  14. package/dist/activities-turn-BMY3CDKL.js.map +1 -0
  15. package/dist/{chunk-LJX7Z2KP.js → chunk-JKMG5ZHH.js} +55 -167
  16. package/dist/chunk-JKMG5ZHH.js.map +1 -0
  17. package/dist/{chunk-3ZTRWDMN.js → chunk-XONQEVAD.js} +1 -1
  18. package/dist/chunk-XONQEVAD.js.map +1 -0
  19. package/dist/index.js +3 -4
  20. package/dist/index.js.map +1 -1
  21. package/dist/observability-metrics.d.ts +1 -1
  22. package/dist/workflow-bundle.js +1 -1
  23. package/package.json +21 -21
  24. package/src/activities/agent-run-admission.ts +1 -98
  25. package/src/activities/agent-turn/admission.ts +11 -5
  26. package/src/activities/agent-turn/claim.ts +12 -0
  27. package/src/activities/agent-turn/compaction-prep.ts +35 -1
  28. package/src/activities/agent-turn/errors.ts +51 -19
  29. package/src/activities/agent-turn/failure-settlement.ts +31 -1
  30. package/src/activities/agent-turn/finalization-monitor.ts +14 -1
  31. package/src/activities/agent-turn/finalization.ts +5 -0
  32. package/src/activities/agent-turn/model-usage.ts +38 -0
  33. package/src/activities/agent-turn/provider-recovery-metrics.ts +64 -0
  34. package/src/activities/agent-turn/run.ts +1 -0
  35. package/src/activities/agent-turn/stream-attempt.ts +32 -16
  36. package/src/activities/agent-turn/tool-environment.ts +9 -2
  37. package/src/activities/agent-turn/turn-context.ts +4 -0
  38. package/src/activities/goals.ts +17 -113
  39. package/src/activities/knowledge-indexing.ts +2 -2
  40. package/src/activities/scheduled-tasks.ts +18 -3
  41. package/src/activities/types.ts +1 -0
  42. package/src/index.ts +0 -2
  43. package/src/observability-metrics.ts +1 -0
  44. package/dist/activities-control-BNPTRTOE.js.map +0 -1
  45. package/dist/activities-turn-5LGZOSVU.js.map +0 -1
  46. package/dist/chunk-3ZTRWDMN.js.map +0 -1
  47. package/dist/chunk-LJX7Z2KP.js.map +0 -1
@@ -1,3 +1,4 @@
1
+ import { canonicalizeConfiguredModelId } from "@opengeni/config";
1
2
  import {
2
3
  applyCreditDebitUpToBalance,
3
4
  recordUsageEvent,
@@ -55,6 +56,27 @@ export function modelUsageSourceKey(input: {
55
56
  return input.dispatchId ? `${input.dispatchId}:${input.positionalKey}` : input.positionalKey;
56
57
  }
57
58
 
59
+ /** Legacy aggregate usage may only debit a policy shared by its completed calls. */
60
+ export function aggregateCreditPolicyRevision(input: {
61
+ responseRevisions: ReadonlySet<number | undefined>;
62
+ lastAdmittedRevision: number | undefined;
63
+ chargesOpenGeniCredits: boolean;
64
+ totalTokens: number | null;
65
+ }): number | undefined {
66
+ if (
67
+ input.chargesOpenGeniCredits &&
68
+ input.responseRevisions.size > 1 &&
69
+ (input.totalTokens ?? 0) > 0
70
+ ) {
71
+ throw new Error("Aggregate model usage spans different credit policy revisions");
72
+ }
73
+ // Bind the completed response, even if later preparation changed admission.
74
+ // Older runtimes without response callbacks retain their admitted snapshot.
75
+ return input.responseRevisions.size === 1
76
+ ? input.responseRevisions.values().next().value
77
+ : input.lastAdmittedRevision;
78
+ }
79
+
58
80
  export function providerContextTokens(
59
81
  usage:
60
82
  | {
@@ -209,6 +231,7 @@ export async function processModelResponseTerminalEvent(input: {
209
231
  externallyBilled: boolean;
210
232
  chargesOpenGeniCredits?: boolean;
211
233
  countsTowardTokenCap?: boolean;
234
+ creditPolicyRevision?: number | undefined;
212
235
  servingCredentialId: string | null;
213
236
  priorSessionCredentialId: string | null;
214
237
  emittedSourceKeys: Set<string>;
@@ -231,6 +254,15 @@ export async function processModelResponseTerminalEvent(input: {
231
254
  if (!terminal) {
232
255
  return { status: "not_response" };
233
256
  }
257
+ // Some providers mirror a terminal response before normalized usage arrives.
258
+ // Claiming that empty raw mirror would discard the SDK's billable response.
259
+ if (
260
+ !terminal.usage &&
261
+ input.event.type === "raw_model_stream_event" &&
262
+ input.event.data.type !== "response_done"
263
+ ) {
264
+ return { status: "not_response" };
265
+ }
234
266
 
235
267
  const responseOrdinal = input.state.responseCount + 1;
236
268
  const sourceKey = modelUsageSourceKey({
@@ -268,6 +300,7 @@ export async function processModelResponseTerminalEvent(input: {
268
300
  turnId: input.turnId,
269
301
  turnAttemptId: input.turnAttemptId,
270
302
  model: input.model,
303
+ creditPolicyRevision: input.creditPolicyRevision,
271
304
  externallyBilled: input.externallyBilled,
272
305
  ...(input.chargesOpenGeniCredits !== undefined
273
306
  ? { chargesOpenGeniCredits: input.chargesOpenGeniCredits }
@@ -374,6 +407,7 @@ export async function processCompactionModelUsageEvent(input: {
374
407
  externallyBilled: boolean;
375
408
  chargesOpenGeniCredits?: boolean;
376
409
  countsTowardTokenCap?: boolean;
410
+ creditPolicyRevision?: number | undefined;
377
411
  servingCredentialId: string | null;
378
412
  priorSessionCredentialId: string | null;
379
413
  emittedSourceKeys: Set<string>;
@@ -416,6 +450,7 @@ export async function processCompactionModelUsageEvent(input: {
416
450
  turnId: input.turnId,
417
451
  turnAttemptId: input.turnAttemptId,
418
452
  model: input.model,
453
+ creditPolicyRevision: input.creditPolicyRevision,
419
454
  externallyBilled: input.externallyBilled,
420
455
  ...(input.chargesOpenGeniCredits !== undefined
421
456
  ? { chargesOpenGeniCredits: input.chargesOpenGeniCredits }
@@ -651,6 +686,7 @@ export async function recordModelUsageAndDebitCredits(
651
686
  externallyBilled: boolean;
652
687
  chargesOpenGeniCredits?: boolean;
653
688
  countsTowardTokenCap?: boolean;
689
+ creditPolicyRevision?: number | undefined;
654
690
  gatewayBilling?: ModelResponseUsage["gatewayBilling"];
655
691
  usage?: ModelUsageInput | ModelCallUsageInput | null;
656
692
  normalizedUsage?: ModelCallUsageNormalization;
@@ -842,6 +878,8 @@ export async function recordModelUsageAndDebitCredits(
842
878
  workspaceId: input.workspaceId,
843
879
  type: "model_usage_debit",
844
880
  requestedAmountMicros: costMicros,
881
+ modelId: canonicalizeConfiguredModelId(settings, input.model),
882
+ creditPolicyRevision: input.creditPolicyRevision,
845
883
  sourceType: "model_response",
846
884
  sourceId: `${input.turnId}:${input.sourceKey}`,
847
885
  idempotencyKey: `credit:model_usage_debit:${input.turnId}:${input.sourceKey}`,
@@ -0,0 +1,64 @@
1
+ import type { Observability } from "@opengeni/observability";
2
+
3
+ export type ProviderRecoveryObservation = {
4
+ startedAt: number;
5
+ cause: "rate_limited" | "unavailable" | "connectivity";
6
+ };
7
+
8
+ export function providerRecoveryCause(code: unknown): ProviderRecoveryObservation["cause"] | null {
9
+ if (code === "provider_rate_limited") return "rate_limited";
10
+ if (code === "provider_unavailable") return "unavailable";
11
+ if (code === "upstream_connectivity_unavailable") return "connectivity";
12
+ return null;
13
+ }
14
+
15
+ export function readProviderRecoveryObservation(
16
+ metadata: Record<string, unknown>,
17
+ ): ProviderRecoveryObservation | undefined {
18
+ const cause = providerRecoveryCause(metadata.providerRecoveryReason);
19
+ const startedAt =
20
+ typeof metadata.providerRecoveryStartedAt === "string"
21
+ ? Date.parse(metadata.providerRecoveryStartedAt)
22
+ : Number.NaN;
23
+ return cause && Number.isFinite(startedAt) ? { cause, startedAt } : undefined;
24
+ }
25
+
26
+ /** Operational observations only; never retry authority, billing or diagnostics. */
27
+ export function recordProviderRecoveryOutcome(
28
+ observability: Observability,
29
+ input: {
30
+ route?: { provider: string; model: string } | undefined;
31
+ cause: ProviderRecoveryObservation["cause"];
32
+ outcome: "scheduled" | "recovered" | "exhausted";
33
+ delayMs?: number;
34
+ elapsedMs?: number;
35
+ },
36
+ ): void {
37
+ if (!input.route) return;
38
+ const labels = { ...input.route, cause: input.cause, outcome: input.outcome };
39
+ try {
40
+ observability.incrementCounter({
41
+ name: "opengeni_model_recovery_total",
42
+ help: "Observed model recovery decisions and successful resumptions.",
43
+ labels,
44
+ });
45
+ if (input.delayMs !== undefined) {
46
+ observability.observeHistogram({
47
+ name: "opengeni_model_recovery_delay_seconds",
48
+ help: "Scheduled model recovery delay, including provider hints and jitter.",
49
+ labels: { ...input.route, cause: input.cause },
50
+ value: Math.max(0, input.delayMs) / 1_000,
51
+ });
52
+ }
53
+ if (input.elapsedMs !== undefined) {
54
+ observability.observeHistogram({
55
+ name: "opengeni_model_recovery_duration_seconds",
56
+ help: "Elapsed recovery episode including backoff, preparation and model requests.",
57
+ labels,
58
+ value: Math.max(0, input.elapsedMs) / 1_000,
59
+ });
60
+ }
61
+ } catch {
62
+ // Metrics cannot interrupt settlement or model progress.
63
+ }
64
+ }
@@ -1090,6 +1090,7 @@ export function createRunAgentTurnActivity(services: () => Promise<ActivityServi
1090
1090
  ),
1091
1091
  );
1092
1092
  const compactionPrep = await prepareCompaction({
1093
+ entitlements,
1093
1094
  input,
1094
1095
  settings: capabilitySettings,
1095
1096
  db,
@@ -18,6 +18,7 @@ import {
18
18
  updateSessionTitleWithEvent,
19
19
  } from "@opengeni/db";
20
20
  import { publishDurableSessionEvents } from "@opengeni/events";
21
+ import { recordProviderRecoveryOutcome } from "./provider-recovery-metrics";
21
22
  import {
22
23
  AssistantMessagePhaseTracker,
23
24
  normalizeModelCallUsage,
@@ -121,6 +122,7 @@ import {
121
122
  completedToolCallFromSdkEvent,
122
123
  } from "./history";
123
124
  import {
125
+ aggregateCreditPolicyRevision,
124
126
  modelUsageSourceKey,
125
127
  recordCompletedModelCallBeforeOwnershipFences,
126
128
  TurnEventPublisher,
@@ -383,6 +385,8 @@ export async function runTurnStreamAttempt(
383
385
  );
384
386
  let parallelSessionTitle: ReturnType<typeof startParallelSessionTitleGeneration> | null = null;
385
387
  let parallelSessionTitleFinished = false;
388
+ let creditPolicyRevision: number | undefined;
389
+ let titleCreditPolicyRevision: number | undefined;
386
390
  const finishParallelSessionTitle = async (): Promise<void> => {
387
391
  if (parallelSessionTitleFinished) return;
388
392
  parallelSessionTitleFinished = true;
@@ -392,6 +396,7 @@ export async function runTurnStreamAttempt(
392
396
  if (generated.usage) {
393
397
  await processSessionTitleModelUsageEvent({
394
398
  usage: generated.usage,
399
+ creditPolicyRevision: titleCreditPolicyRevision,
395
400
  state: sessionTitleUsageState,
396
401
  dispatchId: modelUsageDispatchId,
397
402
  settings,
@@ -594,7 +599,8 @@ export async function runTurnStreamAttempt(
594
599
  let finalReplyNudged = false;
595
600
  const revalidateModelCallAdmission = async () => {
596
601
  await historySink.reconcileConversationTruth({ requireDurable: true });
597
- await ensureRunAllowedBetweenModelCalls({
602
+ creditPolicyRevision = await ensureRunAllowedBetweenModelCalls({
603
+ modelId: resolvedModel?.configured.id ?? turn.model,
598
604
  settings,
599
605
  db,
600
606
  accountId: input.accountId,
@@ -624,6 +630,7 @@ export async function runTurnStreamAttempt(
624
630
  signal: runtimeCancellationSignal,
625
631
  admit: revalidateModelCallAdmission,
626
632
  });
633
+ const responseCreditPolicyRevisions = new Set<number | undefined>();
627
634
  const responseCountBeforeStream = modelResponseState.responseCount;
628
635
  eventing.batcher = null;
629
636
  // The SDK emits every processed call item for one model response before
@@ -841,7 +848,10 @@ export async function runTurnStreamAttempt(
841
848
  attempt.modelRequestStarted = true;
842
849
  return await runtime.runStream(agent, runInput!, eventing.modelRunSettings, {
843
850
  beforeModelRequest: modelCallAdmission.beforeModelRequest,
844
- onModelResponse: modelCallAdmission.onModelResponse,
851
+ onModelResponse: (event) => {
852
+ responseCreditPolicyRevisions.add(creditPolicyRevision);
853
+ return modelCallAdmission.onModelResponse(event);
854
+ },
845
855
  signal: runtimeCancellationSignal,
846
856
  sandboxEnvironment,
847
857
  onModelVisibleContext: async (snapshot) => {
@@ -1067,6 +1077,7 @@ export async function runTurnStreamAttempt(
1067
1077
  ? await media.retainNativeGeneratedImage(generatedImage)
1068
1078
  : null;
1069
1079
  const responseResult = await processModelResponseTerminalEvent({
1080
+ creditPolicyRevision,
1070
1081
  event: next.value,
1071
1082
  state: modelResponseState,
1072
1083
  dispatchId: modelUsageDispatchId,
@@ -1132,6 +1143,15 @@ export async function runTurnStreamAttempt(
1132
1143
  ]);
1133
1144
  attempt.providerRecoveryCount = 0;
1134
1145
  }
1146
+ if (attempt.providerRecoveryObservation) {
1147
+ recordProviderRecoveryOutcome(observability, {
1148
+ route: attempt.modelMetricRoute,
1149
+ cause: attempt.providerRecoveryObservation.cause,
1150
+ outcome: "recovered",
1151
+ elapsedMs: Date.now() - attempt.providerRecoveryObservation.startedAt,
1152
+ });
1153
+ attempt.providerRecoveryObservation = undefined;
1154
+ }
1135
1155
  const rawStreamHistory = (eventing.stream.state as { history?: unknown[] }).history;
1136
1156
  if (Array.isArray(rawStreamHistory)) {
1137
1157
  // The completed image item is normally retained from its own
@@ -1148,21 +1168,9 @@ export async function runTurnStreamAttempt(
1148
1168
  await historySink.reconcileConversationTruth();
1149
1169
  turnLifecycleMetricsFor(observability).progress({ attemptId: input.attemptId });
1150
1170
  modelCheckpointMemoryCollector.schedule(observability);
1151
- await ensureRunAllowedBetweenModelCalls({
1152
- settings,
1153
- db,
1154
- accountId: input.accountId,
1155
- workspaceId: input.workspaceId,
1156
- isExternallyBilledTurn: billingState.isExternallyBilledTurn,
1157
- entitlements,
1158
- chargesOpenGeniCredits: billingState.chargesOpenGeniCredits,
1159
- countsTowardTokenCap: billingState.countsTowardTokenCap,
1160
- initiatingHumanSubjectId: turn.initiatingHumanSubjectId,
1161
- serializedRunState: () =>
1162
- media.compactMediaRunState(String(eventing.stream!.state.toString())),
1163
- });
1164
1171
  }
1165
- // Release only after both the debit and frozen-human admission finish.
1172
+ // Release after settlement. The producer checks fresh admission before
1173
+ // its next request; a final response keeps the revision it ran under.
1166
1174
  modelCallAdmission.settle(next.value);
1167
1175
  const durableSdkEvent = generatedImageReceipt
1168
1176
  ? compactGeneratedImageSdkEvent(next.value, generatedImageReceipt)
@@ -1551,6 +1559,12 @@ export async function runTurnStreamAttempt(
1551
1559
  if (!streamSawPerResponseUsage) {
1552
1560
  const aggregateUsage = eventing.stream.state.usage;
1553
1561
  const normalizedAggregateUsage = normalizeModelCallUsage(aggregateUsage);
1562
+ const aggregatePolicyRevision = aggregateCreditPolicyRevision({
1563
+ responseRevisions: responseCreditPolicyRevisions,
1564
+ lastAdmittedRevision: creditPolicyRevision,
1565
+ chargesOpenGeniCredits: billingState.chargesOpenGeniCredits,
1566
+ totalTokens: normalizedAggregateUsage.totalTokens,
1567
+ });
1554
1568
  const aggregateInput = normalizedAggregateUsage.telemetry.inputTokens;
1555
1569
  const aggregateSourceKey = modelUsageSourceKey({
1556
1570
  responseId: null,
@@ -1573,6 +1587,7 @@ export async function runTurnStreamAttempt(
1573
1587
  leaseLostMessage: "Provider credential lease expired during the active turn",
1574
1588
  recordUsage: async () => {
1575
1589
  const billing = await recordModelUsageAndDebitCredits(settings, db, {
1590
+ creditPolicyRevision: aggregatePolicyRevision,
1576
1591
  accountId: input.accountId,
1577
1592
  workspaceId: input.workspaceId,
1578
1593
  sessionId: input.sessionId,
@@ -1965,6 +1980,7 @@ export async function runTurnStreamAttempt(
1965
1980
  turnExecutionPolicy.providerId,
1966
1981
  turnExecutionPolicy.latencyMode,
1967
1982
  );
1983
+ titleCreditPolicyRevision = creditPolicyRevision;
1968
1984
  parallelSessionTitle = startParallelSessionTitleGeneration({
1969
1985
  signal: runtimeCancellationSignal,
1970
1986
  generate: async (signal) =>
@@ -82,6 +82,7 @@ import { createTurnMediaArtifacts } from "./media-artifacts";
82
82
  import { SandboxChannelAService } from "@opengeni/runtime/sandbox";
83
83
  import { sandboxRunAs } from "@opengeni/runtime";
84
84
  import {
85
+ bundledSkillSelectionForAgentConfig,
85
86
  DEFAULT_FIRST_PARTY_MCP_PERMISSIONS,
86
87
  resolveAgentToolFamilies,
87
88
  type ResourceRef,
@@ -596,7 +597,11 @@ export async function prepareTurnToolRuntime(deps: PrepareTurnToolRuntimeDeps) {
596
597
  session.firstPartyMcpPermissions,
597
598
  linkedAuthority,
598
599
  );
599
- const toolFamilies = resolveAgentToolFamilies(session.agent);
600
+ // Background-command tools need compute: the effective route of this turn,
601
+ // a managed sandbox or an attached Connected Machine, not the durable home.
602
+ const toolFamilies = resolveAgentToolFamilies(session.agent, {
603
+ sandboxAttached: (activeSandboxBackend ?? groupBoxBackend) !== "none",
604
+ });
600
605
  const selectedFirstPartyMcpTools = toolFamilies.firstPartyTools(
601
606
  allowedFirstPartyMcpToolsForSession(runSettings, session.firstPartyMcpTools),
602
607
  );
@@ -669,7 +674,9 @@ export async function prepareTurnToolRuntime(deps: PrepareTurnToolRuntimeDeps) {
669
674
  }),
670
675
  ]);
671
676
  const bundledSkills = loadConfiguredBundledSkills({
672
- bundledSkillIds: session.bundledSkillIds,
677
+ // Rows that could not freeze the "none" default at create (scheduled
678
+ // generated sessions, pre-existing rows) get the same rule here.
679
+ bundledSkillIds: bundledSkillSelectionForAgentConfig(session.bundledSkillIds, session.agent),
673
680
  firstPartyTools: selectedFirstPartyMcpTools,
674
681
  videoGenerationEnabled:
675
682
  skillConfiguration.defaultModelId !== null && skillConfiguration.enabledModelIds.length > 0,
@@ -68,6 +68,10 @@ export type AttemptIdentityState = {
68
68
  triggerEventId: string | undefined;
69
69
  executionGeneration: number;
70
70
  providerRecoveryCount: number;
71
+ providerRecoveryObservation?:
72
+ | import("./provider-recovery-metrics").ProviderRecoveryObservation
73
+ | undefined;
74
+ modelMetricRoute?: { provider: string; model: string };
71
75
  claudeAuthRecovery?: { credentialId: string; credentialVersion: number } | undefined;
72
76
  modelRequestStarted: boolean;
73
77
  redispatchesAtDispatch: number;
@@ -1,27 +1,16 @@
1
1
  import {
2
2
  allowedFirstPartyMcpToolsForSession,
3
- configuredStaticUsageLimits,
4
- isModelAvailableForNewSelection,
5
- policyProviderIdForModel,
6
- resolveModelProvider,
7
3
  resolveTurnExecutionPolicyV1,
8
- withCodexCatalogProvider,
9
- withXaiSubscriptionCatalogProvider,
10
- WORKSPACE_GATEWAY_MODEL_ID_PREFIX,
11
- WORKSPACE_OPENROUTER_MODEL_ID_PREFIX,
12
4
  type Settings,
13
5
  } from "@opengeni/config";
14
6
  import {
15
- evaluateWorkspaceModelPolicy,
16
7
  mergeToolRefs,
17
8
  readTurnExecutionPolicyV1,
18
9
  type SessionGoal,
19
10
  type ToolRef,
20
11
  } from "@opengeni/contracts";
21
- import { isCodexBilledModel } from "@opengeni/codex";
22
12
  import {
23
13
  enqueueSessionWorkflowWakeIfRunnable,
24
- getWorkspaceModelPolicy,
25
14
  getSessionGoal,
26
15
  getSessionTurn,
27
16
  materializeGoalContinuation,
@@ -34,10 +23,10 @@ import type {
34
23
  } from "./types";
35
24
  import {
36
25
  modelFundingForAdmission,
37
- resolveCatalogSettings,
38
- resolveWorkspaceCatalogSettings,
26
+ goalRunBudgetBlocked,
27
+ resolveGoalModelAdmission,
39
28
  } from "@opengeni/core";
40
- import { agentRunAdmissionDenial } from "./agent-run-admission";
29
+ export { goalContinuationModelDecision, goalRunBudgetBlocked } from "@opengeni/core";
41
30
  import { turnCredentialRestriction } from "./agent-turn/credential-restriction";
42
31
 
43
32
  export function createGoalActivities(services: () => Promise<ControlActivityServices>) {
@@ -80,42 +69,18 @@ export function createGoalActivities(services: () => Promise<ControlActivityServ
80
69
  if (session.status === "failed" || session.status === "cancelled") {
81
70
  return { action: "none" };
82
71
  }
83
- let settings = (await resolveCatalogSettings(db, catalogSourceSettings)).settings;
84
- const inheritedContinuationModel = session.model;
85
- let continuationModel = inheritedContinuationModel;
72
+ const modelDecision = await resolveGoalModelAdmission(db, catalogSourceSettings, {
73
+ accountId: input.accountId,
74
+ workspaceId: input.workspaceId,
75
+ model: session.model,
76
+ codexCompactionMode: session.codexCompactionMode,
77
+ latencyMode: session.latencyMode,
78
+ });
79
+ const settings = modelDecision.settings;
80
+ const continuationModel = modelDecision.model;
86
81
  const continuationReasoningEffort = session.reasoningEffort;
87
82
  const continuationLatencyMode = session.latencyMode;
88
- const workspaceModelPolicy = await getWorkspaceModelPolicy(db, input.workspaceId);
89
- if (
90
- inheritedContinuationModel.startsWith(WORKSPACE_GATEWAY_MODEL_ID_PREFIX) ||
91
- inheritedContinuationModel.startsWith(WORKSPACE_OPENROUTER_MODEL_ID_PREFIX) ||
92
- session.model.startsWith(WORKSPACE_GATEWAY_MODEL_ID_PREFIX) ||
93
- session.model.startsWith(WORKSPACE_OPENROUTER_MODEL_ID_PREFIX)
94
- ) {
95
- settings = (
96
- await resolveWorkspaceCatalogSettings(db, catalogSourceSettings, {
97
- accountId: input.accountId,
98
- workspaceId: input.workspaceId,
99
- retainedProductModelIds: [inheritedContinuationModel, session.model],
100
- })
101
- ).settings;
102
- }
103
- const modelDecision = goalContinuationModelDecision({
104
- settings,
105
- workspaceModelPolicy,
106
- inheritedModel: inheritedContinuationModel,
107
- });
108
- continuationModel = modelDecision.model;
109
- let modelPolicyBlocked = modelDecision.blocked;
110
- // remote_v2 sessions may only continue on Codex models — refuse synthesis
111
- // that would leave the portable/non-Codex path (and mixed history shapes).
112
- if (
113
- !modelPolicyBlocked &&
114
- session.codexCompactionMode === "remote_v2" &&
115
- !isCodexBilledModel(continuationModel)
116
- ) {
117
- modelPolicyBlocked = `session is locked to Codex remote compaction v2; model "${continuationModel}" is not a Codex subscription model`;
118
- }
83
+ const modelPolicyBlocked = modelDecision.blocked;
119
84
  const turnExecutionPolicy = modelPolicyBlocked
120
85
  ? undefined
121
86
  : resolveTurnExecutionPolicyV1(settings, {
@@ -183,11 +148,12 @@ export function createGoalActivities(services: () => Promise<ControlActivityServ
183
148
  initiatingHumanSubjectId: causalTurn?.initiatingHumanSubjectId ?? null,
184
149
  },
185
150
  );
151
+ const pausedReason = modelPolicyBlocked
152
+ ? modelDecision.pausedReason
153
+ : budgetBlocked?.pausedReason;
186
154
  return {
187
155
  budgetBlocked: modelPolicyBlocked ?? budgetBlocked?.message ?? null,
188
- budgetPausedReason: modelPolicyBlocked
189
- ? "limits"
190
- : (budgetBlocked?.pausedReason ?? "limits"),
156
+ ...(pausedReason ? { budgetPausedReason: pausedReason } : {}),
191
157
  };
192
158
  },
193
159
  policy: continuationPolicy,
@@ -220,47 +186,6 @@ export function createGoalActivities(services: () => Promise<ControlActivityServ
220
186
  };
221
187
  }
222
188
 
223
- export function goalContinuationModelDecision(input: {
224
- settings: Settings;
225
- workspaceModelPolicy: Awaited<ReturnType<typeof getWorkspaceModelPolicy>>;
226
- inheritedModel: string;
227
- }): { model: string; blocked: string | null } {
228
- const catalogSettings = input.settings.supergrokSubscriptionEnabled
229
- ? withXaiSubscriptionCatalogProvider(
230
- input.settings.codexSubscriptionEnabled
231
- ? withCodexCatalogProvider(input.settings)
232
- : input.settings,
233
- )
234
- : input.settings.codexSubscriptionEnabled
235
- ? withCodexCatalogProvider(input.settings)
236
- : input.settings;
237
- const policyBlocks = (modelId: string): boolean =>
238
- input.workspaceModelPolicy !== null &&
239
- !evaluateWorkspaceModelPolicy(input.workspaceModelPolicy, {
240
- providerId: policyProviderIdForModel(catalogSettings, modelId),
241
- modelId,
242
- }).allowed;
243
- if (!resolveModelProvider(catalogSettings, input.inheritedModel)) {
244
- return {
245
- model: input.inheritedModel,
246
- blocked: `model "${input.inheritedModel}" is no longer in the deployment or workspace catalog; choose an available model before resuming the goal`,
247
- };
248
- }
249
- if (!isModelAvailableForNewSelection(catalogSettings, input.inheritedModel)) {
250
- return {
251
- model: input.inheritedModel,
252
- blocked: `model "${input.inheritedModel}" is retired from new selection; choose an available model before resuming the goal`,
253
- };
254
- }
255
- if (!policyBlocks(input.inheritedModel)) {
256
- return { model: input.inheritedModel, blocked: null };
257
- }
258
- return {
259
- model: input.inheritedModel,
260
- blocked: `workspace model policy blocks model "${input.inheritedModel}"; pick an allowed model or change the workspace model policy`,
261
- };
262
- }
263
-
264
189
  export function goalContinuationFundedWithoutCredits(
265
190
  settings: Settings,
266
191
  model: string,
@@ -364,24 +289,3 @@ export function withFirstPartyTools(settings: Settings, tools: ToolRef[]): ToolR
364
289
  }
365
290
  return mergeToolRefs(tools, [{ kind: "mcp", id: "opengeni" }]);
366
291
  }
367
-
368
- /**
369
- * Goals share scheduled admission and pause visibly without synthesizing work.
370
- */
371
- export async function goalRunBudgetBlocked(
372
- services: Parameters<typeof agentRunAdmissionDenial>[0],
373
- input: Omit<Parameters<typeof agentRunAdmissionDenial>[1], "requestedAgentRuns">,
374
- ): Promise<{ pausedReason: "limits" | "allowance"; message: string } | null> {
375
- const denial = await agentRunAdmissionDenial(services, { ...input, requestedAgentRuns: 1 });
376
- if (denial === null) return null;
377
- if (denial === "allowance_exhausted") {
378
- return { pausedReason: "allowance", message: "Opengeni usage allowance exhausted" };
379
- }
380
- const limits = configuredStaticUsageLimits(services.settings);
381
- const messages = {
382
- insufficient_credits: "insufficient Opengeni credits",
383
- monthly_model_cost_limit: `monthly model cost limit reached (${limits.maxMonthlyCostMicrosPerAccount} micros)`,
384
- monthly_agent_run_limit: `monthly agent run limit reached (${limits.maxMonthlyAgentRunsPerWorkspace})`,
385
- };
386
- return { pausedReason: "limits", message: messages[denial] };
387
- }
@@ -10,7 +10,7 @@ import {
10
10
  creditDebitAttributionMetadata,
11
11
  deferKnowledgeIndexJob,
12
12
  freezeKnowledgeIndexBillingMode,
13
- getBillingBalance,
13
+ getSpendableCreditBalance,
14
14
  guardPaidKnowledgeIndexPublication,
15
15
  knowledgeIndexBillingActivationTime,
16
16
  readKnowledgeIndexSource,
@@ -211,7 +211,7 @@ export function createKnowledgeIndexingActivities(
211
211
  return;
212
212
  }
213
213
  if (paid && current.nextIndex === 0) {
214
- const balance = await getBillingBalance(lockedDb, claim.accountId);
214
+ const balance = await getSpendableCreditBalance(lockedDb, claim.accountId);
215
215
  if (balance.balanceMicros <= 0) {
216
216
  await waitKnowledgeIndexForFunding(lockedDb, claim);
217
217
  result.deferred++;
@@ -98,6 +98,7 @@ import {
98
98
  allowedFirstPartyMcpToolsForSession,
99
99
  resolveFirstPartyMcpToolPolicy,
100
100
  resolveTurnExecutionPolicyV1,
101
+ TurnExecutionPolicyModelUnavailableError,
101
102
  } from "@opengeni/config";
102
103
  import { Context } from "@temporalio/activity";
103
104
  import { createHash } from "node:crypto";
@@ -1082,8 +1083,9 @@ export function createScheduledTaskActivities(services: () => Promise<ControlAct
1082
1083
  }
1083
1084
  }
1084
1085
  : undefined;
1085
- const turnExecutionPolicy = scheduledTaskRunExecutionPolicy(
1086
- resolveTurnExecutionPolicyV1(settings, {
1086
+ let acceptedTurnExecutionPolicy: ReturnType<typeof resolveTurnExecutionPolicyV1>;
1087
+ try {
1088
+ acceptedTurnExecutionPolicy = resolveTurnExecutionPolicyV1(settings, {
1087
1089
  modelId: acceptedModel,
1088
1090
  requestedModelId:
1089
1091
  generatedTarget && task.agentConfig.model ? task.agentConfig.model : null,
@@ -1100,7 +1102,20 @@ export function createScheduledTaskActivities(services: () => Promise<ControlAct
1100
1102
  : "session",
1101
1103
  latencyMode: acceptedLatencyMode,
1102
1104
  latencyModeSource: generatedTarget ? "deployment" : "session",
1103
- }),
1105
+ });
1106
+ } catch (error) {
1107
+ // A retired or removed model refuses every occurrence until the task
1108
+ // (or its target session) names an available model: record that as a
1109
+ // visible terminal run instead of exhausting activity retries.
1110
+ if (!(error instanceof TurnExecutionPolicyModelUnavailableError)) throw error;
1111
+ return await refuseAdmission(
1112
+ "scheduled_model_unavailable",
1113
+ false,
1114
+ `${error.message}: ${acceptedModel}`,
1115
+ );
1116
+ }
1117
+ const turnExecutionPolicy = scheduledTaskRunExecutionPolicy(
1118
+ acceptedTurnExecutionPolicy,
1104
1119
  input,
1105
1120
  creatorPolicy?.credentialRestriction,
1106
1121
  );
@@ -500,6 +500,7 @@ export type DispatchScheduledTaskRunResult =
500
500
  | "machine_enrollment_inactive"
501
501
  | "variable_set_unavailable"
502
502
  | "rig_version_unavailable"
503
+ | "scheduled_model_unavailable"
503
504
  | "knowledge_source_paused"
504
505
  | "legacy_source_schedule_requires_migration"
505
506
  | "atlassian_native_retired"
package/src/index.ts CHANGED
@@ -1214,8 +1214,6 @@ export async function startWorker() {
1214
1214
  rlsStrategy: settings.rlsStrategy,
1215
1215
  expectedRole: settings.runtimeDatabaseRole,
1216
1216
  targetSchema: settings.dbSchema.trim() || "public",
1217
- organizationTenancyCanonicalActivationEnabled:
1218
- settings.organizationTenancyCanonicalActivationEnabled,
1219
1217
  } as const;
1220
1218
  const controlPlaneAuth = resolveNatsControlPlaneAuth(settings);
1221
1219
  let bus: Awaited<ReturnType<typeof createNatsEventBus>> | undefined;
@@ -1308,6 +1308,7 @@ export function recordSandboxDeadlineRotationsRequested(
1308
1308
  export const SANDBOX_COMMAND_CONTAINMENT_OUTCOMES = [
1309
1309
  "idle_enrolled",
1310
1310
  "deadline_enrolled",
1311
+ "quiescence_enrolled",
1311
1312
  "resumed_enrolled",
1312
1313
  "not_eligible",
1313
1314
  "inspection_failed",