open-managed-agents 0.1.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 (197) hide show
  1. package/LICENSE +93 -0
  2. package/README.md +248 -0
  3. package/bin/oma.mjs +499 -0
  4. package/dist/scripts/alpha-openai-compatible-fixture.mjs +134 -0
  5. package/dist/scripts/alpha-smoke-models.mjs +92 -0
  6. package/dist/scripts/alpha-smoke.mjs +581 -0
  7. package/dist/scripts/oma-doctor.js +230 -0
  8. package/dist/scripts/oma-models.js +299 -0
  9. package/dist/scripts/oma-workspaces.js +133 -0
  10. package/dist/src/control-plane/admin/auth.js +43 -0
  11. package/dist/src/control-plane/admin/routes.js +76 -0
  12. package/dist/src/control-plane/admin/service.js +102 -0
  13. package/dist/src/control-plane/admission.js +90 -0
  14. package/dist/src/control-plane/agents/routes.js +63 -0
  15. package/dist/src/control-plane/agents/service.js +687 -0
  16. package/dist/src/control-plane/agents/store.js +391 -0
  17. package/dist/src/control-plane/agents/types.js +1 -0
  18. package/dist/src/control-plane/api-constants.js +3 -0
  19. package/dist/src/control-plane/app.js +907 -0
  20. package/dist/src/control-plane/console/auth.js +68 -0
  21. package/dist/src/control-plane/console/static.js +112 -0
  22. package/dist/src/control-plane/deployment-runtime-config.js +328 -0
  23. package/dist/src/control-plane/deployment-runtime-event-coordinator.js +25 -0
  24. package/dist/src/control-plane/deployment-runtime-turn-guard.js +14 -0
  25. package/dist/src/control-plane/deployment-session-coordinator.js +25 -0
  26. package/dist/src/control-plane/deployment-session-output-coordinator.js +27 -0
  27. package/dist/src/control-plane/deployment-storage.js +354 -0
  28. package/dist/src/control-plane/egress/image.js +48 -0
  29. package/dist/src/control-plane/egress/policy.js +571 -0
  30. package/dist/src/control-plane/egress/presets.js +77 -0
  31. package/dist/src/control-plane/egress/proxy.js +72 -0
  32. package/dist/src/control-plane/egress/ssrf.js +106 -0
  33. package/dist/src/control-plane/egress/utils/debug.js +14 -0
  34. package/dist/src/control-plane/egress/vendor/http-proxy.js +335 -0
  35. package/dist/src/control-plane/egress/vendor/mitm-ca.js +226 -0
  36. package/dist/src/control-plane/egress/vendor/mitm-leaf.js +124 -0
  37. package/dist/src/control-plane/egress/vendor/parent-proxy.js +438 -0
  38. package/dist/src/control-plane/egress/vendor/request-filter.js +105 -0
  39. package/dist/src/control-plane/egress/vendor/sandbox-config.js +1 -0
  40. package/dist/src/control-plane/egress/vendor/tls-terminate-proxy.js +269 -0
  41. package/dist/src/control-plane/environments/routes.js +54 -0
  42. package/dist/src/control-plane/environments/service.js +112 -0
  43. package/dist/src/control-plane/environments/store.js +132 -0
  44. package/dist/src/control-plane/environments/types.js +1 -0
  45. package/dist/src/control-plane/errors.js +81 -0
  46. package/dist/src/control-plane/events/broadcaster.js +217 -0
  47. package/dist/src/control-plane/events/constants.js +2 -0
  48. package/dist/src/control-plane/events/persist.js +39 -0
  49. package/dist/src/control-plane/events/request.js +190 -0
  50. package/dist/src/control-plane/events/routes.js +159 -0
  51. package/dist/src/control-plane/events/runtime-helpers.js +134 -0
  52. package/dist/src/control-plane/events/service.js +1985 -0
  53. package/dist/src/control-plane/events/session-guards.js +29 -0
  54. package/dist/src/control-plane/events/sse.js +6 -0
  55. package/dist/src/control-plane/events/store.js +772 -0
  56. package/dist/src/control-plane/events/tool-persistence.js +102 -0
  57. package/dist/src/control-plane/events/types.js +21 -0
  58. package/dist/src/control-plane/files/routes.js +121 -0
  59. package/dist/src/control-plane/files/service.js +65 -0
  60. package/dist/src/control-plane/files/store-common.js +87 -0
  61. package/dist/src/control-plane/files/store-local.js +488 -0
  62. package/dist/src/control-plane/files/store-memory.js +270 -0
  63. package/dist/src/control-plane/files/store.js +2 -0
  64. package/dist/src/control-plane/files/types.js +7 -0
  65. package/dist/src/control-plane/http.js +25 -0
  66. package/dist/src/control-plane/ids.js +43 -0
  67. package/dist/src/control-plane/logging.js +263 -0
  68. package/dist/src/control-plane/models/auth-storage-backend.js +238 -0
  69. package/dist/src/control-plane/models/catalog.js +78 -0
  70. package/dist/src/control-plane/models/config-security.js +376 -0
  71. package/dist/src/control-plane/models/deployment-config.js +93 -0
  72. package/dist/src/control-plane/models/routes.js +32 -0
  73. package/dist/src/control-plane/models/service.js +138 -0
  74. package/dist/src/control-plane/observability/instruments.js +65 -0
  75. package/dist/src/control-plane/observability/metrics.js +125 -0
  76. package/dist/src/control-plane/observability/routes.js +95 -0
  77. package/dist/src/control-plane/openapi/document.js +282 -0
  78. package/dist/src/control-plane/openapi/routes.js +78 -0
  79. package/dist/src/control-plane/request-idempotency.js +85 -0
  80. package/dist/src/control-plane/secrets/envelope.js +132 -0
  81. package/dist/src/control-plane/secrets/master-key.js +44 -0
  82. package/dist/src/control-plane/secrets/routes.js +20 -0
  83. package/dist/src/control-plane/secrets/service.js +71 -0
  84. package/dist/src/control-plane/secrets/store.js +212 -0
  85. package/dist/src/control-plane/secrets/types.js +3 -0
  86. package/dist/src/control-plane/sessions/pi/custom-tools.js +146 -0
  87. package/dist/src/control-plane/sessions/pi/mcp/bridge.js +527 -0
  88. package/dist/src/control-plane/sessions/pi/mcp/client.js +157 -0
  89. package/dist/src/control-plane/sessions/pi/mcp/credential.js +28 -0
  90. package/dist/src/control-plane/sessions/pi/mcp/fetch.js +69 -0
  91. package/dist/src/control-plane/sessions/pi/mcp/probe.js +202 -0
  92. package/dist/src/control-plane/sessions/pi/mcp/runtime.js +13 -0
  93. package/dist/src/control-plane/sessions/pi/runner.js +1104 -0
  94. package/dist/src/control-plane/sessions/pi/sandbox/cma-glob.js +388 -0
  95. package/dist/src/control-plane/sessions/pi/sandbox/cma-grep.js +124 -0
  96. package/dist/src/control-plane/sessions/pi/sandbox/docker-egress.js +347 -0
  97. package/dist/src/control-plane/sessions/pi/sandbox/docker.js +1589 -0
  98. package/dist/src/control-plane/sessions/pi/sandbox/glob.js +48 -0
  99. package/dist/src/control-plane/sessions/pi/sandbox/image.js +7 -0
  100. package/dist/src/control-plane/sessions/pi/sandbox/microsandbox.js +1677 -0
  101. package/dist/src/control-plane/sessions/pi/sandbox/provider.js +561 -0
  102. package/dist/src/control-plane/sessions/pi/sandbox/selection.js +168 -0
  103. package/dist/src/control-plane/sessions/pi/span-normalizer.js +82 -0
  104. package/dist/src/control-plane/sessions/pi/tool-permissions.js +392 -0
  105. package/dist/src/control-plane/sessions/pi/translator.js +100 -0
  106. package/dist/src/control-plane/sessions/request.js +164 -0
  107. package/dist/src/control-plane/sessions/resources.js +63 -0
  108. package/dist/src/control-plane/sessions/routes.js +88 -0
  109. package/dist/src/control-plane/sessions/serialize.js +24 -0
  110. package/dist/src/control-plane/sessions/service.js +733 -0
  111. package/dist/src/control-plane/sessions/store.js +538 -0
  112. package/dist/src/control-plane/sessions/types.js +1 -0
  113. package/dist/src/control-plane/skills/archive.js +169 -0
  114. package/dist/src/control-plane/skills/routes.js +41 -0
  115. package/dist/src/control-plane/skills/service.js +29 -0
  116. package/dist/src/control-plane/skills/store.js +233 -0
  117. package/dist/src/control-plane/skills/types.js +5 -0
  118. package/dist/src/control-plane/sqlite-transaction.js +15 -0
  119. package/dist/src/control-plane/vaults/mcp-oauth-validate.js +165 -0
  120. package/dist/src/control-plane/vaults/oauth-refresh-ticker.js +43 -0
  121. package/dist/src/control-plane/vaults/oauth-refresh.js +448 -0
  122. package/dist/src/control-plane/vaults/routes.js +66 -0
  123. package/dist/src/control-plane/vaults/service.js +553 -0
  124. package/dist/src/control-plane/vaults/store.js +691 -0
  125. package/dist/src/control-plane/vaults/types.js +1 -0
  126. package/dist/src/control-plane/wake-loop.js +65 -0
  127. package/dist/src/control-plane/wiring.js +92 -0
  128. package/dist/src/control-plane/workspace.js +4 -0
  129. package/dist/src/control-plane/workspaces/store.js +158 -0
  130. package/dist/src/egress-proxy-main.js +103 -0
  131. package/dist/src/main.js +124 -0
  132. package/dist/src/types/agents.js +1 -0
  133. package/dist/src/types/common.js +1 -0
  134. package/dist/src/types/environments.js +1 -0
  135. package/dist/src/types/events.js +121 -0
  136. package/dist/src/types/files.js +1 -0
  137. package/dist/src/types/json.js +19 -0
  138. package/dist/src/types/sessions.js +1 -0
  139. package/dist/ui/managed-agents-console/README.md +52 -0
  140. package/dist/ui/managed-agents-console/docs/agents.md +23 -0
  141. package/dist/ui/managed-agents-console/docs/console.md +23 -0
  142. package/dist/ui/managed-agents-console/docs/dreams.md +5 -0
  143. package/dist/ui/managed-agents-console/docs/environments.md +27 -0
  144. package/dist/ui/managed-agents-console/docs/events.md +19 -0
  145. package/dist/ui/managed-agents-console/docs/files.md +17 -0
  146. package/dist/ui/managed-agents-console/docs/github.md +11 -0
  147. package/dist/ui/managed-agents-console/docs/integrations.md +19 -0
  148. package/dist/ui/managed-agents-console/docs/memory.md +11 -0
  149. package/dist/ui/managed-agents-console/docs/migration.md +17 -0
  150. package/dist/ui/managed-agents-console/docs/multiagent.md +11 -0
  151. package/dist/ui/managed-agents-console/docs/outcomes.md +11 -0
  152. package/dist/ui/managed-agents-console/docs/overview.md +41 -0
  153. package/dist/ui/managed-agents-console/docs/permissions.md +24 -0
  154. package/dist/ui/managed-agents-console/docs/quickstart.md +51 -0
  155. package/dist/ui/managed-agents-console/docs/reference.md +27 -0
  156. package/dist/ui/managed-agents-console/docs/sandbox-reference.md +19 -0
  157. package/dist/ui/managed-agents-console/docs/sandbox-security.md +15 -0
  158. package/dist/ui/managed-agents-console/docs/scheduled-deployments.md +5 -0
  159. package/dist/ui/managed-agents-console/docs/self-hosted-sandboxes.md +15 -0
  160. package/dist/ui/managed-agents-console/docs/session-operations.md +21 -0
  161. package/dist/ui/managed-agents-console/docs/sessions.md +21 -0
  162. package/dist/ui/managed-agents-console/docs/skills.md +15 -0
  163. package/dist/ui/managed-agents-console/docs/tools.md +23 -0
  164. package/dist/ui/managed-agents-console/docs/vaults.md +19 -0
  165. package/dist/ui/managed-agents-console/docs/webhooks.md +11 -0
  166. package/dist/ui/managed-agents-console/index.html +47 -0
  167. package/dist/ui/managed-agents-console/serve.mjs +116 -0
  168. package/dist/ui/managed-agents-console/src/agents-files.jsx +273 -0
  169. package/dist/ui/managed-agents-console/src/api.js +1043 -0
  170. package/dist/ui/managed-agents-console/src/app.jsx +633 -0
  171. package/dist/ui/managed-agents-console/src/auth.jsx +215 -0
  172. package/dist/ui/managed-agents-console/src/console.css +733 -0
  173. package/dist/ui/managed-agents-console/src/data.js +188 -0
  174. package/dist/ui/managed-agents-console/src/detail.jsx +719 -0
  175. package/dist/ui/managed-agents-console/src/docs.jsx +200 -0
  176. package/dist/ui/managed-agents-console/src/environments.jsx +365 -0
  177. package/dist/ui/managed-agents-console/src/forms.jsx +494 -0
  178. package/dist/ui/managed-agents-console/src/icons.jsx +48 -0
  179. package/dist/ui/managed-agents-console/src/skills.jsx +21 -0
  180. package/dist/ui/managed-agents-console/src/sse.js +185 -0
  181. package/dist/ui/managed-agents-console/src/states.jsx +90 -0
  182. package/dist/ui/managed-agents-console/src/tweaks-panel.jsx +541 -0
  183. package/dist/ui/managed-agents-console/src/ui.jsx +190 -0
  184. package/dist/ui/managed-agents-console/src/vaults-data.js +89 -0
  185. package/dist/ui/managed-agents-console/src/vaults.jsx +171 -0
  186. package/dist/ui/managed-agents-console/vendor/babel.min.js +4 -0
  187. package/dist/ui/managed-agents-console/vendor/react-dom.production.min.js +267 -0
  188. package/dist/ui/managed-agents-console/vendor/react.production.min.js +31 -0
  189. package/dist/ui/openapi-docs/VENDOR.md +27 -0
  190. package/dist/ui/openapi-docs/index.html +16 -0
  191. package/dist/ui/openapi-docs/swagger-initializer.js +14 -0
  192. package/dist/ui/openapi-docs/vendor/LICENSE +202 -0
  193. package/dist/ui/openapi-docs/vendor/NOTICE +2 -0
  194. package/dist/ui/openapi-docs/vendor/swagger-ui-bundle.js +2 -0
  195. package/dist/ui/openapi-docs/vendor/swagger-ui-standalone-preset.js +2 -0
  196. package/dist/ui/openapi-docs/vendor/swagger-ui.css +3 -0
  197. package/package.json +53 -0
@@ -0,0 +1,733 @@
1
+ import { createHash } from "node:crypto";
2
+ import { log } from "../logging.js";
3
+ import { EgressPolicyError, parseNetworkingConfig, } from "../egress/policy.js";
4
+ import { ApiError, invalidRequest, notFound, rateLimited, toApiErrorBody } from "../errors.js";
5
+ import { idempotencyCompletion, idempotencyConflictResponse, idempotencyMismatchError, reserveWindow, withIdempotencyReservationHeartbeat, } from "../request-idempotency.js";
6
+ import { RuntimeUnsupportedSessionFileResourcesError } from "../events/types.js";
7
+ import { newFileId, newSessionId, newSessionResourceId, newSessionSkillSnapshotId } from "../ids.js";
8
+ import { parseCreateSession, parseAgentRef } from "./request.js";
9
+ import { resolveBuiltinToolAccessForAgent } from "./pi/tool-permissions.js";
10
+ import { normalizeSessionFileResources, } from "./resources.js";
11
+ import { toManagedSession } from "./serialize.js";
12
+ const MAX_SESSION_FILE_RESOURCES = 10;
13
+ const MAX_SESSION_MOUNTED_BYTES = 50 * 1024 * 1024;
14
+ const DEFAULT_PENDING_SNAPSHOT_CLEANUP_RETRY_DELAY_MS = 30_000;
15
+ const DEFAULT_PENDING_SNAPSHOT_CLEANUP_MAX_ATTEMPTS = 5;
16
+ export class DefaultSessionService {
17
+ store;
18
+ agents;
19
+ environments;
20
+ files;
21
+ maxActiveSessionsPerWorkspace;
22
+ egressCapability;
23
+ vaults;
24
+ skills;
25
+ maxFileResources;
26
+ maxMountedBytes;
27
+ runtime;
28
+ startupSnapshotDeleteSweep;
29
+ startupSnapshotCreateRollbackSweep;
30
+ startedAt = new Date().toISOString();
31
+ deleteSessionRows;
32
+ assertDeletable;
33
+ idempotencyLedger;
34
+ createSessionRowsWithIdempotency;
35
+ pendingSnapshotCleanupRetryDelayMs;
36
+ pendingSnapshotCleanupMaxAttempts;
37
+ onAdmissionRejected;
38
+ modelAvailability;
39
+ pendingSessionCreates = new Map();
40
+ pendingSnapshotDeleteRetryTimers = new Map();
41
+ pendingSnapshotCreateRollbackRetryTimers = new Map();
42
+ constructor(store, agents, environments, files, opts) {
43
+ this.store = store;
44
+ this.agents = agents;
45
+ this.environments = environments;
46
+ this.files = files;
47
+ if (opts.assertDeletable === undefined) {
48
+ throw new Error("DefaultSessionService requires assertDeletable for safe session deletion");
49
+ }
50
+ this.assertDeletable = opts.assertDeletable;
51
+ this.maxActiveSessionsPerWorkspace = opts.maxActiveSessionsPerWorkspace;
52
+ this.onAdmissionRejected = opts.onAdmissionRejected;
53
+ this.modelAvailability = opts.modelAvailability;
54
+ this.maxFileResources = opts.maxFileResources ?? MAX_SESSION_FILE_RESOURCES;
55
+ this.maxMountedBytes = opts.maxMountedBytes ?? MAX_SESSION_MOUNTED_BYTES;
56
+ this.egressCapability = opts.egressCapability;
57
+ this.vaults = opts.vaults;
58
+ this.skills = opts.skills;
59
+ this.runtime = opts.runtime;
60
+ this.deleteSessionRows = opts.deleteSessionRows;
61
+ this.idempotencyLedger = opts.idempotencyLedger;
62
+ this.createSessionRowsWithIdempotency = opts.createSessionRowsWithIdempotency;
63
+ if (this.idempotencyLedger && !this.createSessionRowsWithIdempotency) {
64
+ throw new Error("DefaultSessionService requires createSessionRowsWithIdempotency when idempotencyLedger is configured");
65
+ }
66
+ this.pendingSnapshotCleanupRetryDelayMs =
67
+ opts.pendingSnapshotCleanupRetryDelayMs ??
68
+ DEFAULT_PENDING_SNAPSHOT_CLEANUP_RETRY_DELAY_MS;
69
+ this.pendingSnapshotCleanupMaxAttempts =
70
+ opts.pendingSnapshotCleanupMaxAttempts ??
71
+ DEFAULT_PENDING_SNAPSHOT_CLEANUP_MAX_ATTEMPTS;
72
+ if (this.pendingSnapshotCleanupMaxAttempts < 1) {
73
+ throw invalidRequest("pendingSnapshotCleanupMaxAttempts must be at least 1");
74
+ }
75
+ this.startupSnapshotDeleteSweep = this.sweepPendingInternalSnapshotDeletes().catch((error) => {
76
+ log.warn("snapshot_delete_startup_sweep_failed", { error });
77
+ });
78
+ this.startupSnapshotCreateRollbackSweep =
79
+ this.sweepPendingInternalSnapshotCreateRollbacks({
80
+ createdBefore: this.startedAt,
81
+ }).catch((error) => {
82
+ log.warn("snapshot_create_rollback_startup_sweep_failed", { error });
83
+ });
84
+ }
85
+ async create(workspaceId, input) {
86
+ return this.createInternal(workspaceId, input);
87
+ }
88
+ async createIdempotent(workspaceId, input, idempotency, opts = {}) {
89
+ if (!this.idempotencyLedger) {
90
+ return { status: 200, body: await this.createInternal(workspaceId, input) };
91
+ }
92
+ const reservation = this.idempotencyLedger.reserveIdempotencyKey({
93
+ ...idempotency,
94
+ workspaceId,
95
+ ...reserveWindow(),
96
+ });
97
+ if (reservation.kind === "replay") {
98
+ return {
99
+ status: reservation.responseStatus,
100
+ body: reservation.responseBody,
101
+ };
102
+ }
103
+ if (reservation.kind === "fingerprint_mismatch") {
104
+ throw idempotencyMismatchError();
105
+ }
106
+ if (reservation.kind === "in_progress") {
107
+ return idempotencyConflictResponse(opts.requestId);
108
+ }
109
+ try {
110
+ const session = await this.createInternal(workspaceId, input, {
111
+ idempotency,
112
+ });
113
+ return { status: 200, body: session };
114
+ }
115
+ catch (error) {
116
+ if (shouldReleaseIdempotencyReservation(error)) {
117
+ this.idempotencyLedger.releaseIdempotencyReservation({
118
+ ...idempotency,
119
+ workspaceId,
120
+ });
121
+ throw unwrapIdempotentCreateError(error);
122
+ }
123
+ if (error instanceof ApiError && error.status < 500) {
124
+ const body = toApiErrorBody(error, opts.requestId);
125
+ this.idempotencyLedger.completeIdempotency(idempotencyCompletion(workspaceId, idempotency, {
126
+ status: error.status,
127
+ body,
128
+ }));
129
+ return { status: error.status, body };
130
+ }
131
+ throw error;
132
+ }
133
+ }
134
+ async createInternal(workspaceId, input, opts = {}) {
135
+ // 0113 D9: reserve before the async file-resource preparation below, not
136
+ // just before the row insert. Counting rows alone lets N concurrent
137
+ // creates with resources all pass the cap while none has inserted yet;
138
+ // the in-flight reservation closes that window and bounds the expensive
139
+ // prep itself.
140
+ const release = this.reserveSessionCreateSlot(workspaceId);
141
+ try {
142
+ return await this.createInternalReserved(workspaceId, input, opts);
143
+ }
144
+ finally {
145
+ release();
146
+ }
147
+ }
148
+ reserveSessionCreateSlot(workspaceId) {
149
+ const cap = this.maxActiveSessionsPerWorkspace;
150
+ if (cap === undefined)
151
+ return () => { };
152
+ const pending = this.pendingSessionCreates.get(workspaceId) ?? 0;
153
+ if (this.store.countActive(workspaceId) + pending >= cap) {
154
+ this.onAdmissionRejected?.();
155
+ throw rateLimited("Concurrent active session limit reached for this workspace; archive or delete sessions, or retry later");
156
+ }
157
+ this.pendingSessionCreates.set(workspaceId, pending + 1);
158
+ let released = false;
159
+ return () => {
160
+ if (released)
161
+ return;
162
+ released = true;
163
+ const current = this.pendingSessionCreates.get(workspaceId) ?? 1;
164
+ if (current <= 1)
165
+ this.pendingSessionCreates.delete(workspaceId);
166
+ else
167
+ this.pendingSessionCreates.set(workspaceId, current - 1);
168
+ };
169
+ }
170
+ // Fail-closed egress gate (plan 0117e-3, the slice's key safety property):
171
+ // every present networking shape is parsed before any file/skill/runtime
172
+ // preparation. Unsupported hosted networking and malformed legacy rows must
173
+ // not silently turn into a --network none session.
174
+ assertEgressHonorable(environment) {
175
+ let policy;
176
+ try {
177
+ policy = parseNetworkingConfig(environment.config);
178
+ }
179
+ catch (error) {
180
+ if (error instanceof EgressPolicyError) {
181
+ throw invalidRequest(`Environment ${environment.id} has an invalid networking config: ${error.message}`);
182
+ }
183
+ throw error;
184
+ }
185
+ if (policy === undefined)
186
+ return;
187
+ if (this.egressCapability?.canHonorNetworking !== true) {
188
+ throw invalidRequest(`Environment ${environment.id} grants network egress, but this deployment cannot honor it ` +
189
+ "(requires OMA_SANDBOX_PROVIDER=docker-local with OMA_ENABLE_EGRESS=true and OMA_EGRESS_SIDECAR_IMAGE)");
190
+ }
191
+ if (policy.credentials.length > 0 && !this.egressCapability.hasSecretsStore) {
192
+ throw invalidRequest(`Environment ${environment.id} grants credentials, but this deployment has no secrets store ` +
193
+ "(set OMA_MASTER_KEY or OMA_MASTER_KEY_FILE)");
194
+ }
195
+ }
196
+ requireActiveEnvironment(workspaceId, environmentId) {
197
+ const environment = this.environments.retrieveAny(workspaceId, environmentId);
198
+ if (!environment) {
199
+ throw invalidRequest(`Environment ${environmentId} not found`);
200
+ }
201
+ if (environment.archived_at !== null) {
202
+ throw invalidRequest(`Environment ${environmentId} is archived.`);
203
+ }
204
+ return environment;
205
+ }
206
+ async createInternalReserved(workspaceId, input, opts = {}) {
207
+ const req = parseCreateSession(input);
208
+ const agentRef = parseAgentRef(req.agent);
209
+ const owner = this.agents.retrieveAny(workspaceId, agentRef.id);
210
+ if (!owner) {
211
+ throw invalidRequest(`Agent ${agentRef.id} not found`);
212
+ }
213
+ if (owner.archived_at !== null) {
214
+ throw invalidRequest(`agent ${agentRef.id} is archived and cannot be used to create a session`);
215
+ }
216
+ const agent = agentRef.version === undefined
217
+ ? owner
218
+ : this.agents.retrieveVersion(workspaceId, agentRef.id, agentRef.version);
219
+ if (!agent) {
220
+ throw notFound(`agent.version: ${agentRef.version} not found`);
221
+ }
222
+ this.modelAvailability?.assertReady(agent.model);
223
+ if (agent.skills.length > 0) {
224
+ const read = resolveBuiltinToolAccessForAgent(agent, "read");
225
+ if (!read.enabled || read.permission === "deny") {
226
+ throw invalidRequest("Missing required tool: skills require the read tool to be usable (enabled and not always_deny) on the session's `agent_toolset`");
227
+ }
228
+ for (const attachment of agent.skills) {
229
+ const version = attachment.version ?? "latest";
230
+ if (!this.skills?.getVersion(workspaceId, attachment.skill_id, version)) {
231
+ throw invalidRequest(`Could not resolve one or more skills: skill "${attachment.skill_id}" version "${version}" not found`);
232
+ }
233
+ }
234
+ }
235
+ const environment = this.requireActiveEnvironment(workspaceId, req.environment_id);
236
+ this.assertVaultsUsable(workspaceId, req.vault_ids ?? []);
237
+ this.assertEgressHonorable(environment);
238
+ const now = new Date().toISOString();
239
+ const sessionId = newSessionId();
240
+ let externalSideEffectsStarted = false;
241
+ const preparedFiles = await this.prepareFileResources(workspaceId, sessionId, req.resources ?? [], now, {
242
+ idempotency: opts.idempotency,
243
+ onExternalSideEffect: () => {
244
+ externalSideEffectsStarted = true;
245
+ },
246
+ });
247
+ const preparedSkills = await this.prepareSkillResources(workspaceId, sessionId, agent.skills, now, preparedFiles.totalBytes, {
248
+ idempotency: opts.idempotency,
249
+ onExternalSideEffect: () => { externalSideEffectsStarted = true; },
250
+ });
251
+ const resources = preparedFiles.resources;
252
+ const snapshots = [...preparedFiles.snapshots, ...preparedSkills.snapshots];
253
+ const mounts = [...preparedFiles.mounts, ...preparedSkills.mounts];
254
+ const row = {
255
+ id: sessionId,
256
+ workspace_id: workspaceId,
257
+ type: "session",
258
+ agent: {
259
+ type: "agent",
260
+ id: agent.id,
261
+ version: agent.version,
262
+ },
263
+ environment_id: environment.id,
264
+ vault_ids: req.vault_ids ?? [],
265
+ status: "idle",
266
+ title: req.title ?? null,
267
+ metadata: req.metadata ?? {},
268
+ created_at: now,
269
+ updated_at: now,
270
+ archived_at: null,
271
+ usage: null,
272
+ resources,
273
+ };
274
+ const sessionSnapshots = snapshots.map((snapshot) => ({
275
+ ...snapshot,
276
+ session_id: row.id,
277
+ }));
278
+ let runtimePrepared = false;
279
+ try {
280
+ if (mounts.length > 0 && this.runtime?.prepareSession) {
281
+ externalSideEffectsStarted = true;
282
+ await this.withIdempotencyHeartbeat(workspaceId, opts.idempotency, Promise.resolve(this.runtime.prepareSession(workspaceId, row.id, {
283
+ fileMounts: mounts,
284
+ skills: preparedSkills.skills.map(({ name, description }) => ({ name, description })),
285
+ environmentId: row.environment_id,
286
+ vaultIds: row.vault_ids,
287
+ agent: row.agent,
288
+ })));
289
+ runtimePrepared = true;
290
+ }
291
+ // File and runtime preparation may await. Re-read immediately before the
292
+ // synchronous durable write so an archive/delete that completed while
293
+ // preparation was in flight cannot leave an orphaned session reference.
294
+ this.requireActiveEnvironment(workspaceId, row.environment_id);
295
+ const record = {
296
+ row,
297
+ snapshots: sessionSnapshots,
298
+ skillSnapshots: preparedSkills.skills.map((skill) => ({ ...skill, session_id: row.id })),
299
+ };
300
+ if (opts.idempotency && this.idempotencyLedger) {
301
+ const response = toManagedSession(row);
302
+ const completion = idempotencyCompletion(workspaceId, opts.idempotency, { status: 200, body: response }, { type: "session", id: row.id });
303
+ const created = this.createSessionRowsWithIdempotency(record, {
304
+ complete: () => {
305
+ this.idempotencyLedger?.completeIdempotencyInTransaction(completion);
306
+ },
307
+ });
308
+ return toManagedSession(created);
309
+ }
310
+ return toManagedSession(this.store.create(record));
311
+ }
312
+ catch (error) {
313
+ if (runtimePrepared || mounts.length > 0) {
314
+ await this.closeRuntimeBestEffort(workspaceId, row.id);
315
+ }
316
+ await this.sweepPendingInternalSnapshotCreateRollbacks(workspaceId, row.id).catch((cleanupError) => {
317
+ log.warn("snapshot_create_rollback_sweep_failed", { error: cleanupError });
318
+ });
319
+ if (isUnsupportedFileResourceRuntime(error)) {
320
+ const unsupported = invalidRequest("Session file resources are not supported by the configured runtime.");
321
+ if (opts.idempotency) {
322
+ throw new ReleaseIdempotencyReservationError(unsupported);
323
+ }
324
+ throw unsupported;
325
+ }
326
+ if (opts.idempotency && externalSideEffectsStarted) {
327
+ throw new ReleaseIdempotencyReservationError(error);
328
+ }
329
+ throw error;
330
+ }
331
+ }
332
+ assertVaultsUsable(workspaceId, vaultIds) {
333
+ if (vaultIds.length === 0)
334
+ return;
335
+ if (!this.vaults) {
336
+ throw invalidRequest("Vaults are not supported by this server.");
337
+ }
338
+ this.vaults.assertVaultsUsable(workspaceId, vaultIds);
339
+ }
340
+ retrieve(workspaceId, sessionId) {
341
+ const row = this.store.retrieveAny(workspaceId, sessionId);
342
+ if (!row) {
343
+ throw notFound(`Session ${sessionId} not found`);
344
+ }
345
+ return toManagedSession(row);
346
+ }
347
+ async delete(workspaceId, sessionId) {
348
+ // Domain-owned liveness invariant: reject deleting a running session before
349
+ // any row/file mutation, so no caller (route or internal) can tear down a
350
+ // live runtime. Synchronous, so it runs in the same tick as the row removal.
351
+ this.assertDeletable(workspaceId, sessionId);
352
+ const result = this.deleteSessionRows?.(workspaceId, sessionId) ??
353
+ this.deleteSessionRowsWithDefaultStore(workspaceId, sessionId);
354
+ if (!result) {
355
+ throw notFound(`Session ${sessionId} not found`);
356
+ }
357
+ // Load-bearing, not dead code. The delete-vs-live-indexing race is now closed
358
+ // by assertSessionDeletable (a running turn can't be deleted), but this sweep
359
+ // still covers process-restart / abandoned-turn recovery, where a session row
360
+ // can carry orphaned output/snapshot rows with no live task. Do not remove.
361
+ await this.sweepPendingInternalSnapshotDeletes(workspaceId, sessionId).catch((error) => {
362
+ log.warn("snapshot_delete_sweep_failed", { error });
363
+ });
364
+ await this.cleanupDeletedSessionOutputs(workspaceId, sessionId, result.deletedSessionOutputFiles);
365
+ return { id: result.row.id, type: "session_deleted" };
366
+ }
367
+ deleteSessionRowsWithDefaultStore(workspaceId, sessionId) {
368
+ const row = this.store.delete(workspaceId, sessionId);
369
+ return row ? { row } : undefined;
370
+ }
371
+ async cleanupDeletedSessionOutputs(workspaceId, sessionId, deletedFiles) {
372
+ try {
373
+ if (deletedFiles !== undefined && hasRecordObjectDelete(this.files)) {
374
+ await this.files.deleteObjectsForRecords(deletedFiles);
375
+ return;
376
+ }
377
+ await this.files?.deleteSessionOutputs(workspaceId, sessionId);
378
+ }
379
+ catch (error) {
380
+ log.warn("session_output_cleanup_failed", {
381
+ workspaceId,
382
+ sessionId,
383
+ error,
384
+ });
385
+ }
386
+ }
387
+ async sweepPendingInternalSnapshotDeletes(workspaceId, sessionId) {
388
+ await this.sweepPendingSnapshotCleanup({
389
+ workspaceId,
390
+ sessionId,
391
+ listWorkspaces: () => this.store.listPendingInternalSnapshotDeleteWorkspaces(),
392
+ getRows: (pendingWorkspaceId, pendingSessionId) => this.store.getPendingInternalSnapshotDeletes(pendingWorkspaceId, pendingSessionId),
393
+ clearRow: (rowWorkspaceId, rowSessionId, resourceId) => this.store.clearPendingInternalSnapshotDelete(rowWorkspaceId, rowSessionId, resourceId),
394
+ recordAttempt: (rowWorkspaceId, rowSessionId, resourceId, attemptedAt, error) => this.store.recordPendingInternalSnapshotDeleteAttempt(rowWorkspaceId, rowSessionId, resourceId, attemptedAt, error),
395
+ scheduleRetry: (rowWorkspaceId, rowSessionId) => this.schedulePendingSnapshotCleanupRetry({
396
+ workspaceId: rowWorkspaceId,
397
+ sessionId: rowSessionId,
398
+ timers: this.pendingSnapshotDeleteRetryTimers,
399
+ retryLabel: "delete",
400
+ sweep: () => this.sweepPendingInternalSnapshotDeletes(rowWorkspaceId, rowSessionId),
401
+ }),
402
+ retryLabel: "delete",
403
+ });
404
+ }
405
+ async drainStartupSnapshotSweepsForTest() {
406
+ await this.startupSnapshotDeleteSweep;
407
+ await this.startupSnapshotCreateRollbackSweep;
408
+ }
409
+ async sweepPendingInternalSnapshotCreateRollbacks(workspaceIdOrOpts, sessionId) {
410
+ const opts = typeof workspaceIdOrOpts === "object"
411
+ ? workspaceIdOrOpts
412
+ : { workspaceId: workspaceIdOrOpts, sessionId };
413
+ await this.sweepPendingSnapshotCleanup({
414
+ workspaceId: opts.workspaceId,
415
+ sessionId: opts.sessionId,
416
+ createdBefore: opts.createdBefore,
417
+ listWorkspaces: () => this.store.listPendingInternalSnapshotCreateRollbackWorkspaces(),
418
+ getRows: (pendingWorkspaceId, pendingSessionId) => this.store.getPendingInternalSnapshotCreateRollbacks(pendingWorkspaceId, pendingSessionId),
419
+ clearRow: (rowWorkspaceId, rowSessionId, resourceId) => this.store.clearPendingInternalSnapshotCreateRollback(rowWorkspaceId, rowSessionId, resourceId),
420
+ recordAttempt: (rowWorkspaceId, rowSessionId, resourceId, attemptedAt, error) => this.store.recordPendingInternalSnapshotCreateRollbackAttempt(rowWorkspaceId, rowSessionId, resourceId, attemptedAt, error),
421
+ scheduleRetry: (rowWorkspaceId, rowSessionId) => this.schedulePendingSnapshotCleanupRetry({
422
+ workspaceId: rowWorkspaceId,
423
+ sessionId: rowSessionId,
424
+ timers: this.pendingSnapshotCreateRollbackRetryTimers,
425
+ retryLabel: "create rollback",
426
+ sweep: () => this.sweepPendingInternalSnapshotCreateRollbacks({
427
+ workspaceId: rowWorkspaceId,
428
+ sessionId: rowSessionId,
429
+ }),
430
+ }),
431
+ retryLabel: "create rollback",
432
+ });
433
+ }
434
+ async sweepPendingSnapshotCleanup(opts) {
435
+ if (!this.files)
436
+ return;
437
+ const workspaces = opts.workspaceId === undefined ? opts.listWorkspaces() : [opts.workspaceId];
438
+ for (const pendingWorkspaceId of workspaces) {
439
+ const pending = opts.getRows(pendingWorkspaceId, opts.workspaceId === undefined ? undefined : opts.sessionId);
440
+ for (const row of pending) {
441
+ if (opts.createdBefore !== undefined && row.created_at >= opts.createdBefore) {
442
+ continue;
443
+ }
444
+ if (row.attempt_count >= this.pendingSnapshotCleanupMaxAttempts) {
445
+ continue;
446
+ }
447
+ try {
448
+ await this.files.deleteInternalSnapshot(pendingWorkspaceId, row.snapshot_file_id);
449
+ opts.clearRow(pendingWorkspaceId, row.session_id, row.resource_id);
450
+ }
451
+ catch (error) {
452
+ const message = errorMessage(error);
453
+ const attemptedAt = new Date().toISOString();
454
+ const nextAttemptCount = row.attempt_count + 1;
455
+ opts.recordAttempt(pendingWorkspaceId, row.session_id, row.resource_id, attemptedAt, message);
456
+ const context = {
457
+ workspaceId: pendingWorkspaceId,
458
+ sessionId: row.session_id,
459
+ resourceId: row.resource_id,
460
+ snapshotFileId: row.snapshot_file_id,
461
+ retryLabel: opts.retryLabel,
462
+ attemptCount: nextAttemptCount,
463
+ error: message,
464
+ };
465
+ if (nextAttemptCount >= this.pendingSnapshotCleanupMaxAttempts) {
466
+ this.warnPendingSnapshotCleanupRetryCap(context);
467
+ continue;
468
+ }
469
+ opts.scheduleRetry(pendingWorkspaceId, row.session_id);
470
+ }
471
+ }
472
+ }
473
+ }
474
+ schedulePendingSnapshotCleanupRetry(opts) {
475
+ const key = JSON.stringify([opts.workspaceId, opts.sessionId]);
476
+ if (opts.timers.has(key))
477
+ return;
478
+ const timer = setTimeout(() => {
479
+ opts.timers.delete(key);
480
+ void opts.sweep().catch((error) => {
481
+ log.warn("snapshot_cleanup_retry_failed", {
482
+ retryLabel: opts.retryLabel,
483
+ error,
484
+ });
485
+ this.schedulePendingSnapshotCleanupRetry(opts);
486
+ });
487
+ }, this.pendingSnapshotCleanupRetryDelayMs);
488
+ timer.unref?.();
489
+ opts.timers.set(key, timer);
490
+ }
491
+ warnPendingSnapshotCleanupRetryCap(context) {
492
+ log.warn("snapshot_cleanup_retry_cap_reached", {
493
+ retryLabel: context.retryLabel,
494
+ workspaceId: context.workspaceId,
495
+ sessionId: context.sessionId,
496
+ resourceId: context.resourceId,
497
+ snapshotFileId: context.snapshotFileId,
498
+ attemptCount: context.attemptCount,
499
+ maxAttempts: this.pendingSnapshotCleanupMaxAttempts,
500
+ error: context.error,
501
+ });
502
+ }
503
+ list(workspaceId, opts = {}) {
504
+ const page = this.store.list(workspaceId, opts);
505
+ return {
506
+ data: page.data.map(toManagedSession),
507
+ next_page: page.next_page,
508
+ prev_page: page.prev_page,
509
+ };
510
+ }
511
+ async prepareFileResources(workspaceId, sessionId, resources, now, opts = {}) {
512
+ if (resources.length === 0) {
513
+ return { resources: [], snapshots: [], mounts: [], totalBytes: 0 };
514
+ }
515
+ if (!this.files) {
516
+ throw invalidRequest("File resources are not supported by this server.");
517
+ }
518
+ if (resources.length > this.maxFileResources) {
519
+ throw invalidRequest(`Session file resources exceed the ${this.maxFileResources} file limit`);
520
+ }
521
+ const normalized = normalizeSessionFileResources(resources.map((resource) => ({
522
+ fileId: resource.file_id,
523
+ mountPath: resource.mount_path,
524
+ })));
525
+ const prepared = [];
526
+ let totalBytes = 0;
527
+ for (const resource of normalized) {
528
+ const source = await this.withIdempotencyHeartbeat(workspaceId, opts.idempotency, this.files.retrieveMetadata(workspaceId, resource.fileId));
529
+ if (!source) {
530
+ throw invalidRequest(`File ${resource.fileId} not found`);
531
+ }
532
+ const stream = await this.withIdempotencyHeartbeat(workspaceId, opts.idempotency, this.files.openBytes(workspaceId, resource.fileId));
533
+ if (!stream) {
534
+ throw invalidRequest(`File ${resource.fileId} not found`);
535
+ }
536
+ const bytes = await this.withIdempotencyHeartbeat(workspaceId, opts.idempotency, consumeBytes(stream));
537
+ const sha256 = sha256Hex(bytes);
538
+ if (sha256 !== source.sha256) {
539
+ throw invalidRequest(`File ${resource.fileId} failed integrity validation`);
540
+ }
541
+ totalBytes += bytes.byteLength;
542
+ if (totalBytes > this.maxMountedBytes) {
543
+ throw invalidRequest(`Session file resources exceed the ${limitLabel(this.maxMountedBytes)} mounted byte limit`);
544
+ }
545
+ prepared.push({
546
+ source,
547
+ bytes,
548
+ sha256,
549
+ resourceId: newSessionResourceId(),
550
+ mountPath: resource.mountPath,
551
+ });
552
+ }
553
+ const createdSnapshots = [];
554
+ try {
555
+ for (const item of prepared) {
556
+ const snapshotFileId = newFileId();
557
+ const rollbackRow = {
558
+ workspace_id: workspaceId,
559
+ session_id: sessionId,
560
+ resource_id: item.resourceId,
561
+ file_id: item.source.metadata.id,
562
+ mount_path: item.mountPath,
563
+ snapshot_file_id: snapshotFileId,
564
+ sha256: item.sha256,
565
+ size_bytes: item.bytes.byteLength,
566
+ kind: "upload",
567
+ skill_snapshot_id: null,
568
+ };
569
+ this.store.recordPendingInternalSnapshotCreateRollback(rollbackRow, now);
570
+ opts.onExternalSideEffect?.();
571
+ const snapshot = await this.withIdempotencyHeartbeat(workspaceId, opts.idempotency, this.files.createInternalSnapshot(workspaceId, {
572
+ fileId: snapshotFileId,
573
+ filename: item.source.metadata.filename,
574
+ mimeType: item.source.metadata.mime_type,
575
+ scopeId: item.resourceId,
576
+ body: item.bytes,
577
+ }));
578
+ if (snapshot.metadata.id !== snapshotFileId) {
579
+ throw new Error(`FileStorage returned internal snapshot id ${snapshot.metadata.id}; expected ${snapshotFileId}`);
580
+ }
581
+ createdSnapshots.push({
582
+ workspace_id: workspaceId,
583
+ resource_id: item.resourceId,
584
+ file_id: item.source.metadata.id,
585
+ mount_path: item.mountPath,
586
+ snapshot_file_id: snapshot.metadata.id,
587
+ sha256: snapshot.sha256,
588
+ size_bytes: snapshot.metadata.size_bytes,
589
+ kind: "upload",
590
+ skill_snapshot_id: null,
591
+ });
592
+ }
593
+ }
594
+ catch (error) {
595
+ await this.sweepPendingInternalSnapshotCreateRollbacks(workspaceId, sessionId).catch((cleanupError) => {
596
+ log.warn("snapshot_create_rollback_sweep_failed", { error: cleanupError });
597
+ });
598
+ throw error;
599
+ }
600
+ return {
601
+ resources: prepared.map((item) => ({
602
+ id: item.resourceId,
603
+ type: "file",
604
+ file_id: item.source.metadata.id,
605
+ mount_path: item.mountPath,
606
+ created_at: now,
607
+ updated_at: now,
608
+ })),
609
+ snapshots: createdSnapshots,
610
+ mounts: createdSnapshots.map((snapshot, index) => ({
611
+ kind: "upload",
612
+ mountPath: snapshot.mount_path,
613
+ snapshotFileId: snapshot.snapshot_file_id,
614
+ sha256: snapshot.sha256,
615
+ sizeBytes: snapshot.size_bytes,
616
+ bytes: prepared[index].bytes,
617
+ })),
618
+ totalBytes,
619
+ };
620
+ }
621
+ async prepareSkillResources(workspaceId, sessionId, attachments, now, existingBytes, opts) {
622
+ if (attachments.length === 0)
623
+ return { skills: [], snapshots: [], mounts: [] };
624
+ if (!this.skills || !this.files)
625
+ throw invalidRequest("Skills are not supported by this server.");
626
+ const skills = [];
627
+ const snapshots = [];
628
+ const mounts = [];
629
+ let totalBytes = existingBytes;
630
+ try {
631
+ for (const attachment of attachments) {
632
+ const requested = attachment.version ?? "latest";
633
+ const version = this.skills.getVersion(workspaceId, attachment.skill_id, requested);
634
+ if (!version)
635
+ throw invalidRequest(`Could not resolve one or more skills: skill "${attachment.skill_id}" version "${requested}" not found`);
636
+ const skillSnapshotId = newSessionSkillSnapshotId();
637
+ skills.push({ workspace_id: workspaceId, skill_snapshot_id: skillSnapshotId, skill_id: attachment.skill_id, version: version.version, name: version.name, description: version.description });
638
+ for (const file of this.skills.getVersionFiles(workspaceId, attachment.skill_id, version.version)) {
639
+ const bytes = this.skills.openContent(workspaceId, attachment.skill_id, version.version, file.path);
640
+ if (!bytes || sha256Hex(bytes) !== file.sha256)
641
+ throw invalidRequest(`Skill ${attachment.skill_id} failed integrity validation`);
642
+ totalBytes += bytes.byteLength;
643
+ if (totalBytes > this.maxMountedBytes)
644
+ throw invalidRequest(`Session resources exceed the ${limitLabel(this.maxMountedBytes)} mounted byte limit`);
645
+ const resourceId = newSessionResourceId();
646
+ const snapshotFileId = newFileId();
647
+ const mountPath = `/workspace/skills/${file.path}`;
648
+ const rollbackRow = { workspace_id: workspaceId, session_id: sessionId, resource_id: resourceId, file_id: `${attachment.skill_id}:${version.version}:${file.path}`, mount_path: mountPath, snapshot_file_id: snapshotFileId, sha256: file.sha256, size_bytes: bytes.byteLength, kind: "skill", skill_snapshot_id: skillSnapshotId };
649
+ this.store.recordPendingInternalSnapshotCreateRollback(rollbackRow, now);
650
+ opts.onExternalSideEffect?.();
651
+ const snapshot = await this.withIdempotencyHeartbeat(workspaceId, opts.idempotency, this.files.createInternalSnapshot(workspaceId, { fileId: snapshotFileId, filename: file.path.split("/").at(-1) ?? "skill-file", mimeType: "application/octet-stream", scopeId: resourceId, body: bytes }));
652
+ const row = { workspace_id: workspaceId, resource_id: resourceId, file_id: rollbackRow.file_id, mount_path: mountPath, snapshot_file_id: snapshot.metadata.id, sha256: snapshot.sha256, size_bytes: snapshot.metadata.size_bytes, kind: "skill", skill_snapshot_id: skillSnapshotId };
653
+ snapshots.push(row);
654
+ mounts.push({ kind: "skill", mountPath, snapshotFileId: row.snapshot_file_id, sha256: row.sha256, sizeBytes: row.size_bytes, bytes });
655
+ }
656
+ }
657
+ return { skills, snapshots, mounts };
658
+ }
659
+ catch (error) {
660
+ await this.sweepPendingInternalSnapshotCreateRollbacks(workspaceId, sessionId).catch((cleanupError) => log.warn("snapshot_create_rollback_sweep_failed", { error: cleanupError }));
661
+ throw error;
662
+ }
663
+ }
664
+ async closeRuntimeBestEffort(workspaceId, sessionId) {
665
+ try {
666
+ await this.runtime?.closeSession?.(workspaceId, sessionId);
667
+ }
668
+ catch {
669
+ // The row has not committed yet; snapshot cleanup below is the
670
+ // authoritative control-plane rollback path for this create.
671
+ }
672
+ }
673
+ async withIdempotencyHeartbeat(workspaceId, idempotency, operation) {
674
+ return withIdempotencyReservationHeartbeat(this.idempotencyLedger, idempotency === undefined ? undefined : { ...idempotency, workspaceId }, operation);
675
+ }
676
+ }
677
+ class ReleaseIdempotencyReservationError extends Error {
678
+ inner;
679
+ constructor(inner) {
680
+ super(errorMessage(inner));
681
+ this.inner = inner;
682
+ this.name = "ReleaseIdempotencyReservationError";
683
+ }
684
+ }
685
+ function shouldReleaseIdempotencyReservation(error) {
686
+ if (error instanceof ReleaseIdempotencyReservationError)
687
+ return true;
688
+ // 429 admission rejections are transient: completing the key would replay
689
+ // the 429 forever, even after capacity frees. Release so the same-key retry
690
+ // re-executes (0113 D9).
691
+ if (error instanceof ApiError && error.status === 429)
692
+ return true;
693
+ if (error instanceof ApiError && error.status < 500)
694
+ return false;
695
+ return true;
696
+ }
697
+ function unwrapIdempotentCreateError(error) {
698
+ return error instanceof ReleaseIdempotencyReservationError
699
+ ? error.inner
700
+ : error;
701
+ }
702
+ async function consumeBytes(stream) {
703
+ const chunks = [];
704
+ let totalLength = 0;
705
+ for await (const chunk of stream) {
706
+ chunks.push(chunk);
707
+ totalLength += chunk.byteLength;
708
+ }
709
+ const out = new Uint8Array(totalLength);
710
+ let offset = 0;
711
+ for (const chunk of chunks) {
712
+ out.set(chunk, offset);
713
+ offset += chunk.byteLength;
714
+ }
715
+ return out;
716
+ }
717
+ function sha256Hex(bytes) {
718
+ return createHash("sha256").update(bytes).digest("hex");
719
+ }
720
+ function limitLabel(bytes) {
721
+ if (bytes % (1024 * 1024) === 0)
722
+ return `${bytes / (1024 * 1024)} MiB`;
723
+ return `${bytes} bytes`;
724
+ }
725
+ function errorMessage(error) {
726
+ return error instanceof Error ? error.message : String(error);
727
+ }
728
+ function hasRecordObjectDelete(files) {
729
+ return typeof files?.deleteObjectsForRecords === "function";
730
+ }
731
+ function isUnsupportedFileResourceRuntime(error) {
732
+ return error instanceof RuntimeUnsupportedSessionFileResourcesError;
733
+ }