@opengeni/worker-bundle 0.28.3-canary.0 → 0.28.3

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/index.js CHANGED
@@ -19,7 +19,7 @@ import {
19
19
  startContextCompactionPendingMonitor,
20
20
  startSessionRecoveryMonitor,
21
21
  startTurnCapacityMonitor
22
- } from "./chunk-A4XQWVZK.js";
22
+ } from "./chunk-BARTLVBK.js";
23
23
 
24
24
  // src/index.ts
25
25
  import {
@@ -781,7 +781,7 @@ function temporalWorkflowExecutionNotFound(error) {
781
781
  return isGrpcServiceError(error) && error.code === 5;
782
782
  }
783
783
  async function createDefaultWorkerActivities(role, dependencies = {}) {
784
- return role === "control" ? (await import("./activities-control-JUBMDBDZ.js")).createControlActivities(dependencies) : (await import("./activities-turn-I2ZKGZE6.js")).createTurnActivities(dependencies);
784
+ return role === "control" ? (await import("./activities-control-UXGPMDL3.js")).createControlActivities(dependencies) : (await import("./activities-turn-NOYDE7PX.js")).createTurnActivities(dependencies);
785
785
  }
786
786
  function resolveOpenGeniWorkflowDefinition(moduleUrl = import.meta.url) {
787
787
  const modulePath = fileURLToPath(moduleUrl).replaceAll("\\", "/");
@@ -91,23 +91,15 @@ export declare function recordWorkerDeathRecoveryMetrics(observability: Observab
91
91
  export declare class TurnLifecycleMetrics {
92
92
  private readonly observability;
93
93
  private readonly options;
94
- private readonly attempts;
94
+ private readonly turns;
95
95
  private timer;
96
96
  constructor(observability: Observability, options?: {
97
97
  now?: () => number;
98
98
  refreshIntervalMs?: number;
99
99
  });
100
- start(input: {
101
- attemptId: string;
102
- }): void;
103
- progress(input: {
104
- attemptId: string;
105
- }): void;
106
- finish(input: {
107
- attemptId: string;
108
- outcome: TurnOutcome | null;
109
- durationSeconds?: number;
110
- }): void;
100
+ start(turnId: string): void;
101
+ progress(turnId: string): void;
102
+ finish(turnId: string, outcome: TurnOutcome | null, durationSeconds?: number): void;
111
103
  refreshGauges(): void;
112
104
  stop(): void;
113
105
  private oldestInflightAgeSeconds;
@@ -1,5 +1,4 @@
1
- import { type ObjectStorage } from "@opengeni/storage";
2
- import type { VerifiedHostWorkspaceArchive } from "@opengeni/runtime/sandbox";
1
+ import type { ObjectStorage } from "@opengeni/storage";
3
2
  import { type WorkspaceArchiveDescriptor, type WorkspaceArchiveObjectRef } from "@opengeni/contracts";
4
3
  export declare class WorkspaceArchiveObjectStorageRequiredError extends Error {
5
4
  readonly code: "workspace_archive_object_storage_required";
@@ -14,26 +13,15 @@ export declare function putTarWorkspaceArchiveObject(input: {
14
13
  archive: {
15
14
  bytes: Uint8Array;
16
15
  descriptor: WorkspaceArchiveDescriptor;
17
- } | VerifiedHostWorkspaceArchive;
16
+ };
18
17
  }): Promise<WorkspaceArchiveObjectRef>;
19
- /** The caller owns a fresh candidate returned by this module. A thrown or lost
20
- * database response is UNKNOWN: it may have committed, so never delete on catch.
21
- * Only the transaction's exact candidate disposition can authorize cleanup. */
22
- export declare function persistWorkspaceArchiveCandidate<T extends {
23
- wrote: boolean;
24
- candidateDisposition?: "adopted" | "already_referenced" | "unused";
25
- }>(input: {
26
- objectStorage?: ObjectStorage | null;
27
- ref?: WorkspaceArchiveObjectRef;
28
- persist: () => Promise<T>;
29
- }): Promise<T>;
30
18
  export declare function putVersion1TarArchiveOrInline(input: {
31
19
  backend: string;
32
20
  objectStorage?: ObjectStorage | null | undefined;
33
21
  accountId: string;
34
22
  workspaceId: string;
35
23
  sandboxGroupId: string;
36
- archive: VerifiedHostWorkspaceArchive | {
24
+ archive: {
37
25
  bytes: Uint8Array;
38
26
  descriptor: WorkspaceArchiveDescriptor;
39
27
  base64: string;
@@ -1,7 +1,7 @@
1
1
  import { type Settings } from "@opengeni/config";
2
2
  import { readLease, reinstateTurnLeaseHolder, SandboxLeaseSupersededError, type Database, type LeaseHolderKind } from "@opengeni/db";
3
3
  import { establishSandboxSessionFromEnvelope, type EstablishedSandboxSession, type RuntimeMetricsHooks } from "@opengeni/runtime";
4
- import { type ObjectStorage } from "@opengeni/storage";
4
+ import type { ObjectStorage } from "@opengeni/storage";
5
5
  import type { Observability } from "@opengeni/observability";
6
6
  export type ResumeHolderKind = LeaseHolderKind;
7
7
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/worker-bundle",
3
- "version": "0.28.3-canary.0",
3
+ "version": "0.28.3",
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,22 +54,22 @@
54
54
  },
55
55
  "dependencies": {
56
56
  "@llamaindex/liteparse": "2.14.2",
57
- "@opengeni/agent-proto": "^0.5.1-canary.25",
58
- "@opengeni/capabilities": "^0.3.3-canary.7",
59
- "@opengeni/codemode": "^0.5.5-canary.0",
60
- "@opengeni/codex": "^0.2.22-canary.7",
61
- "@opengeni/config": "^1.1.1-canary.0",
62
- "@opengeni/contracts": "^3.0.1-canary.0",
63
- "@opengeni/core": "^2.9.3-canary.0",
64
- "@opengeni/db": "^4.3.2-canary.0",
65
- "@opengeni/documents": "^0.8.26-canary.0",
66
- "@opengeni/events": "^0.4.24-canary.0",
67
- "@opengeni/github": "^0.7.9-canary.0",
68
- "@opengeni/network": "^0.3.1-canary.7",
69
- "@opengeni/observability": "^0.8.25-canary.0",
70
- "@opengeni/runtime": "^2.5.2-canary.0",
71
- "@opengeni/storage": "^0.2.126-canary.0",
72
- "@opengeni/xai-subscription": "^0.1.4-canary.7",
57
+ "@opengeni/agent-proto": "^0.5.1",
58
+ "@opengeni/capabilities": "^0.3.3",
59
+ "@opengeni/codemode": "^0.5.5",
60
+ "@opengeni/codex": "^0.2.22",
61
+ "@opengeni/config": "^1.1.1",
62
+ "@opengeni/contracts": "^3.0.1",
63
+ "@opengeni/core": "^2.9.3",
64
+ "@opengeni/db": "^4.3.2",
65
+ "@opengeni/documents": "^0.8.26",
66
+ "@opengeni/events": "^0.4.24",
67
+ "@opengeni/github": "^0.7.9",
68
+ "@opengeni/network": "^0.3.1",
69
+ "@opengeni/observability": "^0.8.25",
70
+ "@opengeni/runtime": "^2.5.2",
71
+ "@opengeni/storage": "^0.2.126",
72
+ "@opengeni/xai-subscription": "^0.1.4",
73
73
  "@temporalio/activity": "^1.17.0",
74
74
  "@temporalio/client": "^1.17.0",
75
75
  "@temporalio/worker": "^1.17.0",
@@ -347,7 +347,7 @@ export async function claimTurnAttempt(deps: ClaimTurnDeps): Promise<ClaimTurnOu
347
347
  });
348
348
  return attached.action === "claimed" && attached.turn.id === turn.id;
349
349
  };
350
- turnLifecycleMetricsFor(observability).start({ attemptId: input.attemptId });
350
+ turnLifecycleMetricsFor(observability).start(attempt.turnId);
351
351
  // §7.5 P3 — pass the accepted billing attribution (externally funded turns
352
352
  // bypass OpenGeni credit/token gates)
353
353
  // AND the optional host `entitlements` port (when bound, its admitRun replaces
@@ -451,7 +451,7 @@ export async function claimTurnAttempt(deps: ClaimTurnDeps): Promise<ClaimTurnOu
451
451
  }
452
452
  recordCanonicalStartupMilestones(appended.canonicalStartupMilestones);
453
453
  if (inputs.length > 0) {
454
- turnLifecycleMetricsFor(observability).progress({ attemptId: input.attemptId });
454
+ turnLifecycleMetricsFor(observability).progress(attempt.turnId!);
455
455
  }
456
456
  activityContext?.heartbeat({
457
457
  ...heartbeatDetails,
@@ -515,7 +515,7 @@ export async function claimTurnAttempt(deps: ClaimTurnDeps): Promise<ClaimTurnOu
515
515
  return false;
516
516
  }
517
517
  recordCanonicalStartupMilestones(result.canonicalStartupMilestones);
518
- turnLifecycleMetricsFor(observability).progress({ attemptId: input.attemptId });
518
+ turnLifecycleMetricsFor(observability).progress(attempt.turnId!);
519
519
  await publishDurableSessionEvents(bus, input.workspaceId, input.sessionId, result.events);
520
520
  if (inputSettlement.turnStatus === "requires_action") {
521
521
  // The settlement transaction committed the parent's child_requires_action
@@ -745,17 +745,12 @@ export async function finalizeTurnAttempt(deps: TurnFinalizationDeps): Promise<v
745
745
  status: finalizationError ? "cleanup_failed" : control.activityStatus,
746
746
  durationSeconds,
747
747
  });
748
- if (attempt.turnId) {
749
- // This registry describes physical activity ownership, not durable turn
750
- // settlement. Always clear the exact attempt when its activity unwinds,
751
- // even when failure settlement could not classify an outcome. Otherwise
752
- // a recovered successor can make progress while the predecessor leaves a
753
- // process-local false stuck-turn alert behind.
754
- turnLifecycleMetricsFor(observability).finish({
755
- attemptId: input.attemptId,
756
- outcome: control.turnMetricOutcome,
748
+ if (attempt.turnId && control.activityStatus !== "unknown") {
749
+ turnLifecycleMetricsFor(observability).finish(
750
+ attempt.turnId,
751
+ control.turnMetricOutcome,
757
752
  durationSeconds,
758
- });
753
+ );
759
754
  }
760
755
  activitySpan.end({
761
756
  attributes: {
@@ -1062,7 +1062,7 @@ export async function runTurnStreamAttempt(
1062
1062
  currentToolBatchCallIds = new Set<string>();
1063
1063
  currentToolBatchCompletedCallIds = new Set<string>();
1064
1064
  await historySink.reconcileConversationTruth();
1065
- turnLifecycleMetricsFor(observability).progress({ attemptId: input.attemptId });
1065
+ turnLifecycleMetricsFor(observability).progress(attempt.turnId!);
1066
1066
  modelCheckpointMemoryCollector.schedule(observability);
1067
1067
  try {
1068
1068
  await ensureRunAllowed(
@@ -82,10 +82,7 @@ import {
82
82
  // box is typed as provider loss. Stale capture reconciliation separately uses
83
83
  // establishSandboxSessionFromEnvelope in strict resume-only mode so it can
84
84
  // reuse the runtime's provider-ready proof without ever cold-restoring.
85
- captureWorkspaceArchiveForStorage,
86
- disposeWorkspaceArchive,
87
- type VerifiedWorkspaceArchivePayload,
88
- type VerifiedHostWorkspaceArchive,
85
+ captureVerifiedWorkspaceArchive,
89
86
  cancelSelfhostedOp,
90
87
  assertConsistentSandboxProviderIdentity,
91
88
  createSandboxClientForBackend,
@@ -163,7 +160,9 @@ import {
163
160
  } from "../opensandbox-kubernetes-inventory";
164
161
  import type { ObjectStorage } from "@opengeni/storage";
165
162
  import {
166
- persistWorkspaceArchiveCandidate,
163
+ collectWorkspaceArchiveObjectKeys,
164
+ deleteUnpublishedWorkspaceArchiveObject,
165
+ deleteWorkspaceArchiveObjectKeys,
167
166
  putVersion1TarArchiveOrInline,
168
167
  } from "../sandbox-archive-storage";
169
168
 
@@ -211,7 +210,7 @@ type DrainSandboxClient = {
211
210
  * aborts the terminate before client.delete()).
212
211
  */
213
212
  export type PersistArchiveFn = (
214
- archiveBase64: string | VerifiedHostWorkspaceArchive | null,
213
+ archiveBase64: string | null,
215
214
  archiveMetadata?: WorkspaceArchiveDescriptor,
216
215
  providerSession?: unknown,
217
216
  providerBinding?: Awaited<
@@ -248,7 +247,6 @@ export type TerminateBoxFn = (
248
247
  providerCaptureRequestId?: string,
249
248
  captureDisposition?: DrainCaptureDisposition,
250
249
  capturePolicy?: ProviderWorkspaceCapturePolicy | null,
251
- diskBackedArchives?: boolean,
252
250
  ) => Promise<boolean | ProviderTerminationOutcome>;
253
251
 
254
252
  export type SweepModalOrphansFn = (
@@ -481,7 +479,6 @@ export function createSandboxLeaseActivities(
481
479
  providerCaptureRequestId,
482
480
  captureDisposition,
483
481
  capturePolicy,
484
- diskBackedArchives,
485
482
  ) =>
486
483
  await terminateProviderBox(
487
484
  settings,
@@ -493,7 +490,6 @@ export function createSandboxLeaseActivities(
493
490
  providerCaptureRequestId,
494
491
  captureDisposition,
495
492
  capturePolicy,
496
- diskBackedArchives,
497
493
  ));
498
494
  const sweepModalOrphans: SweepModalOrphansFn =
499
495
  options.sweepModalOrphans ?? sweepModalOrphansForConfiguredBackend;
@@ -2679,7 +2675,7 @@ async function terminateDrainableBox(
2679
2675
  ? (lease.recovery.archive.current?.revision ?? null)
2680
2676
  : null;
2681
2677
  const persistArchive: PersistArchiveFn = async (
2682
- archiveBase64: string | VerifiedHostWorkspaceArchive | null,
2678
+ archiveBase64: string | null,
2683
2679
  archiveMetadata?: WorkspaceArchiveDescriptor,
2684
2680
  _providerSession?: unknown,
2685
2681
  providerBinding?: Awaited<
@@ -2695,7 +2691,6 @@ async function terminateDrainableBox(
2695
2691
  const archiveMetrics = runtimeMetricsHooksForObservability(observability);
2696
2692
  let checkpointArtifactId: string | null = null;
2697
2693
  if (
2698
- typeof archiveBase64 === "string" &&
2699
2694
  archiveBase64 &&
2700
2695
  archiveMetadata?.version === 2 &&
2701
2696
  (archiveMetadata.provider === "modal_snapshot_filesystem" ||
@@ -2740,33 +2735,59 @@ async function terminateDrainableBox(
2740
2735
  if (!archiveMetadata) {
2741
2736
  throw new Error("Sandbox snapshot publication requires a verified archive descriptor");
2742
2737
  }
2743
- const published = await putVersion1TarArchiveOrInline({
2744
- backend: lease.backend,
2745
- objectStorage,
2746
- accountId,
2747
- workspaceId: row.workspaceId,
2748
- sandboxGroupId: row.sandboxGroupId,
2749
- archive:
2750
- typeof archiveBase64 === "object"
2751
- ? archiveBase64
2752
- : {
2753
- bytes: Buffer.from(archiveBase64, "base64"),
2754
- descriptor: archiveMetadata,
2755
- base64: archiveBase64,
2756
- },
2757
- metrics: archiveMetrics,
2758
- });
2759
- result = await persistWorkspaceArchiveCandidate({
2760
- objectStorage,
2761
- ...(published.workspaceArchiveRef ? { ref: published.workspaceArchiveRef } : {}),
2762
- persist: () =>
2763
- persistDrainSnapshot(db, {
2764
- ...baseInput,
2765
- workspaceArchiveMeta: archiveMetadata,
2766
- ...published,
2767
- ...(checkpointArtifactId ? { checkpointArtifactId } : {}),
2768
- }),
2769
- });
2738
+ const priorKeys = collectWorkspaceArchiveObjectKeys(
2739
+ (lease.resumeState as Record<string, unknown> | null | undefined) ?? null,
2740
+ );
2741
+ let workspaceArchiveRef:
2742
+ | Awaited<ReturnType<typeof putVersion1TarArchiveOrInline>>["workspaceArchiveRef"]
2743
+ | undefined;
2744
+ try {
2745
+ const published = await putVersion1TarArchiveOrInline({
2746
+ backend: lease.backend,
2747
+ objectStorage,
2748
+ accountId,
2749
+ workspaceId: row.workspaceId,
2750
+ sandboxGroupId: row.sandboxGroupId,
2751
+ archive: {
2752
+ bytes: Buffer.from(archiveBase64, "base64"),
2753
+ descriptor: archiveMetadata,
2754
+ base64: archiveBase64,
2755
+ },
2756
+ metrics: archiveMetrics,
2757
+ });
2758
+ workspaceArchiveRef = published.workspaceArchiveRef;
2759
+ result = await persistDrainSnapshot(db, {
2760
+ ...baseInput,
2761
+ workspaceArchiveMeta: archiveMetadata,
2762
+ ...published,
2763
+ ...(checkpointArtifactId ? { checkpointArtifactId } : {}),
2764
+ });
2765
+ if (published.workspaceArchiveRef && objectStorage) {
2766
+ if (!result.wrote) {
2767
+ await deleteUnpublishedWorkspaceArchiveObject(
2768
+ objectStorage,
2769
+ published.workspaceArchiveRef,
2770
+ archiveMetrics,
2771
+ );
2772
+ } else {
2773
+ const afterLease = await readLease(db, row.workspaceId, row.sandboxGroupId);
2774
+ const afterKeys = collectWorkspaceArchiveObjectKeys(
2775
+ (afterLease?.resumeState as Record<string, unknown> | null | undefined) ?? null,
2776
+ );
2777
+ await deleteWorkspaceArchiveObjectKeys(
2778
+ objectStorage,
2779
+ [...priorKeys].filter((key) => !afterKeys.has(key)),
2780
+ ).catch(() => undefined);
2781
+ }
2782
+ }
2783
+ } catch (error) {
2784
+ await deleteUnpublishedWorkspaceArchiveObject(
2785
+ objectStorage,
2786
+ workspaceArchiveRef,
2787
+ archiveMetrics,
2788
+ );
2789
+ throw error;
2790
+ }
2770
2791
  }
2771
2792
  if (!result.wrote && checkpointArtifactId) {
2772
2793
  await markSandboxCheckpointArtifactDeletePending(db, {
@@ -2802,7 +2823,6 @@ async function terminateDrainableBox(
2802
2823
  captureClaim?.providerRequestId ?? attempt.operationId,
2803
2824
  captureDisposition,
2804
2825
  capturePolicy,
2805
- Boolean(objectStorage),
2806
2826
  );
2807
2827
  const terminated = typeof termination === "boolean" ? termination : termination.terminated;
2808
2828
  if (!terminated) {
@@ -2935,7 +2955,6 @@ export async function terminateProviderBox(
2935
2955
  providerCaptureRequestId?: string,
2936
2956
  captureDisposition: DrainCaptureDisposition = "capture_required",
2937
2957
  claimedCapturePolicy?: ProviderWorkspaceCapturePolicy | null,
2938
- diskBackedArchives = false,
2939
2958
  ): Promise<ProviderTerminationOutcome> {
2940
2959
  const durableBackendId = (lease.resumeBackendId ?? lease.backend) as string;
2941
2960
  const backend = sandboxBackendForSdkBackendId(durableBackendId) ?? durableBackendId;
@@ -3128,7 +3147,7 @@ export async function terminateProviderBox(
3128
3147
  // snapshot must re-throw BEFORE any terminate so files are never lost — the next
3129
3148
  // sweep retries, the provider idle-timeout is the backstop. A NotFound here (the
3130
3149
  // box raced gone between resume and persist) is success: nothing to persist.
3131
- let verifiedArchive: VerifiedWorkspaceArchivePayload | undefined;
3150
+ let verifiedArchive: Awaited<ReturnType<typeof captureVerifiedWorkspaceArchive>> | undefined;
3132
3151
  const sessionWorkspacePersistence = (
3133
3152
  session as { state?: { workspacePersistence?: unknown } } | undefined
3134
3153
  )?.state?.workspacePersistence;
@@ -3143,15 +3162,10 @@ export async function terminateProviderBox(
3143
3162
  : null;
3144
3163
  try {
3145
3164
  if (captureDisposition !== "archive_published" && session?.persistWorkspace) {
3146
- const capture = captureWorkspaceArchiveForStorage(
3147
- session,
3148
- Date.now(),
3149
- {
3150
- requestId: providerCaptureRequestId ?? randomUUID(),
3151
- strategy: liveCapturePolicy.strategy,
3152
- },
3153
- diskBackedArchives,
3154
- );
3165
+ const capture = captureVerifiedWorkspaceArchive(session, Date.now(), {
3166
+ requestId: providerCaptureRequestId ?? randomUUID(),
3167
+ strategy: liveCapturePolicy.strategy,
3168
+ });
3155
3169
  verifiedArchive = await awaitProviderCaptureWithLatePublication({
3156
3170
  capture,
3157
3171
  timeoutMs: settings.sandboxSnapshotTimeoutMs,
@@ -3165,7 +3179,7 @@ export async function terminateProviderBox(
3165
3179
  publishLate: async (archive) => {
3166
3180
  try {
3167
3181
  const result = await persistArchive(
3168
- archive.kind === "host_spool" ? archive : archive.base64,
3182
+ archive.base64,
3169
3183
  archive.descriptor,
3170
3184
  session,
3171
3185
  checkpointBinding,
@@ -3183,8 +3197,6 @@ export async function terminateProviderBox(
3183
3197
  ...logIdentity,
3184
3198
  error: error instanceof Error ? error.message : String(error),
3185
3199
  });
3186
- } finally {
3187
- await disposeWorkspaceArchive(archive);
3188
3200
  }
3189
3201
  },
3190
3202
  observeLateFailure: (error) => {
@@ -3225,17 +3237,12 @@ export async function terminateProviderBox(
3225
3237
  ...logIdentity,
3226
3238
  });
3227
3239
  } else if (verifiedArchive) {
3228
- let wrote: boolean;
3229
- try {
3230
- ({ wrote } = await persistArchive(
3231
- verifiedArchive.kind === "host_spool" ? verifiedArchive : verifiedArchive.base64,
3232
- verifiedArchive.descriptor,
3233
- session,
3234
- checkpointBinding,
3235
- ));
3236
- } finally {
3237
- await disposeWorkspaceArchive(verifiedArchive);
3238
- }
3240
+ const { wrote } = await persistArchive(
3241
+ verifiedArchive.base64,
3242
+ verifiedArchive.descriptor,
3243
+ session,
3244
+ checkpointBinding,
3245
+ );
3239
3246
  if (!wrote) {
3240
3247
  observability.info(
3241
3248
  "sandbox reaper: lease re-armed during persist — leaving box RUNNING (no terminate)",
@@ -413,7 +413,7 @@ export function recordWorkerDeathRecoveryMetrics(
413
413
  }
414
414
 
415
415
  export class TurnLifecycleMetrics {
416
- private readonly attempts = new Map<string, { startedAt: number; lastProgressAt: number }>();
416
+ private readonly turns = new Map<string, { startedAt: number; lastProgressAt: number }>();
417
417
  private timer: ReturnType<typeof setInterval> | null = null;
418
418
 
419
419
  constructor(
@@ -424,46 +424,42 @@ export class TurnLifecycleMetrics {
424
424
  } = {},
425
425
  ) {}
426
426
 
427
- start(input: { attemptId: string }): void {
427
+ start(turnId: string): void {
428
428
  const now = this.now();
429
- this.attempts.set(input.attemptId, { startedAt: now, lastProgressAt: now });
429
+ this.turns.set(turnId, { startedAt: now, lastProgressAt: now });
430
430
  this.ensureTimer();
431
431
  this.refreshGauges();
432
432
  }
433
433
 
434
- progress(input: { attemptId: string }): void {
435
- const attempt = this.attempts.get(input.attemptId);
436
- if (!attempt) return;
437
- attempt.lastProgressAt = this.now();
434
+ progress(turnId: string): void {
435
+ const turn = this.turns.get(turnId);
436
+ if (!turn) return;
437
+ turn.lastProgressAt = this.now();
438
438
  }
439
439
 
440
- finish(input: {
441
- attemptId: string;
442
- outcome: TurnOutcome | null;
443
- durationSeconds?: number;
444
- }): void {
445
- const startedAt = this.attempts.get(input.attemptId)?.startedAt;
440
+ finish(turnId: string, outcome: TurnOutcome | null, durationSeconds?: number): void {
441
+ const startedAt = this.turns.get(turnId)?.startedAt;
446
442
  if (startedAt !== undefined) {
447
- this.attempts.delete(input.attemptId);
443
+ this.turns.delete(turnId);
448
444
  }
449
- if (input.outcome) {
445
+ if (outcome) {
450
446
  const observedDuration =
451
- input.durationSeconds ??
447
+ durationSeconds ??
452
448
  (startedAt === undefined ? 0 : Math.max(0, (this.now() - startedAt) / 1000));
453
449
  this.observability.incrementCounter({
454
450
  name: "opengeni_turns_total",
455
451
  help: "Total agent turns by terminal outcome.",
456
- labels: { outcome: input.outcome },
452
+ labels: { outcome },
457
453
  });
458
454
  this.observability.observeHistogram({
459
455
  name: "opengeni_turn_duration_seconds",
460
456
  help: "Agent turn duration in seconds by terminal outcome.",
461
- labels: { outcome: input.outcome },
457
+ labels: { outcome },
462
458
  value: observedDuration,
463
459
  });
464
460
  }
465
461
  this.refreshGauges();
466
- if (this.attempts.size === 0) {
462
+ if (this.turns.size === 0) {
467
463
  this.stopTimer();
468
464
  }
469
465
  }
@@ -471,42 +467,42 @@ export class TurnLifecycleMetrics {
471
467
  refreshGauges(): void {
472
468
  this.observability.setGauge({
473
469
  name: "opengeni_turns_inflight",
474
- help: "Current number of in-flight physical agent-turn attempts in this worker process.",
475
- value: this.attempts.size,
470
+ help: "Current number of in-flight agent turns in this worker process.",
471
+ value: this.turns.size,
476
472
  });
477
473
  this.observability.setGauge({
478
474
  name: "opengeni_turn_oldest_inflight_age_seconds",
479
- help: "Age in seconds of the oldest in-flight physical agent-turn attempt in this worker process.",
475
+ help: "Age in seconds of the oldest in-flight agent turn in this worker process.",
480
476
  value: this.oldestInflightAgeSeconds(),
481
477
  });
482
478
  this.observability.setGauge({
483
479
  name: "opengeni_turn_oldest_no_progress_age_seconds",
484
- help: "Seconds since durable progress for the least recently progressing in-flight physical agent-turn attempt.",
480
+ help: "Seconds since durable progress for the least recently progressing in-flight turn.",
485
481
  value: this.oldestNoProgressAgeSeconds(),
486
482
  });
487
483
  }
488
484
 
489
485
  stop(): void {
490
- this.attempts.clear();
486
+ this.turns.clear();
491
487
  this.refreshGauges();
492
488
  this.stopTimer();
493
489
  }
494
490
 
495
491
  private oldestInflightAgeSeconds(): number {
496
- if (this.attempts.size === 0) {
492
+ if (this.turns.size === 0) {
497
493
  return 0;
498
494
  }
499
495
  let oldest = Number.POSITIVE_INFINITY;
500
- for (const { startedAt } of this.attempts.values()) {
496
+ for (const { startedAt } of this.turns.values()) {
501
497
  oldest = Math.min(oldest, startedAt);
502
498
  }
503
499
  return Math.max(0, (this.now() - oldest) / 1000);
504
500
  }
505
501
 
506
502
  private oldestNoProgressAgeSeconds(): number {
507
- if (this.attempts.size === 0) return 0;
503
+ if (this.turns.size === 0) return 0;
508
504
  let leastRecentProgress = Number.POSITIVE_INFINITY;
509
- for (const { lastProgressAt } of this.attempts.values()) {
505
+ for (const { lastProgressAt } of this.turns.values()) {
510
506
  leastRecentProgress = Math.min(leastRecentProgress, lastProgressAt);
511
507
  }
512
508
  return Math.max(0, (this.now() - leastRecentProgress) / 1000);