@opengeni/db 3.7.4 → 4.0.0-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 (113) hide show
  1. package/dist/automations.d.ts +4 -0
  2. package/dist/canonical-human-identities.js +3 -3
  3. package/dist/capability-integrations.d.ts +2 -0
  4. package/dist/{chunk-6DPJA36C.js → chunk-2XEZK4LF.js} +7 -1
  5. package/dist/chunk-2XEZK4LF.js.map +1 -0
  6. package/dist/{chunk-4NAR7FBA.js → chunk-3RI33YQN.js} +67 -1
  7. package/dist/chunk-3RI33YQN.js.map +1 -0
  8. package/dist/{chunk-SZAWDXQC.js → chunk-CJROYHSC.js} +3 -3
  9. package/dist/{chunk-DZCYW2WT.js → chunk-MX6V5F65.js} +2 -2
  10. package/dist/{chunk-S3TDDCS2.js → chunk-N5VXJ4I2.js} +151 -78
  11. package/dist/chunk-N5VXJ4I2.js.map +1 -0
  12. package/dist/{chunk-U4WPPEBH.js → chunk-NLFHMPUB.js} +2 -2
  13. package/dist/{chunk-5NNCGFQY.js → chunk-P6NSEG2S.js} +2 -2
  14. package/dist/{chunk-O56DM5GZ.js → chunk-QM6YOWHZ.js} +624 -57
  15. package/dist/chunk-QM6YOWHZ.js.map +1 -0
  16. package/dist/{chunk-US4IS6IV.js → chunk-RZNOSEMC.js} +2 -2
  17. package/dist/{chunk-F6PBS6WE.js → chunk-UFAMOUWL.js} +2 -2
  18. package/dist/codemode-operations.d.ts +5 -0
  19. package/dist/codex-token-resolver.d.ts +6 -2
  20. package/dist/connection-token-resolver.d.ts +8 -0
  21. package/dist/editable-artifact-durable-export.js +2 -2
  22. package/dist/editable-artifacts.js +3 -3
  23. package/dist/generated-images.d.ts +15 -0
  24. package/dist/index.d.ts +393 -91
  25. package/dist/index.js +9992 -6069
  26. package/dist/index.js.map +1 -1
  27. package/dist/insights.d.ts +9 -0
  28. package/dist/managed-auth-session-sets.js +3 -3
  29. package/dist/managed-user-setup.d.ts +3 -0
  30. package/dist/mcp-oauth.d.ts +80 -0
  31. package/dist/migrate.d.ts +1 -0
  32. package/dist/migrate.js +3 -1
  33. package/dist/model-catalog.d.ts +153 -0
  34. package/dist/model-context-snapshots.d.ts +20 -0
  35. package/dist/new-session-drafts.d.ts +12 -0
  36. package/dist/organization-membership-lifecycle.d.ts +8 -1
  37. package/dist/organization-model-providers.d.ts +101 -0
  38. package/dist/pack-components.d.ts +1 -0
  39. package/dist/pr-review.d.ts +9 -0
  40. package/dist/provision-roles.js +1 -1
  41. package/dist/runtime-posture.d.ts +8 -8
  42. package/dist/schema.d.ts +7708 -5050
  43. package/dist/schema.js +25 -1
  44. package/dist/session-background-commands.d.ts +18 -6
  45. package/dist/session-background-commands.js +15 -9
  46. package/dist/session-control.d.ts +37 -2
  47. package/dist/session-goal-pacing.d.ts +3 -6
  48. package/dist/session-queue-commands.d.ts +17 -0
  49. package/dist/session-tenancy.js +3 -3
  50. package/dist/tool-gateway-approvals.d.ts +31 -0
  51. package/dist/video-generation.js +3 -3
  52. package/dist/workspace-artifacts.d.ts +17 -3
  53. package/dist/xai-subscription.d.ts +8 -0
  54. package/drizzle/0387_bound_host_export_session_payloads.sql +165 -0
  55. package/drizzle/0388_sandbox_provider_deadline_interactions.sql +491 -0
  56. package/drizzle/0389_model_catalog_and_gateway_custom_models.sql +241 -0
  57. package/drizzle/0390_organization_model_provider_connections.sql +197 -0
  58. package/drizzle/0391_sandbox_provider_deadline_interaction_followup.sql +295 -0
  59. package/drizzle/0392_context_compaction_pending_observability.sql +198 -0
  60. package/drizzle/0393_workspace_memory_and_learning_defaults.sql +102 -0
  61. package/drizzle/0394_session_selected_skill_activation.sql +20 -0
  62. package/drizzle/0395_scheduled_task_unclaimed_occurrence_invalidation.sql +154 -0
  63. package/drizzle/0396_model_call_equivalent_credit_cost.sql +28 -0
  64. package/drizzle/0397_sandbox_deadline_rotation_preemption.sql +113 -0
  65. package/drizzle/0398_organization_workspace_management_entry.sql +206 -0
  66. package/drizzle/0399_additional_managed_organization_creation.sql +691 -0
  67. package/drizzle/0400_session_attempt_model_context_snapshots.sql +65 -0
  68. package/drizzle/0401_organization_user_setup_token_transport.sql +178 -0
  69. package/drizzle/0402_session_input_wait_and_background_command_results.sql +1023 -0
  70. package/drizzle/0403_codex_unconditional_credential_leasing.sql +181 -0
  71. package/drizzle/0404_mcp_oauth_authorization_server.sql +471 -0
  72. package/drizzle/0405_tool_gateway_approval_capabilities.sql +184 -0
  73. package/drizzle/0406_workspace_html_site_sources.sql +71 -0
  74. package/package.json +6 -6
  75. package/src/automations.ts +9 -1
  76. package/src/capability-integrations.ts +8 -0
  77. package/src/codemode-operations.ts +31 -21
  78. package/src/codex-token-resolver.ts +16 -6
  79. package/src/connection-token-resolver.ts +52 -3
  80. package/src/generated-images.ts +70 -6
  81. package/src/index.ts +3894 -994
  82. package/src/insights-model-bundle.ts +54 -5
  83. package/src/insights.ts +53 -7
  84. package/src/managed-user-setup.ts +14 -3
  85. package/src/mcp-oauth.ts +488 -0
  86. package/src/migrate.ts +9 -1
  87. package/src/model-catalog.ts +682 -0
  88. package/src/model-context-snapshots.ts +151 -0
  89. package/src/new-session-drafts.ts +66 -21
  90. package/src/organization-membership-lifecycle.ts +47 -5
  91. package/src/organization-model-providers.ts +614 -0
  92. package/src/pack-components.ts +15 -2
  93. package/src/pr-review.ts +19 -0
  94. package/src/provision-roles.ts +4 -0
  95. package/src/runtime-posture.ts +64 -0
  96. package/src/schema.ts +666 -61
  97. package/src/session-background-commands.ts +180 -78
  98. package/src/session-control.ts +181 -12
  99. package/src/session-goal-pacing.ts +4 -8
  100. package/src/session-queue-commands.ts +157 -82
  101. package/src/tool-gateway-approvals.ts +168 -0
  102. package/src/workspace-artifacts.ts +633 -178
  103. package/src/xai-subscription.ts +66 -14
  104. package/dist/chunk-4NAR7FBA.js.map +0 -1
  105. package/dist/chunk-6DPJA36C.js.map +0 -1
  106. package/dist/chunk-O56DM5GZ.js.map +0 -1
  107. package/dist/chunk-S3TDDCS2.js.map +0 -1
  108. /package/dist/{chunk-SZAWDXQC.js.map → chunk-CJROYHSC.js.map} +0 -0
  109. /package/dist/{chunk-DZCYW2WT.js.map → chunk-MX6V5F65.js.map} +0 -0
  110. /package/dist/{chunk-U4WPPEBH.js.map → chunk-NLFHMPUB.js.map} +0 -0
  111. /package/dist/{chunk-5NNCGFQY.js.map → chunk-P6NSEG2S.js.map} +0 -0
  112. /package/dist/{chunk-US4IS6IV.js.map → chunk-RZNOSEMC.js.map} +0 -0
  113. /package/dist/{chunk-F6PBS6WE.js.map → chunk-UFAMOUWL.js.map} +0 -0
@@ -1,10 +1,14 @@
1
- import type {
2
- WorkspaceArtifact,
3
- WorkspaceArtifactDetailResponse,
4
- WorkspaceArtifactEvent,
5
- WorkspaceArtifactMutationResponse,
6
- WorkspaceArtifactVersion,
1
+ import {
2
+ WorkspaceArtifactRequestedTools,
3
+ type ToolGatewayIdentity,
4
+ type WorkspaceArtifact,
5
+ type WorkspaceArtifactDetailResponse,
6
+ type WorkspaceArtifactEvent,
7
+ type WorkspaceArtifactMutationResponse,
8
+ type WorkspaceArtifactVersion,
7
9
  } from "@opengeni/contracts";
10
+ import { parseVerifiedAttemptToolCatalog } from "@opengeni/codemode";
11
+ import { createHash } from "node:crypto";
8
12
  import { and, desc, eq, lt, or, sql } from "drizzle-orm";
9
13
  import type { Database } from "./database";
10
14
  import { withRlsContext, withWorkspaceRls } from "./database";
@@ -13,7 +17,12 @@ import * as schema from "./schema";
13
17
  type ArtifactRow = typeof schema.workspaceArtifacts.$inferSelect;
14
18
  type VersionRow = typeof schema.workspaceArtifactVersions.$inferSelect;
15
19
  type EventRow = typeof schema.workspaceArtifactEvents.$inferSelect;
16
- type ArtifactMutationToolName = "artifacts_create" | "artifacts_publish" | "artifacts_rollback";
20
+ type ArtifactMutationToolName =
21
+ | "artifacts_create"
22
+ | "artifacts_publish"
23
+ | "artifacts_rollback"
24
+ | "artifacts_archive"
25
+ | "artifacts_restore";
17
26
 
18
27
  export class WorkspaceArtifactNotFoundError extends Error {
19
28
  readonly name = "WorkspaceArtifactNotFoundError";
@@ -47,6 +56,9 @@ function versionFromRow(row: VersionRow): WorkspaceArtifactVersion {
47
56
  contentType: "text/html",
48
57
  contentSha256: row.contentSha256,
49
58
  sizeBytes: row.sizeBytes,
59
+ sourceSha256: row.sourceSha256,
60
+ sourceSizeBytes: row.sourceSizeBytes,
61
+ requestedTools: WorkspaceArtifactRequestedTools.parse(row.requestedTools),
50
62
  sourceSessionId: row.sourceSessionId,
51
63
  sourceTurnId: row.sourceTurnId,
52
64
  sourceAttemptId: row.sourceAttemptId,
@@ -125,23 +137,34 @@ async function artifactRow(scopedDb: any, workspaceId: string, artifactId: strin
125
137
  export async function listWorkspaceArtifacts(
126
138
  db: Database,
127
139
  workspaceId: string,
128
- options: { limit?: number; cursor?: string } = {},
129
- ): Promise<{ artifacts: WorkspaceArtifact[]; nextCursor: string | null; truncated: boolean }> {
140
+ options: {
141
+ limit?: number;
142
+ cursor?: string;
143
+ status?: "active" | "archived";
144
+ } = {},
145
+ ): Promise<{
146
+ artifacts: WorkspaceArtifact[];
147
+ nextCursor: string | null;
148
+ truncated: boolean;
149
+ }> {
130
150
  return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
131
151
  const limit = Math.min(Math.max(options.limit ?? 50, 1), 100);
132
152
  const cursor = options.cursor ? decodeListCursor(options.cursor) : null;
133
- const visibility = cursor
134
- ? and(
135
- eq(schema.workspaceArtifacts.workspaceId, workspaceId),
136
- or(
137
- lt(schema.workspaceArtifacts.updatedAt, cursor.updatedAt),
138
- and(
139
- eq(schema.workspaceArtifacts.updatedAt, cursor.updatedAt),
140
- lt(schema.workspaceArtifacts.id, cursor.id),
153
+ const visibility = and(
154
+ eq(schema.workspaceArtifacts.workspaceId, workspaceId),
155
+ ...(options.status ? [eq(schema.workspaceArtifacts.status, options.status)] : []),
156
+ ...(cursor
157
+ ? [
158
+ or(
159
+ lt(schema.workspaceArtifacts.updatedAt, cursor.updatedAt),
160
+ and(
161
+ eq(schema.workspaceArtifacts.updatedAt, cursor.updatedAt),
162
+ lt(schema.workspaceArtifacts.id, cursor.id),
163
+ ),
141
164
  ),
142
- ),
143
- )
144
- : eq(schema.workspaceArtifacts.workspaceId, workspaceId);
165
+ ]
166
+ : []),
167
+ );
145
168
  const rows = await scopedDb
146
169
  .select()
147
170
  .from(schema.workspaceArtifacts)
@@ -237,7 +260,12 @@ export async function getWorkspaceArtifactContentRef(
237
260
  workspaceId: string,
238
261
  artifactId: string,
239
262
  versionId?: string,
240
- ): Promise<{ artifactId: string; version: WorkspaceArtifactVersion; contentKey: string }> {
263
+ ): Promise<{
264
+ artifactId: string;
265
+ version: WorkspaceArtifactVersion;
266
+ contentKey: string;
267
+ sourceKey: string | null;
268
+ }> {
241
269
  return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
242
270
  const artifact = await artifactRow(scopedDb, workspaceId, artifactId);
243
271
  if (!artifact) throw new WorkspaceArtifactNotFoundError("Artifact not found");
@@ -255,7 +283,12 @@ export async function getWorkspaceArtifactContentRef(
255
283
  )
256
284
  .limit(1);
257
285
  if (!version) throw new WorkspaceArtifactNotFoundError("Artifact version not found");
258
- return { artifactId, version: versionFromRow(version), contentKey: version.contentKey };
286
+ return {
287
+ artifactId,
288
+ version: versionFromRow(version),
289
+ contentKey: version.contentKey,
290
+ sourceKey: version.sourceKey,
291
+ };
259
292
  });
260
293
  }
261
294
 
@@ -265,6 +298,10 @@ type PublishMetadata = {
265
298
  contentKey: string;
266
299
  contentSha256: string;
267
300
  sizeBytes: number;
301
+ sourceKey: string;
302
+ sourceSha256: string;
303
+ sourceSizeBytes: number;
304
+ requestedTools?: ToolGatewayIdentity[];
268
305
  operationKey: string;
269
306
  actorSubjectId: string;
270
307
  sourceSessionId: string | null;
@@ -273,8 +310,70 @@ type PublishMetadata = {
273
310
  sourceExecutionGeneration: number | null;
274
311
  sourceToolName: ArtifactMutationToolName | null;
275
312
  persistContent: () => Promise<void>;
313
+ discardContent: () => Promise<void>;
276
314
  };
277
315
 
316
+ async function discardPersistedArtifactContent(
317
+ input: Pick<PublishMetadata, "discardContent">,
318
+ error: unknown,
319
+ ): Promise<never> {
320
+ await input.discardContent().catch(() => undefined);
321
+ throw error;
322
+ }
323
+
324
+ type ArtifactOperationReplay = NonNullable<Awaited<ReturnType<typeof replayForOperation>>>;
325
+
326
+ function persistedArtifactContentIsReferenced(
327
+ replay: ArtifactOperationReplay,
328
+ input: Pick<PublishMetadata, "contentKey" | "sourceKey">,
329
+ ): boolean {
330
+ return (
331
+ replay.version.contentKey === input.contentKey || replay.version.sourceKey === input.sourceKey
332
+ );
333
+ }
334
+
335
+ async function reconcilePersistedArtifactMutation(
336
+ db: Database,
337
+ input: PublishMetadata,
338
+ error: unknown,
339
+ validateReplay: (replay: ArtifactOperationReplay) => void,
340
+ ): Promise<WorkspaceArtifactMutationResponse> {
341
+ let replay: ArtifactOperationReplay | null;
342
+ try {
343
+ replay = await withRlsContext(
344
+ db,
345
+ { accountId: input.accountId, workspaceId: input.workspaceId },
346
+ async (scopedDb) => {
347
+ await lockOperation(scopedDb, input.workspaceId, input.operationKey);
348
+ return await replayForOperation(scopedDb, input.workspaceId, input.operationKey);
349
+ },
350
+ );
351
+ } catch {
352
+ // The transaction outcome is still unknown. Leaking an unreferenced object
353
+ // is recoverable; deleting content that may have committed is not.
354
+ throw error;
355
+ }
356
+ if (!replay) return await discardPersistedArtifactContent(input, error);
357
+ try {
358
+ validateReplay(replay);
359
+ } catch (replayError) {
360
+ if (!persistedArtifactContentIsReferenced(replay, input)) {
361
+ await input.discardContent().catch(() => undefined);
362
+ }
363
+ throw replayError;
364
+ }
365
+ if (!persistedArtifactContentIsReferenced(replay, input)) {
366
+ await input.discardContent().catch(() => undefined);
367
+ }
368
+ return mutationResult(
369
+ replay.artifact,
370
+ replay.version,
371
+ replay.event,
372
+ true,
373
+ replay.current ?? replay.version,
374
+ );
375
+ }
376
+
278
377
  async function assertAttemptAuthority(
279
378
  scopedDb: any,
280
379
  input: Pick<
@@ -368,6 +467,61 @@ async function assertAttemptAuthority(
368
467
  }
369
468
  }
370
469
 
470
+ async function assertArtifactRequestedToolsInAttemptCatalog(
471
+ scopedDb: any,
472
+ input: Pick<
473
+ PublishMetadata,
474
+ | "accountId"
475
+ | "workspaceId"
476
+ | "sourceSessionId"
477
+ | "sourceTurnId"
478
+ | "sourceAttemptId"
479
+ | "sourceExecutionGeneration"
480
+ >,
481
+ requestedTools: readonly ToolGatewayIdentity[],
482
+ ): Promise<void> {
483
+ if (input.sourceAttemptId === null || requestedTools.length === 0) return;
484
+ if (
485
+ input.sourceSessionId === null ||
486
+ input.sourceTurnId === null ||
487
+ input.sourceExecutionGeneration === null
488
+ ) {
489
+ throw new WorkspaceArtifactOperationError("Artifact attempt provenance is incomplete");
490
+ }
491
+ const [row] = await scopedDb
492
+ .select({ catalog: schema.sessionAttemptToolCatalogs.catalog })
493
+ .from(schema.sessionAttemptToolCatalogs)
494
+ .where(
495
+ and(
496
+ eq(schema.sessionAttemptToolCatalogs.accountId, input.accountId),
497
+ eq(schema.sessionAttemptToolCatalogs.workspaceId, input.workspaceId),
498
+ eq(schema.sessionAttemptToolCatalogs.sessionId, input.sourceSessionId),
499
+ eq(schema.sessionAttemptToolCatalogs.turnId, input.sourceTurnId),
500
+ eq(schema.sessionAttemptToolCatalogs.attemptId, input.sourceAttemptId),
501
+ eq(schema.sessionAttemptToolCatalogs.executionGeneration, input.sourceExecutionGeneration),
502
+ ),
503
+ )
504
+ .limit(1);
505
+ let allowed: Set<string>;
506
+ try {
507
+ const catalog = parseVerifiedAttemptToolCatalog(row?.catalog);
508
+ allowed = new Set(catalog.entries.map((entry) => toolIdentityKey(entry.identity)));
509
+ } catch {
510
+ throw new WorkspaceArtifactOperationError(
511
+ "Artifact requested tool authority is unavailable for the exact attempt",
512
+ );
513
+ }
514
+ if (requestedTools.some((identity) => !allowed.has(toolIdentityKey(identity)))) {
515
+ throw new WorkspaceArtifactOperationError(
516
+ "Artifact requested tools must be present in the exact attempt tool catalog",
517
+ );
518
+ }
519
+ }
520
+
521
+ function toolIdentityKey(identity: ToolGatewayIdentity): string {
522
+ return `${identity.serverId}\u0000${identity.toolName}`;
523
+ }
524
+
371
525
  async function replayForOperation(scopedDb: any, workspaceId: string, operationKey: string) {
372
526
  const [event] = await scopedDb
373
527
  .select()
@@ -385,10 +539,35 @@ async function replayForOperation(scopedDb: any, workspaceId: string, operationK
385
539
  const [version] = await scopedDb
386
540
  .select()
387
541
  .from(schema.workspaceArtifactVersions)
388
- .where(eq(schema.workspaceArtifactVersions.id, event.toVersionId))
542
+ .where(
543
+ and(
544
+ eq(schema.workspaceArtifactVersions.workspaceId, workspaceId),
545
+ eq(schema.workspaceArtifactVersions.id, event.toVersionId),
546
+ ),
547
+ )
389
548
  .limit(1);
390
549
  if (!version) return null;
391
- return { artifact, version, event, current: await currentVersion(scopedDb, artifact) } as const;
550
+ let fromVersion: VersionRow | null = null;
551
+ if (event.fromVersionId) {
552
+ const [row] = await scopedDb
553
+ .select()
554
+ .from(schema.workspaceArtifactVersions)
555
+ .where(
556
+ and(
557
+ eq(schema.workspaceArtifactVersions.workspaceId, workspaceId),
558
+ eq(schema.workspaceArtifactVersions.id, event.fromVersionId),
559
+ ),
560
+ )
561
+ .limit(1);
562
+ fromVersion = row ?? null;
563
+ }
564
+ return {
565
+ artifact,
566
+ version,
567
+ fromVersion,
568
+ event,
569
+ current: await currentVersion(scopedDb, artifact),
570
+ } as const;
392
571
  }
393
572
 
394
573
  async function lockOperation(scopedDb: any, workspaceId: string, operationKey: string) {
@@ -410,6 +589,30 @@ function assertCreateReplay(replay: Awaited<ReturnType<typeof replayForOperation
410
589
  }
411
590
  }
412
591
 
592
+ function assertCreateReplayMatchesInput(
593
+ replay: ArtifactOperationReplay,
594
+ input: PublishMetadata & {
595
+ slug: string;
596
+ title: string;
597
+ description: string | null;
598
+ requestedSlug?: string | null;
599
+ },
600
+ ): void {
601
+ assertCreateReplay(replay);
602
+ const requestDigest = createArtifactRequestDigest(input);
603
+ if (replay.event.requestDigest !== null && replay.event.requestDigest !== requestDigest) {
604
+ throw new WorkspaceArtifactConflictError(
605
+ "Idempotency key was already used with different artifact metadata",
606
+ );
607
+ }
608
+ if (replay.version.contentSha256 !== input.contentSha256) {
609
+ throw new WorkspaceArtifactConflictError(
610
+ "Idempotency key was already used with different content",
611
+ );
612
+ }
613
+ assertReplayVersionMetadata(replay.version, input, input.requestedTools ?? []);
614
+ }
615
+
413
616
  function assertPublishReplay(
414
617
  replay: Awaited<ReturnType<typeof replayForOperation>>,
415
618
  artifactId: string,
@@ -427,6 +630,83 @@ function assertPublishReplay(
427
630
  }
428
631
  }
429
632
 
633
+ function assertPublishReplayMatchesInput(
634
+ replay: ArtifactOperationReplay,
635
+ input: PublishMetadata & {
636
+ artifactId: string;
637
+ expectedCurrentVersionId: string;
638
+ },
639
+ ): void {
640
+ assertPublishReplay(replay, input.artifactId, input.expectedCurrentVersionId);
641
+ const expectedRequestedTools = input.requestedTools ?? replay.fromVersion?.requestedTools;
642
+ if (!expectedRequestedTools) {
643
+ throw new WorkspaceArtifactConflictError(
644
+ "Idempotency key was already used for a publication with missing source authority",
645
+ );
646
+ }
647
+ const requestDigest = publishArtifactRequestDigest(input, expectedRequestedTools);
648
+ if (replay.event.requestDigest !== null && replay.event.requestDigest !== requestDigest) {
649
+ throw new WorkspaceArtifactConflictError(
650
+ "Idempotency key was already used with different publication metadata",
651
+ );
652
+ }
653
+ if (replay.version.contentSha256 !== input.contentSha256) {
654
+ throw new WorkspaceArtifactConflictError(
655
+ "Idempotency key was already used with different content",
656
+ );
657
+ }
658
+ assertReplayVersionMetadata(replay.version, input, expectedRequestedTools);
659
+ }
660
+
661
+ function hashArtifactRequest(value: unknown): string {
662
+ return createHash("sha256").update(JSON.stringify(value), "utf8").digest("hex");
663
+ }
664
+
665
+ function createArtifactRequestDigest(
666
+ input: PublishMetadata & {
667
+ slug: string;
668
+ title: string;
669
+ description: string | null;
670
+ requestedSlug?: string | null;
671
+ },
672
+ ): string {
673
+ return hashArtifactRequest({
674
+ version: 1,
675
+ operation: "create",
676
+ requestedSlug: input.requestedSlug === undefined ? input.slug : input.requestedSlug,
677
+ title: input.title,
678
+ description: input.description,
679
+ contentSha256: input.contentSha256,
680
+ sourceSha256: input.sourceSha256,
681
+ requestedTools: input.requestedTools ?? [],
682
+ });
683
+ }
684
+
685
+ function publishArtifactRequestDigest(
686
+ input: PublishMetadata & {
687
+ artifactId: string;
688
+ expectedCurrentVersionId: string;
689
+ title?: string;
690
+ description?: string | null;
691
+ },
692
+ requestedTools: readonly ToolGatewayIdentity[],
693
+ ): string {
694
+ return hashArtifactRequest({
695
+ version: 1,
696
+ operation: "publish",
697
+ artifactId: input.artifactId,
698
+ expectedCurrentVersionId: input.expectedCurrentVersionId,
699
+ title: input.title === undefined ? { present: false } : { present: true, value: input.title },
700
+ description:
701
+ input.description === undefined
702
+ ? { present: false }
703
+ : { present: true, value: input.description },
704
+ contentSha256: input.contentSha256,
705
+ sourceSha256: input.sourceSha256,
706
+ requestedTools,
707
+ });
708
+ }
709
+
430
710
  function assertRollbackReplay(
431
711
  replay: Awaited<ReturnType<typeof replayForOperation>>,
432
712
  artifactId: string,
@@ -448,6 +728,28 @@ function assertRollbackReplay(
448
728
  }
449
729
  }
450
730
 
731
+ function assertStatusReplay(
732
+ replay: Awaited<ReturnType<typeof replayForOperation>>,
733
+ artifactId: string,
734
+ status: "active" | "archived",
735
+ expectedCurrentVersionId: string,
736
+ reason: string,
737
+ ) {
738
+ const expectedType = status === "archived" ? "archived" : "restored";
739
+ if (
740
+ !replay ||
741
+ replay.event.type !== expectedType ||
742
+ replay.event.artifactId !== artifactId ||
743
+ replay.event.fromVersionId !== expectedCurrentVersionId ||
744
+ replay.event.toVersionId !== expectedCurrentVersionId ||
745
+ replay.event.reason !== reason
746
+ ) {
747
+ throw new WorkspaceArtifactConflictError(
748
+ "Idempotency key was already used for a different operation",
749
+ );
750
+ }
751
+ }
752
+
451
753
  function mutationResult(
452
754
  artifact: ArtifactRow,
453
755
  version: VersionRow,
@@ -470,92 +772,105 @@ export async function createWorkspaceArtifact(
470
772
  slug: string;
471
773
  title: string;
472
774
  description: string | null;
775
+ requestedSlug?: string | null;
473
776
  },
474
777
  ): Promise<WorkspaceArtifactMutationResponse> {
475
- return await withRlsContext(
476
- db,
477
- { accountId: input.accountId, workspaceId: input.workspaceId },
478
- async (scopedDb) => {
479
- return await scopedDb.transaction(async (tx) => {
480
- await lockOperation(tx, input.workspaceId, input.operationKey);
481
- await assertAttemptAuthority(tx, input);
482
- const replay = await replayForOperation(tx, input.workspaceId, input.operationKey);
483
- if (replay) {
484
- assertCreateReplay(replay);
485
- if (replay.version.contentSha256 !== input.contentSha256) {
486
- throw new WorkspaceArtifactConflictError(
487
- "Idempotency key was already used with different content",
778
+ let contentPersisted = false;
779
+ try {
780
+ return await withRlsContext(
781
+ db,
782
+ { accountId: input.accountId, workspaceId: input.workspaceId },
783
+ async (scopedDb) => {
784
+ return await scopedDb.transaction(async (tx) => {
785
+ await lockOperation(tx, input.workspaceId, input.operationKey);
786
+ const replay = await replayForOperation(tx, input.workspaceId, input.operationKey);
787
+ if (replay) {
788
+ assertCreateReplayMatchesInput(replay, input);
789
+ return mutationResult(
790
+ replay.artifact,
791
+ replay.version,
792
+ replay.event,
793
+ true,
794
+ replay.current ?? replay.version,
488
795
  );
489
796
  }
490
- return mutationResult(
491
- replay.artifact,
492
- replay.version,
493
- replay.event,
494
- true,
495
- replay.current ?? replay.version,
496
- );
497
- }
498
- await input.persistContent();
499
- const [artifact] = await tx
500
- .insert(schema.workspaceArtifacts)
501
- .values({
502
- id: input.artifactId,
503
- accountId: input.accountId,
504
- workspaceId: input.workspaceId,
505
- slug: input.slug,
506
- title: input.title,
507
- description: input.description,
508
- createdBySubjectId: input.actorSubjectId,
509
- })
510
- .returning();
511
- const [version] = await tx
512
- .insert(schema.workspaceArtifactVersions)
513
- .values({
514
- accountId: input.accountId,
515
- workspaceId: input.workspaceId,
516
- artifactId: input.artifactId,
517
- revision: 1,
518
- contentKey: input.contentKey,
519
- contentSha256: input.contentSha256,
520
- sizeBytes: input.sizeBytes,
521
- operationKey: input.operationKey,
522
- sourceSessionId: input.sourceSessionId,
523
- sourceTurnId: input.sourceTurnId,
524
- sourceAttemptId: input.sourceAttemptId,
525
- sourceExecutionGeneration: input.sourceExecutionGeneration,
526
- createdBySubjectId: input.actorSubjectId,
527
- })
528
- .returning();
529
- const [updated] = await tx
530
- .update(schema.workspaceArtifacts)
531
- .set({
532
- currentVersionId: version!.id,
533
- updatedAt: new Date(),
534
- })
535
- .where(eq(schema.workspaceArtifacts.id, artifact!.id))
536
- .returning();
537
- const [event] = await tx
538
- .insert(schema.workspaceArtifactEvents)
539
- .values({
540
- accountId: input.accountId,
541
- workspaceId: input.workspaceId,
542
- artifactId: input.artifactId,
543
- type: "published",
544
- fromVersionId: null,
545
- toVersionId: version!.id,
546
- operationKey: input.operationKey,
547
- sourceSessionId: input.sourceSessionId,
548
- sourceTurnId: input.sourceTurnId,
549
- sourceAttemptId: input.sourceAttemptId,
550
- sourceExecutionGeneration: input.sourceExecutionGeneration,
551
- actorSubjectId: input.actorSubjectId,
552
- reason: "Initial publication",
553
- })
554
- .returning();
555
- return mutationResult(updated!, version!, event!, false);
797
+ await assertAttemptAuthority(tx, input);
798
+ await assertArtifactRequestedToolsInAttemptCatalog(tx, input, input.requestedTools ?? []);
799
+ await input.persistContent();
800
+ contentPersisted = true;
801
+ const [artifact] = await tx
802
+ .insert(schema.workspaceArtifacts)
803
+ .values({
804
+ id: input.artifactId,
805
+ accountId: input.accountId,
806
+ workspaceId: input.workspaceId,
807
+ slug: input.slug,
808
+ title: input.title,
809
+ description: input.description,
810
+ createdBySubjectId: input.actorSubjectId,
811
+ })
812
+ .returning();
813
+ const [version] = await tx
814
+ .insert(schema.workspaceArtifactVersions)
815
+ .values({
816
+ accountId: input.accountId,
817
+ workspaceId: input.workspaceId,
818
+ artifactId: input.artifactId,
819
+ revision: 1,
820
+ contentKey: input.contentKey,
821
+ contentSha256: input.contentSha256,
822
+ sizeBytes: input.sizeBytes,
823
+ sourceKey: input.sourceKey,
824
+ sourceSha256: input.sourceSha256,
825
+ sourceSizeBytes: input.sourceSizeBytes,
826
+ requestedTools: input.requestedTools ?? [],
827
+ operationKey: input.operationKey,
828
+ sourceSessionId: input.sourceSessionId,
829
+ sourceTurnId: input.sourceTurnId,
830
+ sourceAttemptId: input.sourceAttemptId,
831
+ sourceExecutionGeneration: input.sourceExecutionGeneration,
832
+ createdBySubjectId: input.actorSubjectId,
833
+ })
834
+ .returning();
835
+ const [updated] = await tx
836
+ .update(schema.workspaceArtifacts)
837
+ .set({
838
+ currentVersionId: version!.id,
839
+ updatedAt: new Date(),
840
+ })
841
+ .where(eq(schema.workspaceArtifacts.id, artifact!.id))
842
+ .returning();
843
+ const [event] = await tx
844
+ .insert(schema.workspaceArtifactEvents)
845
+ .values({
846
+ accountId: input.accountId,
847
+ workspaceId: input.workspaceId,
848
+ artifactId: input.artifactId,
849
+ type: "published",
850
+ fromVersionId: null,
851
+ toVersionId: version!.id,
852
+ operationKey: input.operationKey,
853
+ requestDigest: createArtifactRequestDigest(input),
854
+ sourceSessionId: input.sourceSessionId,
855
+ sourceTurnId: input.sourceTurnId,
856
+ sourceAttemptId: input.sourceAttemptId,
857
+ sourceExecutionGeneration: input.sourceExecutionGeneration,
858
+ actorSubjectId: input.actorSubjectId,
859
+ reason: "Initial publication",
860
+ })
861
+ .returning();
862
+ return mutationResult(updated!, version!, event!, false);
863
+ });
864
+ },
865
+ );
866
+ } catch (error) {
867
+ if (contentPersisted) {
868
+ return await reconcilePersistedArtifactMutation(db, input, error, (replay) => {
869
+ assertCreateReplayMatchesInput(replay, input);
556
870
  });
557
- },
558
- );
871
+ }
872
+ throw error;
873
+ }
559
874
  }
560
875
 
561
876
  export async function publishWorkspaceArtifactVersion(
@@ -566,6 +881,146 @@ export async function publishWorkspaceArtifactVersion(
566
881
  title?: string;
567
882
  description?: string | null;
568
883
  },
884
+ ): Promise<WorkspaceArtifactMutationResponse> {
885
+ let contentPersisted = false;
886
+ try {
887
+ return await withRlsContext(
888
+ db,
889
+ { accountId: input.accountId, workspaceId: input.workspaceId },
890
+ async (scopedDb) => {
891
+ return await scopedDb.transaction(async (tx) => {
892
+ await lockOperation(tx, input.workspaceId, input.operationKey);
893
+ const replay = await replayForOperation(tx, input.workspaceId, input.operationKey);
894
+ if (replay) {
895
+ assertPublishReplayMatchesInput(replay, input);
896
+ return mutationResult(
897
+ replay.artifact,
898
+ replay.version,
899
+ replay.event,
900
+ true,
901
+ replay.current ?? replay.version,
902
+ );
903
+ }
904
+ await assertAttemptAuthority(tx, input);
905
+ const artifact = await artifactRow(tx, input.workspaceId, input.artifactId, true);
906
+ if (!artifact) throw new WorkspaceArtifactNotFoundError("Artifact not found");
907
+ if (artifact.status !== "active") {
908
+ throw new WorkspaceArtifactOperationError("Archived artifacts cannot be published");
909
+ }
910
+ if (artifact.currentVersionId !== input.expectedCurrentVersionId) {
911
+ throw new WorkspaceArtifactConflictError(
912
+ "Artifact changed in another request",
913
+ artifact.currentVersionId,
914
+ );
915
+ }
916
+ const current = await currentVersion(tx, artifact);
917
+ if (!current) throw new WorkspaceArtifactNotFoundError("Artifact has no current version");
918
+ await assertArtifactRequestedToolsInAttemptCatalog(
919
+ tx,
920
+ input,
921
+ input.requestedTools ?? current.requestedTools,
922
+ );
923
+ const [latest] = await tx
924
+ .select()
925
+ .from(schema.workspaceArtifactVersions)
926
+ .where(
927
+ and(
928
+ eq(schema.workspaceArtifactVersions.workspaceId, input.workspaceId),
929
+ eq(schema.workspaceArtifactVersions.artifactId, input.artifactId),
930
+ ),
931
+ )
932
+ .orderBy(desc(schema.workspaceArtifactVersions.revision))
933
+ .limit(1);
934
+ await input.persistContent();
935
+ contentPersisted = true;
936
+ const [version] = await tx
937
+ .insert(schema.workspaceArtifactVersions)
938
+ .values({
939
+ accountId: input.accountId,
940
+ workspaceId: input.workspaceId,
941
+ artifactId: input.artifactId,
942
+ revision: (latest?.revision ?? 0) + 1,
943
+ contentKey: input.contentKey,
944
+ contentSha256: input.contentSha256,
945
+ sizeBytes: input.sizeBytes,
946
+ sourceKey: input.sourceKey,
947
+ sourceSha256: input.sourceSha256,
948
+ sourceSizeBytes: input.sourceSizeBytes,
949
+ requestedTools: input.requestedTools ?? current.requestedTools,
950
+ operationKey: input.operationKey,
951
+ sourceSessionId: input.sourceSessionId,
952
+ sourceTurnId: input.sourceTurnId,
953
+ sourceAttemptId: input.sourceAttemptId,
954
+ sourceExecutionGeneration: input.sourceExecutionGeneration,
955
+ createdBySubjectId: input.actorSubjectId,
956
+ })
957
+ .returning();
958
+ const update: Partial<typeof schema.workspaceArtifacts.$inferInsert> = {
959
+ currentVersionId: version!.id,
960
+ updatedAt: new Date(),
961
+ };
962
+ if (input.title !== undefined) update.title = input.title;
963
+ if (input.description !== undefined) update.description = input.description;
964
+ const [updated] = await tx
965
+ .update(schema.workspaceArtifacts)
966
+ .set(update)
967
+ .where(eq(schema.workspaceArtifacts.id, artifact.id))
968
+ .returning();
969
+ const [event] = await tx
970
+ .insert(schema.workspaceArtifactEvents)
971
+ .values({
972
+ accountId: input.accountId,
973
+ workspaceId: input.workspaceId,
974
+ artifactId: input.artifactId,
975
+ type: "published",
976
+ fromVersionId: artifact.currentVersionId,
977
+ toVersionId: version!.id,
978
+ operationKey: input.operationKey,
979
+ requestDigest: publishArtifactRequestDigest(
980
+ input,
981
+ input.requestedTools ?? current.requestedTools,
982
+ ),
983
+ sourceSessionId: input.sourceSessionId,
984
+ sourceTurnId: input.sourceTurnId,
985
+ sourceAttemptId: input.sourceAttemptId,
986
+ sourceExecutionGeneration: input.sourceExecutionGeneration,
987
+ actorSubjectId: input.actorSubjectId,
988
+ reason: `Published revision ${version!.revision}`,
989
+ })
990
+ .returning();
991
+ return mutationResult(updated!, version!, event!, false);
992
+ });
993
+ },
994
+ );
995
+ } catch (error) {
996
+ if (contentPersisted) {
997
+ return await reconcilePersistedArtifactMutation(db, input, error, (replay) => {
998
+ assertPublishReplayMatchesInput(replay, input);
999
+ });
1000
+ }
1001
+ throw error;
1002
+ }
1003
+ }
1004
+
1005
+ export async function rollbackWorkspaceArtifact(
1006
+ db: Database,
1007
+ input: Omit<
1008
+ PublishMetadata,
1009
+ | "contentKey"
1010
+ | "contentSha256"
1011
+ | "sizeBytes"
1012
+ | "sourceKey"
1013
+ | "sourceSha256"
1014
+ | "sourceSizeBytes"
1015
+ | "requestedTools"
1016
+ | "persistContent"
1017
+ | "discardContent"
1018
+ > & {
1019
+ artifactId: string;
1020
+ versionId: string;
1021
+ expectedCurrentVersionId: string;
1022
+ reason: string;
1023
+ },
569
1024
  ): Promise<WorkspaceArtifactMutationResponse> {
570
1025
  return await withRlsContext(
571
1026
  db,
@@ -573,15 +1028,15 @@ export async function publishWorkspaceArtifactVersion(
573
1028
  async (scopedDb) => {
574
1029
  return await scopedDb.transaction(async (tx) => {
575
1030
  await lockOperation(tx, input.workspaceId, input.operationKey);
576
- await assertAttemptAuthority(tx, input);
577
1031
  const replay = await replayForOperation(tx, input.workspaceId, input.operationKey);
578
1032
  if (replay) {
579
- assertPublishReplay(replay, input.artifactId, input.expectedCurrentVersionId);
580
- if (replay.version.contentSha256 !== input.contentSha256) {
581
- throw new WorkspaceArtifactConflictError(
582
- "Idempotency key was already used with different content",
583
- );
584
- }
1033
+ assertRollbackReplay(
1034
+ replay,
1035
+ input.artifactId,
1036
+ input.versionId,
1037
+ input.expectedCurrentVersionId,
1038
+ input.reason,
1039
+ );
585
1040
  return mutationResult(
586
1041
  replay.artifact,
587
1042
  replay.version,
@@ -590,56 +1045,33 @@ export async function publishWorkspaceArtifactVersion(
590
1045
  replay.current ?? replay.version,
591
1046
  );
592
1047
  }
1048
+ await assertAttemptAuthority(tx, input);
593
1049
  const artifact = await artifactRow(tx, input.workspaceId, input.artifactId, true);
594
1050
  if (!artifact) throw new WorkspaceArtifactNotFoundError("Artifact not found");
595
1051
  if (artifact.status !== "active") {
596
- throw new WorkspaceArtifactOperationError("Archived artifacts cannot be published");
1052
+ throw new WorkspaceArtifactOperationError("Archived artifacts cannot be rolled back");
597
1053
  }
598
- if (artifact.currentVersionId !== input.expectedCurrentVersionId) {
1054
+ if (artifact.currentVersionId !== input.expectedCurrentVersionId)
599
1055
  throw new WorkspaceArtifactConflictError(
600
1056
  "Artifact changed in another request",
601
1057
  artifact.currentVersionId,
602
1058
  );
603
- }
604
- const [latest] = await tx
1059
+ const [target] = await tx
605
1060
  .select()
606
1061
  .from(schema.workspaceArtifactVersions)
607
1062
  .where(
608
1063
  and(
609
1064
  eq(schema.workspaceArtifactVersions.workspaceId, input.workspaceId),
610
1065
  eq(schema.workspaceArtifactVersions.artifactId, input.artifactId),
1066
+ eq(schema.workspaceArtifactVersions.id, input.versionId),
611
1067
  ),
612
1068
  )
613
- .orderBy(desc(schema.workspaceArtifactVersions.revision))
614
1069
  .limit(1);
615
- await input.persistContent();
616
- const [version] = await tx
617
- .insert(schema.workspaceArtifactVersions)
618
- .values({
619
- accountId: input.accountId,
620
- workspaceId: input.workspaceId,
621
- artifactId: input.artifactId,
622
- revision: (latest?.revision ?? 0) + 1,
623
- contentKey: input.contentKey,
624
- contentSha256: input.contentSha256,
625
- sizeBytes: input.sizeBytes,
626
- operationKey: input.operationKey,
627
- sourceSessionId: input.sourceSessionId,
628
- sourceTurnId: input.sourceTurnId,
629
- sourceAttemptId: input.sourceAttemptId,
630
- sourceExecutionGeneration: input.sourceExecutionGeneration,
631
- createdBySubjectId: input.actorSubjectId,
632
- })
633
- .returning();
634
- const update: Partial<typeof schema.workspaceArtifacts.$inferInsert> = {
635
- currentVersionId: version!.id,
636
- updatedAt: new Date(),
637
- };
638
- if (input.title !== undefined) update.title = input.title;
639
- if (input.description !== undefined) update.description = input.description;
1070
+ if (!target) throw new WorkspaceArtifactNotFoundError("Artifact version not found");
1071
+ await assertArtifactRequestedToolsInAttemptCatalog(tx, input, target.requestedTools);
640
1072
  const [updated] = await tx
641
1073
  .update(schema.workspaceArtifacts)
642
- .set(update)
1074
+ .set({ currentVersionId: target.id, updatedAt: new Date() })
643
1075
  .where(eq(schema.workspaceArtifacts.id, artifact.id))
644
1076
  .returning();
645
1077
  const [event] = await tx
@@ -648,29 +1080,40 @@ export async function publishWorkspaceArtifactVersion(
648
1080
  accountId: input.accountId,
649
1081
  workspaceId: input.workspaceId,
650
1082
  artifactId: input.artifactId,
651
- type: "published",
1083
+ type: "rolled_back",
652
1084
  fromVersionId: artifact.currentVersionId,
653
- toVersionId: version!.id,
1085
+ toVersionId: target.id,
654
1086
  operationKey: input.operationKey,
655
1087
  sourceSessionId: input.sourceSessionId,
656
1088
  sourceTurnId: input.sourceTurnId,
657
1089
  sourceAttemptId: input.sourceAttemptId,
658
1090
  sourceExecutionGeneration: input.sourceExecutionGeneration,
659
1091
  actorSubjectId: input.actorSubjectId,
660
- reason: `Published revision ${version!.revision}`,
1092
+ reason: input.reason,
661
1093
  })
662
1094
  .returning();
663
- return mutationResult(updated!, version!, event!, false);
1095
+ return mutationResult(updated!, target, event!, false);
664
1096
  });
665
1097
  },
666
1098
  );
667
1099
  }
668
1100
 
669
- export async function rollbackWorkspaceArtifact(
1101
+ export async function setWorkspaceArtifactStatus(
670
1102
  db: Database,
671
- input: Omit<PublishMetadata, "contentKey" | "contentSha256" | "sizeBytes" | "persistContent"> & {
1103
+ input: Omit<
1104
+ PublishMetadata,
1105
+ | "contentKey"
1106
+ | "contentSha256"
1107
+ | "sizeBytes"
1108
+ | "sourceKey"
1109
+ | "sourceSha256"
1110
+ | "sourceSizeBytes"
1111
+ | "requestedTools"
1112
+ | "persistContent"
1113
+ | "discardContent"
1114
+ > & {
672
1115
  artifactId: string;
673
- versionId: string;
1116
+ status: "active" | "archived";
674
1117
  expectedCurrentVersionId: string;
675
1118
  reason: string;
676
1119
  },
@@ -678,16 +1121,15 @@ export async function rollbackWorkspaceArtifact(
678
1121
  return await withRlsContext(
679
1122
  db,
680
1123
  { accountId: input.accountId, workspaceId: input.workspaceId },
681
- async (scopedDb) => {
682
- return await scopedDb.transaction(async (tx) => {
1124
+ async (scopedDb) =>
1125
+ await scopedDb.transaction(async (tx) => {
683
1126
  await lockOperation(tx, input.workspaceId, input.operationKey);
684
- await assertAttemptAuthority(tx, input);
685
1127
  const replay = await replayForOperation(tx, input.workspaceId, input.operationKey);
686
1128
  if (replay) {
687
- assertRollbackReplay(
1129
+ assertStatusReplay(
688
1130
  replay,
689
1131
  input.artifactId,
690
- input.versionId,
1132
+ input.status,
691
1133
  input.expectedCurrentVersionId,
692
1134
  input.reason,
693
1135
  );
@@ -699,31 +1141,30 @@ export async function rollbackWorkspaceArtifact(
699
1141
  replay.current ?? replay.version,
700
1142
  );
701
1143
  }
1144
+ await assertAttemptAuthority(tx, input);
702
1145
  const artifact = await artifactRow(tx, input.workspaceId, input.artifactId, true);
703
1146
  if (!artifact) throw new WorkspaceArtifactNotFoundError("Artifact not found");
704
- if (artifact.status !== "active") {
705
- throw new WorkspaceArtifactOperationError("Archived artifacts cannot be rolled back");
706
- }
707
- if (artifact.currentVersionId !== input.expectedCurrentVersionId)
1147
+ if (artifact.currentVersionId !== input.expectedCurrentVersionId) {
708
1148
  throw new WorkspaceArtifactConflictError(
709
1149
  "Artifact changed in another request",
710
1150
  artifact.currentVersionId,
711
1151
  );
712
- const [target] = await tx
713
- .select()
714
- .from(schema.workspaceArtifactVersions)
715
- .where(
716
- and(
717
- eq(schema.workspaceArtifactVersions.workspaceId, input.workspaceId),
718
- eq(schema.workspaceArtifactVersions.artifactId, input.artifactId),
719
- eq(schema.workspaceArtifactVersions.id, input.versionId),
720
- ),
721
- )
722
- .limit(1);
723
- if (!target) throw new WorkspaceArtifactNotFoundError("Artifact version not found");
1152
+ }
1153
+ const current = await currentVersion(tx, artifact);
1154
+ if (!current) throw new WorkspaceArtifactNotFoundError("Artifact has no current version");
1155
+ if (input.status === "active") {
1156
+ await assertArtifactRequestedToolsInAttemptCatalog(tx, input, current.requestedTools);
1157
+ }
1158
+ if (artifact.status === input.status) {
1159
+ throw new WorkspaceArtifactOperationError(
1160
+ input.status === "archived"
1161
+ ? "Artifact is already archived"
1162
+ : "Artifact is already active",
1163
+ );
1164
+ }
724
1165
  const [updated] = await tx
725
1166
  .update(schema.workspaceArtifacts)
726
- .set({ currentVersionId: target.id, updatedAt: new Date() })
1167
+ .set({ status: input.status, updatedAt: new Date() })
727
1168
  .where(eq(schema.workspaceArtifacts.id, artifact.id))
728
1169
  .returning();
729
1170
  const [event] = await tx
@@ -732,9 +1173,9 @@ export async function rollbackWorkspaceArtifact(
732
1173
  accountId: input.accountId,
733
1174
  workspaceId: input.workspaceId,
734
1175
  artifactId: input.artifactId,
735
- type: "rolled_back",
736
- fromVersionId: artifact.currentVersionId,
737
- toVersionId: target.id,
1176
+ type: input.status === "archived" ? "archived" : "restored",
1177
+ fromVersionId: current.id,
1178
+ toVersionId: current.id,
738
1179
  operationKey: input.operationKey,
739
1180
  sourceSessionId: input.sourceSessionId,
740
1181
  sourceTurnId: input.sourceTurnId,
@@ -744,8 +1185,22 @@ export async function rollbackWorkspaceArtifact(
744
1185
  reason: input.reason,
745
1186
  })
746
1187
  .returning();
747
- return mutationResult(updated!, target, event!, false);
748
- });
749
- },
1188
+ return mutationResult(updated!, current, event!, false);
1189
+ }),
750
1190
  );
751
1191
  }
1192
+
1193
+ function assertReplayVersionMetadata(
1194
+ version: VersionRow,
1195
+ input: PublishMetadata,
1196
+ expectedRequestedTools: ToolGatewayIdentity[],
1197
+ ): void {
1198
+ if (
1199
+ version.sourceSha256 !== input.sourceSha256 ||
1200
+ JSON.stringify(version.requestedTools) !== JSON.stringify(expectedRequestedTools)
1201
+ ) {
1202
+ throw new WorkspaceArtifactConflictError(
1203
+ "Idempotency key was already used with different source or requested tools",
1204
+ );
1205
+ }
1206
+ }