@opengeni/worker-bundle 0.27.3 → 0.28.3-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.
Files changed (37) hide show
  1. package/dist/activities/agent-turn/model-usage.d.ts +1 -1
  2. package/dist/activities/mcp-credentials.d.ts +3 -1
  3. package/dist/activities/sandbox-lease.d.ts +4 -4
  4. package/dist/activities/workspace-capture.d.ts +12 -1
  5. package/dist/{activities-control-WZRFEIJH.js → activities-control-JUBMDBDZ.js} +58 -73
  6. package/dist/activities-control-JUBMDBDZ.js.map +1 -0
  7. package/dist/{activities-turn-V7J34ZOF.js → activities-turn-I2ZKGZE6.js} +268 -187
  8. package/dist/activities-turn-I2ZKGZE6.js.map +1 -0
  9. package/dist/{chunk-BARTLVBK.js → chunk-A4XQWVZK.js} +25 -25
  10. package/dist/chunk-A4XQWVZK.js.map +1 -0
  11. package/dist/{chunk-C54VDT6I.js → chunk-ZNIKZQMN.js} +148 -111
  12. package/dist/chunk-ZNIKZQMN.js.map +1 -0
  13. package/dist/index.js +2 -2
  14. package/dist/observability-metrics.d.ts +12 -4
  15. package/dist/sandbox-archive-storage.d.ts +15 -3
  16. package/dist/sandbox-resume.d.ts +1 -1
  17. package/package.json +17 -17
  18. package/src/activities/agent-turn/agent-build.ts +30 -1
  19. package/src/activities/agent-turn/claim.ts +3 -3
  20. package/src/activities/agent-turn/finalization.ts +62 -41
  21. package/src/activities/agent-turn/model-usage.ts +10 -1
  22. package/src/activities/agent-turn/skill-read.ts +2 -21
  23. package/src/activities/agent-turn/stream-attempt.ts +3 -2
  24. package/src/activities/agent-turn/tool-environment.ts +52 -10
  25. package/src/activities/goals.ts +2 -0
  26. package/src/activities/mcp-credentials.ts +42 -2
  27. package/src/activities/sandbox-lease.ts +64 -71
  28. package/src/activities/scheduled-tasks.ts +8 -6
  29. package/src/activities/workspace-capture.ts +148 -126
  30. package/src/observability-metrics.ts +28 -24
  31. package/src/pr-review-credentials.ts +2 -0
  32. package/src/sandbox-archive-storage.ts +82 -18
  33. package/src/sandbox-resume.ts +79 -63
  34. package/dist/activities-control-WZRFEIJH.js.map +0 -1
  35. package/dist/activities-turn-V7J34ZOF.js.map +0 -1
  36. package/dist/chunk-BARTLVBK.js.map +0 -1
  37. package/dist/chunk-C54VDT6I.js.map +0 -1
@@ -86,7 +86,7 @@ import {
86
86
  withFirstPartyTools,
87
87
  wrapLazyTurnBoxWithRouting,
88
88
  wrapTurnBoxWithRouting
89
- } from "./chunk-C54VDT6I.js";
89
+ } from "./chunk-ZNIKZQMN.js";
90
90
  import {
91
91
  StreamTimingMetrics,
92
92
  makeMachineOpObserver,
@@ -112,7 +112,7 @@ import {
112
112
  recordTurnWorkerPreparationTotal,
113
113
  runtimeMetricsHooksForObservability,
114
114
  turnLifecycleMetricsFor
115
- } from "./chunk-BARTLVBK.js";
115
+ } from "./chunk-A4XQWVZK.js";
116
116
 
117
117
  // src/activities-turn.ts
118
118
  import { createProductionAgentRuntime, summarizeForCompaction as summarizeForCompaction2 } from "@opengeni/runtime";
@@ -3550,6 +3550,7 @@ function createTurnContext(input) {
3550
3550
  // src/activities/agent-turn/finalization.ts
3551
3551
  import {
3552
3552
  commitSessionAttemptQuiescence,
3553
+ listPendingSessionTurns,
3553
3554
  recordCodexAccountUsageWithWakeTargets,
3554
3555
  releaseCodexCredentialLease,
3555
3556
  releaseXaiCredentialLease,
@@ -3713,6 +3714,50 @@ async function readCaptureRepository(svc, root) {
3713
3714
  }
3714
3715
  }
3715
3716
  var loggedStorageNullOnce = false;
3717
+ async function captureWhileIdle(input) {
3718
+ if (input.signal?.aborted) return;
3719
+ const controller = new AbortController();
3720
+ const signal = input.signal ? AbortSignal.any([input.signal, controller.signal]) : controller.signal;
3721
+ let checking = false;
3722
+ let stopped = false;
3723
+ const check = async () => {
3724
+ if (checking || stopped || signal.aborted) return;
3725
+ checking = true;
3726
+ try {
3727
+ let timer2;
3728
+ let onAbort;
3729
+ try {
3730
+ const pending = await Promise.race([
3731
+ input.hasPendingWork(),
3732
+ new Promise((resolve) => {
3733
+ onAbort = () => resolve(true);
3734
+ signal.addEventListener("abort", onAbort, { once: true });
3735
+ timer2 = setTimeout(() => resolve(true), 1e3);
3736
+ })
3737
+ ]);
3738
+ if (pending) controller.abort();
3739
+ } finally {
3740
+ clearTimeout(timer2);
3741
+ if (onAbort) signal.removeEventListener("abort", onAbort);
3742
+ }
3743
+ } catch {
3744
+ controller.abort();
3745
+ } finally {
3746
+ checking = false;
3747
+ }
3748
+ };
3749
+ await check();
3750
+ if (signal.aborted) return;
3751
+ const timer = setInterval(() => void check(), 250);
3752
+ timer.unref?.();
3753
+ try {
3754
+ await input.capture(signal);
3755
+ } finally {
3756
+ stopped = true;
3757
+ clearInterval(timer);
3758
+ controller.abort();
3759
+ }
3760
+ }
3716
3761
  async function captureWorkspaceRevision(input) {
3717
3762
  const { observability } = input;
3718
3763
  const stage = { value: "preflight" };
@@ -3755,13 +3800,23 @@ async function captureWorkspaceRevision(input) {
3755
3800
  name: "opengeni_workspace_captures_inflight",
3756
3801
  help: "Current workspace-capture operations still resident in this worker process."
3757
3802
  });
3803
+ const ownership = { keys: /* @__PURE__ */ new Set(), retain: false };
3758
3804
  const capture = runCapture(
3759
3805
  input,
3760
3806
  { ...input, objectStorage: input.objectStorage },
3761
3807
  startedAt,
3762
3808
  controller.signal,
3763
- stage
3764
- ).finally(() => {
3809
+ stage,
3810
+ ownership
3811
+ ).finally(async () => {
3812
+ if (!ownership.retain && ownership.keys.size > 0) {
3813
+ await safeDelete(
3814
+ input.objectStorage,
3815
+ [...ownership.keys],
3816
+ observability,
3817
+ new AbortController().signal
3818
+ );
3819
+ }
3765
3820
  observability.incrementGauge({
3766
3821
  name: "opengeni_workspace_captures_inflight",
3767
3822
  help: "Current workspace-capture operations still resident in this worker process.",
@@ -3788,13 +3843,17 @@ async function captureWorkspaceRevision(input) {
3788
3843
  });
3789
3844
  return;
3790
3845
  }
3791
- observability.warn("workspace capture failed \u2014 turn outcome unaffected", {
3792
- "opengeni.session_id": input.sessionId,
3793
- "opengeni.turn_id": input.turnId ?? "",
3794
- "error.message": error instanceof Error ? error.message : String(error),
3795
- "workspace_capture.duration_ms": Date.now() - startedAt,
3796
- "workspace_capture.stage": stage.value
3797
- });
3846
+ const reason = controller.signal.aborted ? "deadline_exceeded" : isBoxExitingError(error) ? "sandbox_exited" : "operation_failed";
3847
+ observability.warn(
3848
+ `workspace capture failed \u2014 stage=${stage.value} reason=${reason} durationMs=${Date.now() - startedAt}; review cache not updated`,
3849
+ {
3850
+ "opengeni.session_id": input.sessionId,
3851
+ "opengeni.turn_id": input.turnId ?? "",
3852
+ "error.message": error instanceof Error ? error.message : String(error),
3853
+ "workspace_capture.duration_ms": Date.now() - startedAt,
3854
+ "workspace_capture.stage": stage.value
3855
+ }
3856
+ );
3798
3857
  observability.incrementCounter({
3799
3858
  name: "opengeni_workspace_capture_total",
3800
3859
  labels: { result: "failed" }
@@ -3805,7 +3864,8 @@ async function captureWorkspaceRevision(input) {
3805
3864
  controller.abort();
3806
3865
  }
3807
3866
  }
3808
- async function runCapture(input, ctx, startedAt, signal, stage) {
3867
+ async function runCapture(input, ctx, startedAt, signal, stage, ownership) {
3868
+ const ownedKeys = ownership.keys;
3809
3869
  const { observability } = input;
3810
3870
  const storage = ctx.objectStorage;
3811
3871
  const keepN = input.keepLatest ?? KEEP_LATEST_REVISIONS;
@@ -3892,6 +3952,13 @@ async function runCapture(input, ctx, startedAt, signal, stage) {
3892
3952
  }
3893
3953
  const files = [];
3894
3954
  const blobKeys = /* @__PURE__ */ new Set();
3955
+ const captureId = crypto.randomUUID();
3956
+ const retainedRefs = retainedCaptureBlobRefs(
3957
+ input.workspaceId,
3958
+ input.sessionId,
3959
+ prev?.blobKeys ?? []
3960
+ );
3961
+ const storedKeys = new Set(retainedRefs.values());
3895
3962
  let totalBytes = 0;
3896
3963
  let tooLargeCount = 0;
3897
3964
  let binaryCount = 0;
@@ -3921,6 +3988,7 @@ async function runCapture(input, ctx, startedAt, signal, stage) {
3921
3988
  });
3922
3989
  throwIfCaptureAborted(signal);
3923
3990
  } catch (error) {
3991
+ if (signal.aborted) throw error;
3924
3992
  const boxExiting = classifyCaptureEntryError(error);
3925
3993
  if (boxExiting) throw boxExiting;
3926
3994
  continue;
@@ -3942,7 +4010,7 @@ async function runCapture(input, ctx, startedAt, signal, stage) {
3942
4010
  }
3943
4011
  const bytes = Buffer.from(read.content, "base64");
3944
4012
  const hash = sha256(bytes);
3945
- const contentRef = blobKey(input.workspaceId, input.sessionId, hash);
4013
+ const contentRef = retainedRefs.get(hash) ?? blobKey(input.workspaceId, input.sessionId, `${captureId}/${hash}`);
3946
4014
  if (read.isBinary) binaryCount += 1;
3947
4015
  if (!blobKeys.has(contentRef)) {
3948
4016
  totalBytes += bytes.byteLength;
@@ -3959,6 +4027,17 @@ async function runCapture(input, ctx, startedAt, signal, stage) {
3959
4027
  }
3960
4028
  }
3961
4029
  blobKeys.add(contentRef);
4030
+ if (!storedKeys.has(contentRef)) {
4031
+ stage.value = "content_upload";
4032
+ ownedKeys.add(contentRef);
4033
+ await storage.putObject({
4034
+ key: contentRef,
4035
+ contentType: "application/octet-stream",
4036
+ body: bytes
4037
+ });
4038
+ throwIfCaptureAborted(signal);
4039
+ storedKeys.add(contentRef);
4040
+ }
3962
4041
  files.push({
3963
4042
  // baseHash (git HEAD blob sha) is intentionally null in M1: the wake-on-edit
3964
4043
  // flush guard (M3/C2) compares live content against the after-image `hash`,
@@ -3975,7 +4054,7 @@ async function runCapture(input, ctx, startedAt, signal, stage) {
3975
4054
  deleted: false
3976
4055
  });
3977
4056
  }
3978
- let fingerprint = changeFingerprint(repos, files);
4057
+ const fingerprint = changeFingerprint(repos, files);
3979
4058
  if (prev && prev.stats.fingerprint === fingerprint) {
3980
4059
  observability.incrementCounter({
3981
4060
  name: "opengeni_workspace_capture_total",
@@ -3986,102 +4065,9 @@ async function runCapture(input, ctx, startedAt, signal, stage) {
3986
4065
  stage.value = "tree_index";
3987
4066
  const tree = await buildTreeIndex(svc, startedAt, signal);
3988
4067
  throwIfCaptureAborted(signal);
3989
- const turnKey = input.turnId;
4068
+ const turnKey = `${input.turnId}/${captureId}`;
3990
4069
  const treeKey = `workspace-captures/${input.workspaceId}/${input.sessionId}/trees/${turnKey}.json`;
3991
4070
  const manifestKey = `workspace-captures/${input.workspaceId}/${input.sessionId}/manifests/${turnKey}.json`;
3992
- const uploadedKeys = /* @__PURE__ */ new Set();
3993
- blobKeys.clear();
3994
- totalBytes = 0;
3995
- let refreshedFiles = 0;
3996
- const omittedFiles = /* @__PURE__ */ new Set();
3997
- for (const file of files) {
3998
- stage.value = "content_upload";
3999
- throwIfCaptureAborted(signal);
4000
- if (file.deleted || file.tooLarge || !file.contentRef || !file.hash) {
4001
- continue;
4002
- }
4003
- let reread;
4004
- try {
4005
- reread = await svc.fsRead({
4006
- path: file.path,
4007
- encoding: "base64",
4008
- maxBytes: PER_FILE_CONTENT_GUARD_BYTES
4009
- });
4010
- throwIfCaptureAborted(signal);
4011
- } catch (error) {
4012
- if (signal.aborted) throw error;
4013
- const boxExiting = classifyCaptureEntryError(error);
4014
- if (boxExiting) throw boxExiting;
4015
- omittedFiles.add(file.path);
4016
- refreshedFiles += 1;
4017
- continue;
4018
- }
4019
- if (reread.truncated) {
4020
- file.hash = null;
4021
- file.contentRef = null;
4022
- file.sizeBytes = reread.sizeBytes;
4023
- file.isBinary = reread.isBinary;
4024
- file.tooLarge = true;
4025
- refreshedFiles += 1;
4026
- continue;
4027
- }
4028
- const bytes = Buffer.from(reread.content, "base64");
4029
- const hash = sha256(bytes);
4030
- const contentRef = blobKey(input.workspaceId, input.sessionId, hash);
4031
- if (hash !== file.hash || bytes.byteLength !== file.sizeBytes || reread.isBinary !== file.isBinary) {
4032
- file.hash = hash;
4033
- file.contentRef = contentRef;
4034
- file.sizeBytes = bytes.byteLength;
4035
- file.isBinary = reread.isBinary;
4036
- refreshedFiles += 1;
4037
- }
4038
- if (!blobKeys.has(contentRef)) {
4039
- totalBytes += bytes.byteLength;
4040
- if (totalBytes > WHOLE_CAPTURE_GUARD_BYTES) {
4041
- observability.warn("workspace capture skipped \u2014 whole-capture guard tripped", {
4042
- "opengeni.session_id": input.sessionId,
4043
- "workspace_capture.total_bytes": totalBytes
4044
- });
4045
- observability.incrementCounter({
4046
- name: "opengeni_workspace_capture_total",
4047
- labels: { result: "guard_tripped" }
4048
- });
4049
- return;
4050
- }
4051
- }
4052
- blobKeys.add(contentRef);
4053
- if (!uploadedKeys.has(contentRef)) {
4054
- await storage.putObject({
4055
- key: contentRef,
4056
- contentType: "application/octet-stream",
4057
- body: bytes
4058
- });
4059
- throwIfCaptureAborted(signal);
4060
- uploadedKeys.add(contentRef);
4061
- }
4062
- }
4063
- if (omittedFiles.size > 0) {
4064
- for (let index = files.length - 1; index >= 0; index -= 1) {
4065
- const file = files[index];
4066
- if (file && omittedFiles.has(file.path)) files.splice(index, 1);
4067
- }
4068
- }
4069
- if (refreshedFiles > 0) {
4070
- observability.incrementCounter({
4071
- name: "opengeni_workspace_capture_files_refreshed_total",
4072
- amount: refreshedFiles
4073
- });
4074
- }
4075
- tooLargeCount = files.filter((file) => file.tooLarge).length;
4076
- binaryCount = files.filter((file) => !file.deleted && file.isBinary).length;
4077
- fingerprint = changeFingerprint(repos, files);
4078
- if (prev && prev.stats.fingerprint === fingerprint) {
4079
- observability.incrementCounter({
4080
- name: "opengeni_workspace_capture_total",
4081
- labels: { result: "skipped_empty" }
4082
- });
4083
- return;
4084
- }
4085
4071
  const capturedAt = /* @__PURE__ */ new Date();
4086
4072
  const stats = {
4087
4073
  repoCount: repos.length,
@@ -4118,11 +4104,13 @@ async function runCapture(input, ctx, startedAt, signal, stage) {
4118
4104
  })
4119
4105
  );
4120
4106
  stage.value = "tree_upload";
4107
+ ownedKeys.add(treeKey);
4121
4108
  await storage.putObject({ key: treeKey, contentType: "application/json", body: treeBytes });
4122
4109
  throwIfCaptureAborted(signal);
4123
4110
  stats.durationMs = Date.now() - startedAt;
4124
4111
  const manifestBytes = utf8(JSON.stringify(manifest));
4125
4112
  stage.value = "manifest_upload";
4113
+ ownedKeys.add(manifestKey);
4126
4114
  await storage.putObject({
4127
4115
  key: manifestKey,
4128
4116
  contentType: "application/json",
@@ -4131,6 +4119,7 @@ async function runCapture(input, ctx, startedAt, signal, stage) {
4131
4119
  throwIfCaptureAborted(signal);
4132
4120
  const sizeBytes = totalBytes + treeBytes.byteLength + manifestBytes.byteLength;
4133
4121
  stage.value = "database_commit";
4122
+ ownership.retain = true;
4134
4123
  const inserted = await insertWorkspaceCapture(input.db, {
4135
4124
  accountId: input.accountId,
4136
4125
  workspaceId: input.workspaceId,
@@ -4147,6 +4136,7 @@ async function runCapture(input, ctx, startedAt, signal, stage) {
4147
4136
  stats,
4148
4137
  capturedAt
4149
4138
  });
4139
+ ownership.retain = inserted !== null;
4150
4140
  if (process.env.OPENGENI_TEST_SCENARIO === "sandbox") {
4151
4141
  console.log(
4152
4142
  `[sandbox-e2e] capture commit inserted=${Boolean(inserted)} revision=${revision} epoch=${input.leaseEpoch} group=${input.sandboxGroupId}`
@@ -4164,6 +4154,7 @@ async function runCapture(input, ctx, startedAt, signal, stage) {
4164
4154
  if (input.publish) {
4165
4155
  await input.publish(inserted.events).catch(() => void 0);
4166
4156
  }
4157
+ stage.value = "garbage_collection";
4167
4158
  let gcDeleted = 0;
4168
4159
  try {
4169
4160
  throwIfCaptureAborted(signal);
@@ -4323,6 +4314,16 @@ function utf8(s) {
4323
4314
  function blobKey(workspaceId, sessionId, sha256Hex) {
4324
4315
  return `workspace-captures/${workspaceId}/${sessionId}/blobs/${sha256Hex}`;
4325
4316
  }
4317
+ function retainedCaptureBlobRefs(workspaceId, sessionId, latestKeys) {
4318
+ const prefix = blobKey(workspaceId, sessionId, "");
4319
+ const refs = /* @__PURE__ */ new Map();
4320
+ for (const key of latestKeys) {
4321
+ if (!key.startsWith(prefix)) continue;
4322
+ const match = /^(?:[0-9a-f-]{36}\/)?([0-9a-f]{64})$/.exec(key.slice(prefix.length));
4323
+ if (match?.[1]) refs.set(match[1], key);
4324
+ }
4325
+ return refs;
4326
+ }
4326
4327
  async function safeDelete(storage, keys, observability, signal) {
4327
4328
  for (const key of keys) {
4328
4329
  throwIfCaptureAborted(signal);
@@ -4692,39 +4693,49 @@ async function finalizeTurnAttempt(deps) {
4692
4693
  cancellationRequested: finalizerSignal?.aborted === true,
4693
4694
  deadlineRotationRequested: sandboxState.deadlineRotationRequested
4694
4695
  });
4695
- if (runTurnEndPersistence && attempt.turnId && sandboxState.resolvedSandbox && sandboxState.setupBoxSession && sandboxState.sandboxGroupId) {
4696
+ if (settings.workspaceCaptureEnabled && runTurnEndPersistence && attempt.turnId && sandboxState.resolvedSandbox && sandboxState.setupBoxSession && sandboxState.sandboxGroupId) {
4696
4697
  sandboxState.turnEndCaptureInProgress = true;
4697
- if (sandboxState.snapshotInFlight) {
4698
- await waitForWarmSnapshot(
4699
- sandboxState.snapshotInFlight,
4700
- settings.sandboxSnapshotTimeoutMs,
4701
- finalizerSignal
4702
- );
4703
- }
4704
- const captureEstablished = sandboxState.resolvedSandbox.established;
4705
- await captureWorkspaceRevision({
4706
- db,
4707
- objectStorage,
4708
- settings,
4709
- publish: async (events) => {
4710
- await publishDurableSessionEvents(bus, input.workspaceId, input.sessionId, events);
4711
- },
4712
- session: sandboxState.setupBoxSession,
4713
- openReadSession: async () => await openFreshWorkspaceCaptureSession({
4714
- backendId: captureEstablished.backendId,
4715
- client: captureEstablished.client,
4716
- session: sandboxState.setupBoxSession,
4717
- expectedInstanceId: captureEstablished.instanceId
4718
- }),
4719
- leaseEpoch: sandboxState.resolvedSandbox.leaseEpoch,
4720
- sandboxGroupId: sandboxState.sandboxGroupId,
4721
- accountId: input.accountId,
4722
- workspaceId: input.workspaceId,
4723
- sessionId: input.sessionId,
4724
- turnId: attempt.turnId,
4725
- attemptId: input.attemptId,
4726
- observability,
4727
- ...finalizerSignal ? { signal: finalizerSignal } : {}
4698
+ const captureSandbox = sandboxState.resolvedSandbox;
4699
+ const captureSession = sandboxState.setupBoxSession;
4700
+ const captureGroupId = sandboxState.sandboxGroupId;
4701
+ const captureTurnId = attempt.turnId;
4702
+ await captureWhileIdle({
4703
+ ...finalizerSignal ? { signal: finalizerSignal } : {},
4704
+ hasPendingWork: async () => (await listPendingSessionTurns(db, input.workspaceId, input.sessionId)).some(
4705
+ (turn) => turn.id !== captureTurnId
4706
+ ),
4707
+ capture: async (signal) => {
4708
+ if (sandboxState.snapshotInFlight) {
4709
+ await waitForWarmSnapshot(
4710
+ sandboxState.snapshotInFlight,
4711
+ settings.sandboxSnapshotTimeoutMs,
4712
+ signal
4713
+ );
4714
+ }
4715
+ if (signal.aborted) return;
4716
+ await captureWorkspaceRevision({
4717
+ db,
4718
+ objectStorage,
4719
+ settings,
4720
+ publish: async (events) => publishDurableSessionEvents(bus, input.workspaceId, input.sessionId, events),
4721
+ session: captureSession,
4722
+ openReadSession: async () => openFreshWorkspaceCaptureSession({
4723
+ backendId: captureSandbox.established.backendId,
4724
+ client: captureSandbox.established.client,
4725
+ session: captureSession,
4726
+ expectedInstanceId: captureSandbox.established.instanceId
4727
+ }),
4728
+ leaseEpoch: captureSandbox.leaseEpoch,
4729
+ sandboxGroupId: captureGroupId,
4730
+ accountId: input.accountId,
4731
+ workspaceId: input.workspaceId,
4732
+ sessionId: input.sessionId,
4733
+ turnId: captureTurnId,
4734
+ attemptId: input.attemptId,
4735
+ observability,
4736
+ signal
4737
+ });
4738
+ }
4728
4739
  });
4729
4740
  }
4730
4741
  eventing.toolPreparationClosing = true;
@@ -4879,12 +4890,12 @@ async function finalizeTurnAttempt(deps) {
4879
4890
  status: finalizationError ? "cleanup_failed" : control.activityStatus,
4880
4891
  durationSeconds
4881
4892
  });
4882
- if (attempt.turnId && control.activityStatus !== "unknown") {
4883
- turnLifecycleMetricsFor(observability).finish(
4884
- attempt.turnId,
4885
- control.turnMetricOutcome,
4893
+ if (attempt.turnId) {
4894
+ turnLifecycleMetricsFor(observability).finish({
4895
+ attemptId: input.attemptId,
4896
+ outcome: control.turnMetricOutcome,
4886
4897
  durationSeconds
4887
- );
4898
+ });
4888
4899
  }
4889
4900
  activitySpan.end({
4890
4901
  attributes: {
@@ -4995,8 +5006,13 @@ function createCompactionModelUsageEventState(claimedSourceKeys = /* @__PURE__ *
4995
5006
  return { usageCount: 0, claimedSourceKeys };
4996
5007
  }
4997
5008
  var createSessionTitleModelUsageEventState = createCompactionModelUsageEventState;
4998
- function modelResponseContextSignal(state) {
4999
- return state.contextSignal;
5009
+ function modelResponseContextSignal(state, responseCountBeforeStream = 0) {
5010
+ const signal = state.contextSignal;
5011
+ if (!signal || signal.revision <= responseCountBeforeStream) return null;
5012
+ return {
5013
+ revision: signal.revision - responseCountBeforeStream,
5014
+ totalTokens: signal.totalTokens
5015
+ };
5000
5016
  }
5001
5017
  function assertModelResponseLatencyMode(input) {
5002
5018
  if (input.requested === "standard") {
@@ -8250,7 +8266,7 @@ async function runTurnStreamAttempt(deps) {
8250
8266
  await toolResultSpill.materializeDeferred();
8251
8267
  }
8252
8268
  } : {},
8253
- contextCompactionSignal: () => modelResponseContextSignal(modelResponseState),
8269
+ contextCompactionSignal: () => modelResponseContextSignal(modelResponseState, responseCountBeforeStream),
8254
8270
  contextCompactionRequested: () => isSessionCompactionRequested(db, input.workspaceId, input.sessionId),
8255
8271
  onModelPreparationPhase: (measurement) => {
8256
8272
  recordTurnStartupPhase(observability, {
@@ -8427,7 +8443,7 @@ async function runTurnStreamAttempt(deps) {
8427
8443
  currentToolBatchCallIds = /* @__PURE__ */ new Set();
8428
8444
  currentToolBatchCompletedCallIds = /* @__PURE__ */ new Set();
8429
8445
  await historySink.reconcileConversationTruth();
8430
- turnLifecycleMetricsFor(observability).progress(attempt.turnId);
8446
+ turnLifecycleMetricsFor(observability).progress({ attemptId: input.attemptId });
8431
8447
  modelCheckpointMemoryCollector.schedule(observability);
8432
8448
  try {
8433
8449
  await ensureRunAllowed(
@@ -9535,7 +9551,7 @@ async function claimTurnAttempt(deps) {
9535
9551
  });
9536
9552
  return attached.action === "claimed" && attached.turn.id === turn.id;
9537
9553
  };
9538
- turnLifecycleMetricsFor(observability).start(attempt.turnId);
9554
+ turnLifecycleMetricsFor(observability).start({ attemptId: input.attemptId });
9539
9555
  await waitForTurnOperation(
9540
9556
  ensureRunAllowed(
9541
9557
  capabilitySettings,
@@ -9613,7 +9629,7 @@ async function claimTurnAttempt(deps) {
9613
9629
  }
9614
9630
  recordCanonicalStartupMilestones(appended.canonicalStartupMilestones);
9615
9631
  if (inputs.length > 0) {
9616
- turnLifecycleMetricsFor(observability).progress(attempt.turnId);
9632
+ turnLifecycleMetricsFor(observability).progress({ attemptId: input.attemptId });
9617
9633
  }
9618
9634
  activityContext?.heartbeat({
9619
9635
  ...heartbeatDetails,
@@ -9664,7 +9680,7 @@ async function claimTurnAttempt(deps) {
9664
9680
  return false;
9665
9681
  }
9666
9682
  recordCanonicalStartupMilestones(result.canonicalStartupMilestones);
9667
- turnLifecycleMetricsFor(observability).progress(attempt.turnId);
9683
+ turnLifecycleMetricsFor(observability).progress({ attemptId: input.attemptId });
9668
9684
  await publishDurableSessionEvents4(bus, input.workspaceId, input.sessionId, result.events);
9669
9685
  if (inputSettlement.turnStatus === "requires_action") {
9670
9686
  await deliverChildRequiresActionToParent(
@@ -14287,6 +14303,8 @@ async function prepareRunCredentials(deps) {
14287
14303
  import { getWorkspaceConnectionModelRestrictions } from "@opengeni/db";
14288
14304
  import {
14289
14305
  beginConnectorActionExecution,
14306
+ getExternalLinkTurnAuthorization,
14307
+ getSessionTurnForAttempt as getSessionTurnForAttempt2,
14290
14308
  getWorkspaceVideoGenerationPolicy,
14291
14309
  listSkillDescriptors as listSkillDescriptors2,
14292
14310
  completeConnectorActionExecution,
@@ -14922,6 +14940,8 @@ function driveQuery(value) {
14922
14940
  import {
14923
14941
  buildConnectionTokenResolver,
14924
14942
  buildHostConnectionTokenResolver,
14943
+ getSessionTurnForAttempt,
14944
+ authorizeDirectHostMcpUse,
14925
14945
  resolveAcceptedConnectionUse,
14926
14946
  sessionTenancyProductActivated
14927
14947
  } from "@opengeni/db";
@@ -14929,7 +14949,10 @@ import { recordTenancyCompatibilityLaneUse } from "@opengeni/observability";
14929
14949
  var OPENGENI_CONNECTION_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
14930
14950
  function connectionTokenResolverForTurn(input) {
14931
14951
  const hostResolver = input.connectionCredentials?.mcpCredentials;
14932
- const baseResolver = hostResolver ? buildHostConnectionTokenResolver(hostResolver, {
14952
+ const readHostTurn = input.getHostTurnForAttempt ?? getSessionTurnForAttempt;
14953
+ const hostDb = input.db;
14954
+ const nativeResolver = buildConnectionTokenResolver(input.db, input.settings);
14955
+ const configuredResolver = hostResolver ? buildHostConnectionTokenResolver(hostResolver, {
14933
14956
  accountId: input.accountId,
14934
14957
  workspaceId: input.workspaceId,
14935
14958
  sessionId: input.sessionId,
@@ -14939,8 +14962,25 @@ function connectionTokenResolverForTurn(input) {
14939
14962
  executionGeneration: input.turn.executionGeneration,
14940
14963
  initiator: input.turn.initiator,
14941
14964
  initiatorContext: input.turn.initiatorContext,
14942
- surface: "model"
14943
- }) : buildConnectionTokenResolver(input.db, input.settings);
14965
+ surface: "model",
14966
+ // Only immutable captured direct-turn authority is currently supported.
14967
+ // Direct, scheduled and exact causal/child snapshots are validated;
14968
+ // no authority follows merely from a binding or creator identity.
14969
+ authorizeDurableBinding: (request) => authorizeDirectHostMcpUse(hostDb, request),
14970
+ authorizeExecution: async (request) => {
14971
+ if (request.connectionRef.authoritySource !== "host" && (!request.connectionRef.connectionId || OPENGENI_CONNECTION_ID_PATTERN.test(request.connectionRef.connectionId)))
14972
+ return true;
14973
+ if (!request.attemptId) return false;
14974
+ const current = await readHostTurn(
14975
+ hostDb,
14976
+ request.workspaceId,
14977
+ request.sessionId,
14978
+ request.attemptId
14979
+ );
14980
+ return current !== null && current.id === request.turnId && current.executionGeneration === request.executionGeneration;
14981
+ }
14982
+ }) : nativeResolver;
14983
+ const baseResolver = (request) => input.connectionCredentials?.mcpAuthoritySource === "host" && request.connectionRef.authoritySource !== "host" ? nativeResolver(request) : configuredResolver(request);
14944
14984
  return async (request) => {
14945
14985
  if (request.connectionRef.authoritySource === "host") {
14946
14986
  if (hostResolver) return await baseResolver(request);
@@ -14956,7 +14996,7 @@ function connectionTokenResolverForTurn(input) {
14956
14996
  ...request.connectionRef.selectedResources ? { selectedResources: request.connectionRef.selectedResources } : {}
14957
14997
  };
14958
14998
  }
14959
- if (hostResolver && request.connectionRef.connectionId && !OPENGENI_CONNECTION_ID_PATTERN.test(request.connectionRef.connectionId)) {
14999
+ if (hostResolver && input.connectionCredentials?.mcpAuthoritySource !== "host" && request.connectionRef.connectionId && !OPENGENI_CONNECTION_ID_PATTERN.test(request.connectionRef.connectionId)) {
14960
15000
  try {
14961
15001
  return await baseResolver(request);
14962
15002
  } catch {
@@ -16063,10 +16103,14 @@ import {
16063
16103
  resolveCatalogSettings as resolveCatalogSettings2,
16064
16104
  resolveWorkspaceModelSelection,
16065
16105
  withFrozenPersonalConnectionDelegations,
16066
- resolveSessionToolPolicy
16106
+ resolveSessionToolPolicy,
16107
+ hasPermission as hasPermission2
16067
16108
  } from "@opengeni/core";
16068
16109
  import { SandboxChannelAService as SandboxChannelAService3 } from "@opengeni/runtime/sandbox";
16069
16110
  import { sandboxRunAs as sandboxRunAs4 } from "@opengeni/runtime";
16111
+ import {
16112
+ DEFAULT_FIRST_PARTY_MCP_PERMISSIONS as DEFAULT_FIRST_PARTY_MCP_PERMISSIONS2
16113
+ } from "@opengeni/contracts";
16070
16114
 
16071
16115
  // src/activities/agent-turn/turn-sandbox-access.ts
16072
16116
  async function resolveTurnSandboxAccess(sandboxState, sdkOwnedSandboxSession, unavailableMessage) {
@@ -16189,10 +16233,9 @@ import {
16189
16233
 
16190
16234
  // src/activities/agent-turn/skill-read.ts
16191
16235
  import {
16192
- assertSkillRelativePath,
16236
+ listSkillPaths,
16193
16237
  PORTABLE_SKILL_MAX_FILES,
16194
16238
  readSkillFiles,
16195
- SKILL_READ_MAX_OUTPUT_BYTES,
16196
16239
  SKILL_READ_MAX_PATHS
16197
16240
  } from "@opengeni/runtime/skill-library";
16198
16241
  var SKILL_READ_TOOL_NAME = "skill_read";
@@ -16239,7 +16282,7 @@ function createSkillReadAttemptToolDefinition(input) {
16239
16282
  const loaded = await input.load(args.skill);
16240
16283
  const metadata = "files" in loaded ? loaded : null;
16241
16284
  const files = metadata ? metadata.files : loaded;
16242
- const selected = args.listFiles === true ? listSkillPaths(files) : readSkillFiles(files, args.paths);
16285
+ const selected = args.listFiles === true ? listSkillPaths(files, PORTABLE_SKILL_MAX_FILES) : readSkillFiles(files, args.paths);
16243
16286
  const output = {
16244
16287
  ...metadata ? {
16245
16288
  skillId: metadata.skillId,
@@ -16257,22 +16300,6 @@ function createSkillReadAttemptToolDefinition(input) {
16257
16300
  }
16258
16301
  };
16259
16302
  }
16260
- function listSkillPaths(files) {
16261
- if (files.length > PORTABLE_SKILL_MAX_FILES) {
16262
- throw new Error(`Skill inventory exceeds ${PORTABLE_SKILL_MAX_FILES} files.`);
16263
- }
16264
- const seen = /* @__PURE__ */ new Set();
16265
- for (const { path } of files) {
16266
- assertSkillRelativePath(path);
16267
- if (seen.has(path)) throw new Error(`Duplicate stored Skill path: ${path}`);
16268
- seen.add(path);
16269
- }
16270
- const result = { paths: [...seen].sort() };
16271
- if (Buffer.byteLength(JSON.stringify(result), "utf8") > SKILL_READ_MAX_OUTPUT_BYTES) {
16272
- throw new Error("Skill inventory exceeds the read output limit.");
16273
- }
16274
- return result;
16275
- }
16276
16303
 
16277
16304
  // src/activities/agent-turn/skill-search.ts
16278
16305
  import { PublicSkillSearchError } from "@opengeni/core";
@@ -16494,7 +16521,7 @@ function createSkillInstallAttemptToolDefinition(input) {
16494
16521
  // src/activities/agent-turn/skill-transfer.ts
16495
16522
  import { posix } from "path";
16496
16523
  import {
16497
- assertSkillRelativePath as assertSkillRelativePath2,
16524
+ assertSkillRelativePath,
16498
16525
  buildPortableSkillArtifact as buildPortableSkillArtifact2,
16499
16526
  PORTABLE_SKILL_MAX_FILES as PORTABLE_SKILL_MAX_FILES3,
16500
16527
  PORTABLE_SKILL_MAX_FILE_BYTES,
@@ -16518,7 +16545,7 @@ function guardSkillFilesystem(fs, authority) {
16518
16545
  };
16519
16546
  }
16520
16547
  async function checkoutSkillDirectory(fs, directory, files) {
16521
- assertSkillRelativePath2(directory);
16548
+ assertSkillRelativePath(directory);
16522
16549
  const artifact = buildPortableSkillArtifact2(files);
16523
16550
  const parent = posix.dirname(directory);
16524
16551
  if (parent !== ".") await fs.fsMkdir({ path: parent, recursive: true });
@@ -16538,7 +16565,7 @@ async function checkoutSkillDirectory(fs, directory, files) {
16538
16565
  return { directory, fileCount: artifact.files.length };
16539
16566
  }
16540
16567
  async function readSkillDirectory(fs, directory) {
16541
- assertSkillRelativePath2(directory);
16568
+ assertSkillRelativePath(directory);
16542
16569
  const pending = [directory];
16543
16570
  const seen = new Set(pending);
16544
16571
  const files = [];
@@ -16559,7 +16586,7 @@ async function readSkillDirectory(fs, directory) {
16559
16586
  throw new Error(`Skill directory listing is incomplete: ${current}`);
16560
16587
  }
16561
16588
  for (const entry of listing.root.children) {
16562
- assertSkillRelativePath2(entry.path);
16589
+ assertSkillRelativePath(entry.path);
16563
16590
  if (posix.dirname(entry.path) !== current || posix.basename(entry.path) !== entry.name) {
16564
16591
  throw new Error("Skill directory listing returned a path outside the requested directory.");
16565
16592
  }
@@ -17218,6 +17245,19 @@ async function prepareTurnToolRuntime(deps) {
17218
17245
  }
17219
17246
  };
17220
17247
  })() : void 0;
17248
+ const linkedAuthority = await getExternalLinkTurnAuthorization(
17249
+ db,
17250
+ {
17251
+ accountId: input.accountId,
17252
+ workspaceId: input.workspaceId
17253
+ },
17254
+ turn.id
17255
+ );
17256
+ if (linkedAuthority && !linkedAuthority.authorized)
17257
+ throw new Error("Native identity link was revoked");
17258
+ const effectiveFirstPartyPermissions = linkedAuthority ? (session.firstPartyMcpPermissions ?? DEFAULT_FIRST_PARTY_MCP_PERMISSIONS2).filter(
17259
+ (permission) => hasPermission2(linkedAuthority.permissions, permission)
17260
+ ) : session.firstPartyMcpPermissions;
17221
17261
  const selectedFirstPartyMcpTools = allowedFirstPartyMcpToolsForSession(
17222
17262
  runSettings,
17223
17263
  session.firstPartyMcpTools
@@ -17229,11 +17269,17 @@ async function prepareTurnToolRuntime(deps) {
17229
17269
  title: session.title,
17230
17270
  titleSource: session.titleSource,
17231
17271
  firstPartyMcpTools: selectedFirstPartyMcpTools,
17232
- firstPartyMcpPermissions: session.firstPartyMcpPermissions
17272
+ firstPartyMcpPermissions: effectiveFirstPartyPermissions
17233
17273
  }),
17234
17274
  parallelGenerationAvailable: typeof runtime.generateSessionTitle === "function"
17235
17275
  });
17236
- const googleDrivePublicationAllowed = selectedFirstPartyMcpTools.includes("editable_artifact_export") && selectedFirstPartyMcpTools.includes("editable_artifact_export_status") && (!session.firstPartyMcpPermissions?.length || session.firstPartyMcpPermissions.includes("artifacts:read") && session.firstPartyMcpPermissions.includes("artifacts:publish"));
17276
+ const googleDrivePublicationAllowed = selectedFirstPartyMcpTools.includes("editable_artifact_export") && selectedFirstPartyMcpTools.includes("editable_artifact_export_status") && hasPermission2(
17277
+ [...effectiveFirstPartyPermissions ?? DEFAULT_FIRST_PARTY_MCP_PERMISSIONS2],
17278
+ "artifacts:read"
17279
+ ) && hasPermission2(
17280
+ [...effectiveFirstPartyPermissions ?? DEFAULT_FIRST_PARTY_MCP_PERMISSIONS2],
17281
+ "artifacts:publish"
17282
+ );
17237
17283
  const googleDriveConnectorBindings = googleDrivePublicationTool && googleDrivePublicationTarget && googleDrivePublicationAllowed ? [
17238
17284
  {
17239
17285
  modelName: googleDrivePublicationTool.modelName,
@@ -17448,7 +17494,7 @@ async function prepareTurnToolRuntime(deps) {
17448
17494
  attemptId: input.attemptId,
17449
17495
  executionGeneration: attempt.executionGeneration
17450
17496
  },
17451
- ...session.firstPartyMcpPermissions?.length ? { permissions: session.firstPartyMcpPermissions } : {},
17497
+ ...effectiveFirstPartyPermissions ? { permissions: effectiveFirstPartyPermissions } : {},
17452
17498
  selectedTools: selectedFirstPartyMcpTools,
17453
17499
  subjectId: "worker:first-party-mcp",
17454
17500
  subjectLabel: "OpenGeni worker",
@@ -17529,6 +17575,18 @@ async function prepareTurnToolRuntime(deps) {
17529
17575
  ...credentialSubjectId ? { credentialSubjectId } : {},
17530
17576
  ...codexAppsAuth ? { codexAppsAuth } : {},
17531
17577
  resolveCredential,
17578
+ ...linkedAuthority ? {
17579
+ authorizeAttemptExecution: async () => {
17580
+ const current = await getSessionTurnForAttempt2(
17581
+ db,
17582
+ input.workspaceId,
17583
+ input.sessionId,
17584
+ input.attemptId
17585
+ );
17586
+ if (!current || current.id !== turn.id || current.executionGeneration !== attempt.executionGeneration)
17587
+ throw new Error("The linked agent attempt is no longer authorized");
17588
+ }
17589
+ } : {},
17532
17590
  onAuthNeeded: publishToolAuthNeeded,
17533
17591
  materializeConnectorAttachments,
17534
17592
  refreshOwnedCommand: async (commandId) => {
@@ -17555,7 +17613,7 @@ async function prepareTurnToolRuntime(deps) {
17555
17613
  },
17556
17614
  // Manager-style sessions carry a creation-validated permission set
17557
17615
  // for their first-party MCP token; null keeps the fixed default.
17558
- ...session.firstPartyMcpPermissions?.length ? { firstPartyPermissions: session.firstPartyMcpPermissions } : {},
17616
+ ...effectiveFirstPartyPermissions ? { firstPartyPermissions: effectiveFirstPartyPermissions } : {},
17559
17617
  firstPartyTools: titleToolPlan.remoteFirstPartyMcpTools,
17560
17618
  nestedAgentDepth: session.nestedAgentDepth,
17561
17619
  effectiveMaxNestedAgentDepth: session.effectiveMaxNestedAgentDepth,
@@ -17823,7 +17881,9 @@ import {
17823
17881
  getXaiSessionAccountPin as getXaiSessionAccountPin2,
17824
17882
  setXaiSessionAccountPin as setXaiSessionAccountPin2,
17825
17883
  getWorkspaceVideoGenerationPolicy as getWorkspaceVideoGenerationPolicy2,
17826
- loadWorkspaceVercelAiGatewayCredentialLease
17884
+ loadWorkspaceVercelAiGatewayCredentialLease,
17885
+ getExternalLinkTurnAuthorization as getExternalLinkTurnAuthorization2,
17886
+ getSessionTurnForAttempt as getSessionTurnForAttempt3
17827
17887
  } from "@opengeni/db";
17828
17888
  import {
17829
17889
  formatSkillCatalog
@@ -19372,11 +19432,33 @@ async function buildTurnAgent(deps) {
19372
19432
  outcome: "completed",
19373
19433
  durationSeconds: (performance.now() - postToolPreparationStartedAt) / 1e3
19374
19434
  });
19435
+ const linkedToolAuthority = await getExternalLinkTurnAuthorization2(
19436
+ db,
19437
+ {
19438
+ accountId: input.accountId,
19439
+ workspaceId: input.workspaceId
19440
+ },
19441
+ turn.id
19442
+ );
19443
+ if (linkedToolAuthority && !linkedToolAuthority.authorized)
19444
+ throw new Error("Native identity link was revoked");
19375
19445
  const agent = (() => {
19376
19446
  const agentConstructionStartedAt = performance.now();
19377
19447
  let agentConstructionOutcome = "completed";
19378
19448
  try {
19379
19449
  return runtime.buildAgent(eventing.modelRunSettings, runtimeResources, {
19450
+ ...linkedToolAuthority ? {
19451
+ authorizeAttemptExecution: async () => {
19452
+ const current = await getSessionTurnForAttempt3(
19453
+ db,
19454
+ input.workspaceId,
19455
+ input.sessionId,
19456
+ input.attemptId
19457
+ );
19458
+ if (!current || current.id !== turn.id || current.executionGeneration !== turn.executionGeneration)
19459
+ throw new Error("The linked agent attempt is no longer authorized");
19460
+ }
19461
+ } : {},
19380
19462
  ...preparedTools.inputWaitYield ? { inputWaitYield: preparedTools.inputWaitYield } : {},
19381
19463
  reasoningEffort: turn.reasoningEffort,
19382
19464
  latencyMode: turnExecutionPolicy.latencyMode,
@@ -19463,7 +19545,6 @@ async function buildTurnAgent(deps) {
19463
19545
  ),
19464
19546
  onRetainableSessionImageOutput: media.retainSessionImageAtToolBoundary,
19465
19547
  skillCatalog: deps.skillCatalog,
19466
- serverSkillReading: true,
19467
19548
  ...!structuredWorkspacePolicyActive && workspaceAgentInstructions ? { instructionsTemplate: workspaceAgentInstructions } : {},
19468
19549
  ...workspaceGovernance ? { workspaceGovernance } : {},
19469
19550
  ...workspaceMemory ? { workspaceMemory } : {},
@@ -21970,4 +22051,4 @@ export {
21970
22051
  createTurnActivities,
21971
22052
  createTurnActivitiesFromServices
21972
22053
  };
21973
- //# sourceMappingURL=activities-turn-V7J34ZOF.js.map
22054
+ //# sourceMappingURL=activities-turn-I2ZKGZE6.js.map