@opengeni/core 2.2.0-canary.0 → 2.4.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.
@@ -535,13 +535,15 @@ async function resolvePackRig(
535
535
  image: null,
536
536
  };
537
537
  }
538
- const image = rig.activeVersion.image ?? null;
539
- const status =
540
- pack.sandboxImage && image !== pack.sandboxImage
541
- ? "mismatch"
542
- : pack.rig?.requireVerified && rig.activeVersionHealth?.checkHealth !== "passing"
543
- ? "unverified"
544
- : "ready";
538
+ // Explicit Rig images are disabled: a selected Rig always composes on the
539
+ // deployment platform base. Historical image values remain stored but are
540
+ // neither effective runtime truth nor a valid v2 Pack compatibility target.
541
+ const image = null;
542
+ const status = pack.sandboxImage
543
+ ? "mismatch"
544
+ : pack.rig?.requireVerified && rig.activeVersionHealth?.checkHealth !== "passing"
545
+ ? "unverified"
546
+ : "ready";
545
547
  return {
546
548
  required,
547
549
  status,
@@ -561,7 +563,7 @@ function packRigBlocker(rig: PackRigResolution, legacyImage: string | null): str
561
563
  return "Verify the selected compute environment before installing this Pack";
562
564
  }
563
565
  if (legacyImage) {
564
- return `The selected compute environment must use the Pack's previous image ${legacyImage}; the Pack will not replace workspace compute`;
566
+ return `This Pack requires the retired explicit sandbox image ${legacyImage}; Rig-bound Packs currently must use the deployment-managed platform sandbox`;
565
567
  }
566
568
  return "The selected compute environment does not satisfy this Pack's requirement";
567
569
  }
@@ -27,6 +27,7 @@ import {
27
27
  getWorkspaceInstructionPolicyBaseline,
28
28
  getWorkspaceKnowledgeChangeProposalSummary,
29
29
  getWorkspaceKnowledgeClaimInitiatingHuman,
30
+ materializeConfirmedRememberKnowledgeMemory,
30
31
  nestedPostgresSqlState,
31
32
  rebaselineWorkspaceInstructionPolicyKnowledgeProposal,
32
33
  } from "@opengeni/db";
@@ -101,6 +102,7 @@ export type RememberRouterOptions = {
101
102
  rebaselineProposal?: typeof rebaselineWorkspaceInstructionPolicyKnowledgeProposal;
102
103
  activateHumanConfirmed?: typeof activateHumanConfirmedLearningDecision;
103
104
  confirmKnowledgeClaim?: typeof confirmRememberKnowledgeClaim;
105
+ materializeKnowledgeMemory?: typeof materializeConfirmedRememberKnowledgeMemory;
104
106
  proposalSummary?: typeof getWorkspaceKnowledgeChangeProposalSummary;
105
107
  claimInitiatingHuman?: typeof getWorkspaceKnowledgeClaimInitiatingHuman;
106
108
  instructionPolicyBaseline?: typeof getWorkspaceInstructionPolicyBaseline;
@@ -283,6 +285,8 @@ function activationSummary(receipt: GovernedLearningActivationReceipt) {
283
285
  * and the result is either an immediate activation (automatic policy), a
284
286
  * proposal for the human Knowledge review lifecycle, or a durable proposal that
285
287
  * still needs one bound human confirmation through `request_human_input`.
288
+ * A confirmed Knowledge claim is then materialized as a keyword-searchable
289
+ * Memory record from its exact approved Task-note text.
286
290
  */
287
291
  export function createRememberRouter(options: RememberRouterOptions): {
288
292
  remember: (input: { attempt: unknown; request: unknown }) => Promise<RememberReceiptType>;
@@ -297,6 +301,8 @@ export function createRememberRouter(options: RememberRouterOptions): {
297
301
  const activateHumanConfirmed =
298
302
  options.activateHumanConfirmed ?? activateHumanConfirmedLearningDecision;
299
303
  const confirmKnowledgeClaim = options.confirmKnowledgeClaim ?? confirmRememberKnowledgeClaim;
304
+ const materializeKnowledgeMemory =
305
+ options.materializeKnowledgeMemory ?? materializeConfirmedRememberKnowledgeMemory;
300
306
  const proposalSummary = options.proposalSummary ?? getWorkspaceKnowledgeChangeProposalSummary;
301
307
  const claimInitiatingHuman =
302
308
  options.claimInitiatingHuman ?? getWorkspaceKnowledgeClaimInitiatingHuman;
@@ -362,7 +368,11 @@ export function createRememberRouter(options: RememberRouterOptions): {
362
368
  scope: request.scope,
363
369
  };
364
370
  if (route.decision === "blocked" || route.write === null) {
365
- return RememberReceipt.parse({ ...base, status: "blocked", reason: "learning_policy_off" });
371
+ return RememberReceipt.parse({
372
+ ...base,
373
+ status: "blocked",
374
+ reason: "learning_policy_off",
375
+ });
366
376
  }
367
377
  if (route.write.knowledgeChangeProposalId === null) {
368
378
  // Knowledge is owned by the human review lifecycle: the same one-click
@@ -424,19 +434,30 @@ export function createRememberRouter(options: RememberRouterOptions): {
424
434
  if (!initiatingHuman) {
425
435
  throw new RememberError("proposal_unavailable", "The remember claim is not available");
426
436
  }
427
- const receipt = await confirmKnowledgeClaim(options.db, {
428
- caller: {
429
- workspaceId: attempt.workspaceId,
430
- subjectId: initiatingHuman,
431
- sessionId: attempt.sessionId,
432
- turnId: attempt.turnId,
433
- executionGeneration: attempt.executionGeneration,
434
- },
435
- request: {
436
- operationId: request.operationId,
437
- claimId: request.claimId,
438
- humanInputRequestId: request.humanInputRequestId,
439
- },
437
+ // Approval and Memory materialization are one outer transaction. The
438
+ // nested RLS helpers use savepoints, so a failed Memory write rolls the
439
+ // approval back instead of leaving an approved but unretrievable claim.
440
+ const { receipt, materialization } = await options.db.transaction(async (tx) => {
441
+ const transaction = tx as unknown as Database;
442
+ const activationReceipt = await confirmKnowledgeClaim(transaction, {
443
+ caller: {
444
+ workspaceId: attempt.workspaceId,
445
+ subjectId: initiatingHuman,
446
+ sessionId: attempt.sessionId,
447
+ turnId: attempt.turnId,
448
+ executionGeneration: attempt.executionGeneration,
449
+ },
450
+ request: {
451
+ operationId: request.operationId,
452
+ claimId: request.claimId,
453
+ humanInputRequestId: request.humanInputRequestId,
454
+ },
455
+ });
456
+ const materializedMemory = await materializeKnowledgeMemory(
457
+ transaction,
458
+ activationReceipt,
459
+ );
460
+ return { receipt: activationReceipt, materialization: materializedMemory };
440
461
  });
441
462
  return RememberConfirmReceipt.parse({
442
463
  status: "activated",
@@ -448,10 +469,11 @@ export function createRememberRouter(options: RememberRouterOptions): {
448
469
  destination: "knowledge",
449
470
  receiptId: receipt.id,
450
471
  claimId: receipt.claimId,
472
+ memoryId: materialization.memoryId,
451
473
  approvalReviewId: receipt.approvalReviewId,
452
474
  effectiveAt: receipt.createdAt,
453
475
  authorityKind: "human_confirmed",
454
- undo: "knowledge_review",
476
+ undo: "memory_management",
455
477
  },
456
478
  });
457
479
  }
@@ -54,6 +54,7 @@ import {
54
54
  type TurnInitiator,
55
55
  type TurnInitiatorContext,
56
56
  type TurnExecutionPolicyV1,
57
+ type VariableSet,
57
58
  type XaiProviderAccountAuthoritySnapshotV1,
58
59
  } from "@opengeni/contracts";
59
60
  import {
@@ -65,7 +66,7 @@ import {
65
66
  getChannel,
66
67
  getRig,
67
68
  getWorkspaceDefaultRigId,
68
- listDistinctVariableSetIdsInGroup,
69
+ listDistinctVariableSetSelectionsInGroup,
69
70
  listDistinctRigVersionIdsInGroup,
70
71
  getSandbox,
71
72
  getSession,
@@ -87,7 +88,7 @@ import {
87
88
  requireSession,
88
89
  submitHumanPromptInTransaction,
89
90
  appendSessionEventsWithLockedSessionUpdate,
90
- updateSessionTitle as updateSessionTitleRow,
91
+ updateSessionTitleWithEvent,
91
92
  withWorkspaceSubjectSessionActivityRls,
92
93
  type CreateSessionMcpServerInput,
93
94
  type Database,
@@ -107,7 +108,6 @@ import {
107
108
  type NewSessionDraftSnapshot,
108
109
  } from "@opengeni/db";
109
110
  import {
110
- appendAndPublishEvents,
111
111
  publishDurableSessionEvents,
112
112
  publishDurableWorkspaceControlEvent,
113
113
  type EventBus,
@@ -640,8 +640,9 @@ export async function createAndStartSessionWithOutcome(input: {
640
640
  createdBy?: TurnInitiator;
641
641
  createdByContext?: TurnInitiatorContext;
642
642
  createdByActor?: Extract<SessionCommandActor, { type: "agent_attempt" }> | null;
643
- // Names/ids only; the session.created payload never carries variable values.
644
- variableSet?: { id: string; name: string } | null;
643
+ // Ordered low-to-high precedence. Names/ids only; session.created never
644
+ // carries variable values.
645
+ variableSets?: Array<{ id: string; name: string; scope: VariableSet["scope"] }>;
645
646
  // The rig + frozen active rig version resolved at create (M3). Both null ⇒ a
646
647
  // rig-less session (byte-for-byte today's behavior). Frozen here so a later
647
648
  // rig promote never moves an existing session's version.
@@ -760,7 +761,8 @@ export async function createAndStartSessionWithOutcome(input: {
760
761
  reasoningEffort: input.reasoningEffort,
761
762
  latencyMode: input.latencyMode ?? "standard",
762
763
  sandboxBackend: input.sandboxBackend,
763
- variableSetId: input.variableSet?.id ?? null,
764
+ variableSetIds: input.variableSets?.map((variableSet) => variableSet.id) ?? [],
765
+ variableSetId: input.variableSets?.at(-1)?.id ?? null,
764
766
  rigId: input.rigId ?? null,
765
767
  rigVersionId: input.rigVersionId ?? null,
766
768
  channelId: input.channelId ?? null,
@@ -831,7 +833,8 @@ export async function createAndStartSessionWithOutcome(input: {
831
833
  reasoningEffort: input.reasoningEffort,
832
834
  latencyMode: input.latencyMode ?? "standard",
833
835
  sandboxBackend: input.sandboxBackend,
834
- variableSetId: input.variableSet?.id ?? null,
836
+ variableSetIds: input.variableSets?.map((variableSet) => variableSet.id) ?? [],
837
+ variableSetId: input.variableSets?.at(-1)?.id ?? null,
835
838
  rigId: input.rigId ?? null,
836
839
  rigVersionId: input.rigVersionId ?? null,
837
840
  channelId: input.channelId ?? null,
@@ -900,7 +903,7 @@ async function finishStartSession(
900
903
  reasoningEffort: Settings["openaiReasoningEffort"];
901
904
  turnExecutionPolicy: TurnExecutionPolicyV1;
902
905
  sandboxBackend: Settings["sandboxBackend"];
903
- variableSet?: { id: string; name: string } | null;
906
+ variableSets?: Array<{ id: string; name: string; scope: VariableSet["scope"] }>;
904
907
  goal?: GoalSpec | null;
905
908
  sessionMcpServers?: SessionMcpServerMetadata[];
906
909
  seedTargetSandbox?: {
@@ -978,10 +981,13 @@ async function finishStartSession(
978
981
  turnExecutionPolicy: input.turnExecutionPolicy,
979
982
  createdEventPayload: {
980
983
  toolPolicy: input.toolPolicy,
981
- ...(input.variableSet
984
+ ...(input.variableSets?.length
982
985
  ? {
983
- variableSetId: input.variableSet.id,
984
- variableSetName: input.variableSet.name,
986
+ variableSetIds: input.variableSets.map((variableSet) => variableSet.id),
987
+ variableSets: input.variableSets,
988
+ // Legacy highest-precedence aliases.
989
+ variableSetId: input.variableSets.at(-1)!.id,
990
+ variableSetName: input.variableSets.at(-1)!.name,
985
991
  }
986
992
  : {}),
987
993
  ...(input.sessionMcpServers?.length ? { mcpServers: input.sessionMcpServers } : {}),
@@ -1719,17 +1725,15 @@ export async function createSessionForRequestWithOutcome(
1719
1725
  });
1720
1726
  }
1721
1727
  await validateFileResources(db, grant.accountId, workspaceId, grant.subjectId, resources);
1722
- // VariableSet attachment requires variable-sets:use on the calling grant
1723
- // (validateVariableSetAttachment enforces it), preserving the invariant
1724
- // that sandboxed agents cannot self-attach workspace secrets.
1725
- const variableSet = payload.variableSetId
1726
- ? await validateVariableSetAttachment(
1727
- { settings, db },
1728
- grant,
1729
- workspaceId,
1730
- payload.variableSetId,
1731
- )
1732
- : null;
1728
+ // Every selected Variable Set is independently authorized. Scope does not
1729
+ // affect precedence: explicit order is low-to-high and later sets win name
1730
+ // collisions.
1731
+ const variableSets: VariableSet[] = [];
1732
+ for (const variableSetId of payload.variableSetIds ?? []) {
1733
+ variableSets.push(
1734
+ await validateVariableSetAttachment({ settings, db }, grant, workspaceId, variableSetId),
1735
+ );
1736
+ }
1733
1737
  // RIG BINDING (M3). Resolve the rig this session rides — a UUID binds that
1734
1738
  // rig, null explicitly opts out, and omission inherits the workspace default
1735
1739
  // (workspaces.default_rig_id) — then FREEZE both the rig id and its currently-
@@ -1827,7 +1831,12 @@ export async function createSessionForRequestWithOutcome(
1827
1831
  ...(payload.sandboxBackend ? { sandboxBackend: payload.sandboxBackend } : {}),
1828
1832
  ...(payload.targetSandboxId ? { targetSandboxId: payload.targetSandboxId } : {}),
1829
1833
  ...(payload.workingDir ? { workingDir: payload.workingDir } : {}),
1830
- ...(payload.variableSetId ? { variableSetId: payload.variableSetId } : {}),
1834
+ ...(variableSets.length
1835
+ ? {
1836
+ variableSetIds: variableSets.map((variableSet) => variableSet.id),
1837
+ variableSetId: variableSets.at(-1)!.id,
1838
+ }
1839
+ : {}),
1831
1840
  ...(payload.rigId ? { rigId: payload.rigId } : {}),
1832
1841
  ...(payload.goal ? { goal: payload.goal } : {}),
1833
1842
  ...(payload.firstPartyMcpPermissions
@@ -2041,9 +2050,9 @@ export async function createSessionForRequestWithOutcome(
2041
2050
  // provided-session attach — no shared box state exists to conflict, and
2042
2051
  // env-differing spawns from such parents shared safely before the env-aware
2043
2052
  // check. They keep sharing (and keep inheriting "none").
2044
- const requestedVariableSetId = payload.variableSetId ?? null;
2045
- const variableSetMatchesGroup = (memberVariableSetId: string | null): boolean =>
2046
- memberVariableSetId === requestedVariableSetId;
2053
+ const requestedVariableSetIds = variableSets.map((variableSet) => variableSet.id);
2054
+ const variableSetsMatchGroup = (memberVariableSetIds: readonly string[]): boolean =>
2055
+ stableJson(memberVariableSetIds) === stableJson(requestedVariableSetIds);
2047
2056
  // RIG-AWARE GROUPING (M3), the exact sibling of the env-aware gate above: the
2048
2057
  // box's rig-baked setup/tooling is fixed at cold-create, so a session joining a
2049
2058
  // shared box must ride the SAME frozen rig_version_id. A mismatch is a genuine
@@ -2067,8 +2076,7 @@ export async function createSessionForRequestWithOutcome(
2067
2076
  }
2068
2077
  const parent = parentSession;
2069
2078
  const parentBoxed = parent.sandboxBackend !== "none";
2070
- const variableSetMismatch =
2071
- parentBoxed && !variableSetMatchesGroup(parent.variableSetId ?? null);
2079
+ const variableSetMismatch = parentBoxed && !variableSetsMatchGroup(parent.variableSetIds);
2072
2080
  let rigMismatch = parentBoxed && !rigVersionMatchesGroup(parent.rigVersionId ?? null);
2073
2081
  if (parentBoxed && !rigMismatch) {
2074
2082
  const memberRigVersionIds = await listDistinctRigVersionIdsInGroup(
@@ -2113,14 +2121,14 @@ export async function createSessionForRequestWithOutcome(
2113
2121
  // the join verdict nondeterministic. Post-env-aware groups are homogeneous
2114
2122
  // (both join paths enforce equality), so this reads one distinct value in
2115
2123
  // the common case; a mixed legacy group deterministically rejects.
2116
- const memberVariableSetIds = await listDistinctVariableSetIdsInGroup(
2124
+ const memberVariableSetSelections = await listDistinctVariableSetSelectionsInGroup(
2117
2125
  db,
2118
2126
  workspaceId,
2119
2127
  sandboxChoice.groupId,
2120
2128
  );
2121
2129
  if (
2122
- !memberVariableSetIds.every((memberVariableSetId) =>
2123
- variableSetMatchesGroup(memberVariableSetId),
2130
+ !memberVariableSetSelections.every((memberVariableSetIds) =>
2131
+ variableSetsMatchGroup(memberVariableSetIds),
2124
2132
  )
2125
2133
  ) {
2126
2134
  throw new HTTPException(422, {
@@ -2263,7 +2271,11 @@ export async function createSessionForRequestWithOutcome(
2263
2271
  ...(creationInitiator.initiator ? { createdBy: creationInitiator.initiator } : {}),
2264
2272
  ...(creationInitiator.context ? { createdByContext: creationInitiator.context } : {}),
2265
2273
  createdByActor: creationInitiator.actor ?? null,
2266
- variableSet: variableSet ? { id: variableSet.id, name: variableSet.name } : null,
2274
+ variableSets: variableSets.map((variableSet) => ({
2275
+ id: variableSet.id,
2276
+ name: variableSet.name,
2277
+ scope: variableSet.scope,
2278
+ })),
2267
2279
  // Frozen rig binding (M3): both null for a rig-less session (today's path).
2268
2280
  rigId: frozenRigId,
2269
2281
  rigVersionId: frozenRigVersionId,
@@ -2663,25 +2675,18 @@ export async function updateSessionTitle(
2663
2675
  surface: "core",
2664
2676
  });
2665
2677
  const workspaceId = grant.workspaceId;
2666
- const result = await updateSessionTitleRow(db, {
2678
+ const result = await updateSessionTitleWithEvent(db, {
2667
2679
  workspaceId,
2668
2680
  sessionId,
2669
2681
  title,
2670
2682
  source,
2671
2683
  });
2672
- if (result.updated) {
2673
- await appendAndPublishEvents(db, bus, workspaceId, sessionId, [
2674
- {
2675
- type: "session.title_set",
2676
- payload: {
2677
- title: result.title ?? title,
2678
- source,
2679
- },
2680
- },
2681
- ]);
2684
+ if (result.events.length > 0) {
2685
+ await publishDurableSessionEvents(bus, workspaceId, sessionId, result.events);
2682
2686
  }
2683
2687
  return {
2684
- ...result,
2688
+ updated: result.updated,
2689
+ title: result.title,
2685
2690
  relatedSessionAccess: authorization?.relatedSessionAccess ?? "root",
2686
2691
  };
2687
2692
  }
package/src/rigs/index.ts CHANGED
@@ -252,7 +252,7 @@ export async function createRigForApi(
252
252
  description: payload.description ?? null,
253
253
  createdBy,
254
254
  initialVersion: {
255
- image: payload.image ?? null,
255
+ image: null,
256
256
  setupScript: payload.setupScript ?? null,
257
257
  checks: payload.checks,
258
258
  credentialHooks: payload.credentialHooks,
@@ -452,7 +452,7 @@ export async function promoteSetupAppendChange(
452
452
  });
453
453
  }
454
454
  const nextDefinition = {
455
- image: base.image,
455
+ image: null,
456
456
  setupScript: appendRigSetupCommand(base.setupScript, payload.command),
457
457
  checks: base.checks,
458
458
  credentialHooks: base.credentialHooks,
@@ -526,7 +526,6 @@ export async function promoteVerifiedDefinitionEditChangeForApi(
526
526
  throw new HTTPException(404, { message: "base rig version not found" });
527
527
  }
528
528
  const payload = change.payload as {
529
- image?: unknown;
530
529
  setupScript?: unknown;
531
530
  checks?: unknown;
532
531
  credentialHooks?: unknown;
@@ -534,7 +533,7 @@ export async function promoteVerifiedDefinitionEditChangeForApi(
534
533
  changelog?: unknown;
535
534
  };
536
535
  const nextDefinition = {
537
- image: payload.image === undefined ? base.image : (payload.image as string | null),
536
+ image: null,
538
537
  setupScript:
539
538
  payload.setupScript === undefined ? base.setupScript : (payload.setupScript as string | null),
540
539
  checks: Array.isArray(payload.checks) ? (payload.checks as RigVersion["checks"]) : base.checks,
@@ -606,7 +605,7 @@ export async function createRigVersionForApi(
606
605
  rig.workspaceId,
607
606
  rig.id,
608
607
  {
609
- image: payload.image === undefined ? base.image : payload.image,
608
+ image: null,
610
609
  setupScript: payload.setupScript === undefined ? base.setupScript : payload.setupScript,
611
610
  checks: payload.checks ?? base.checks,
612
611
  credentialHooks: payload.credentialHooks ?? base.credentialHooks,
@@ -11,7 +11,7 @@ import {
11
11
 
12
12
  export type RigProviderImageDefinition = Pick<
13
13
  RigVersion,
14
- "image" | "setupScript" | "checks" | "credentialHooks" | "defaultVariableSetIds"
14
+ "setupScript" | "checks" | "credentialHooks" | "defaultVariableSetIds"
15
15
  >;
16
16
 
17
17
  function sha256(value: string): string {
@@ -42,10 +42,9 @@ export function rigProviderImageContentHash(input: {
42
42
  }): string {
43
43
  return sha256(
44
44
  stableJson({
45
- version: 1,
45
+ version: 2,
46
46
  backend: input.backend,
47
47
  sourceImage: input.sourceImage,
48
- image: input.definition.image,
49
48
  setupScript: input.definition.setupScript,
50
49
  checks: input.definition.checks,
51
50
  credentialHooks: input.definition.credentialHooks,
@@ -83,13 +83,11 @@ export function settingsWithPackSandboxImage(
83
83
  }
84
84
 
85
85
  export function settingsWithRigImage(settings: Settings, rigImage: string | null): Settings {
86
- if (!rigImage) return settings;
87
- return {
88
- ...settings,
89
- dockerImage: rigImage,
90
- modalImageRef: rigImage,
91
- modalImageId: undefined,
92
- };
86
+ // Explicit Rig image overrides are intentionally inert. Preserve this helper
87
+ // as a compatibility seam for callers and historical rows while ensuring the
88
+ // deployment-owned platform image remains authoritative.
89
+ void rigImage;
90
+ return settings;
93
91
  }
94
92
 
95
93
  export function rigProviderImageSourceImage(
@@ -250,10 +248,12 @@ export async function resolveSessionSandboxRuntime(
250
248
  throw new Error(`Frozen rig version ${session.rigVersionId} is unavailable`);
251
249
  }
252
250
  const legacy = legacySandboxRuntimeFromPacks(packs);
253
- const logicalSettings = settingsWithRigImage(
254
- settingsWithPackSandboxImage(settings, legacy.sandboxImage, legacy.sandboxProviderImages),
255
- rigVersion?.image ?? null,
256
- );
251
+ // A Rig is always a setup/check layer over the deployment-owned platform
252
+ // sandbox. Legacy pre-v2 Pack image compatibility remains available only to
253
+ // rig-less sessions; it cannot replace the base beneath a Rig.
254
+ const logicalSettings = rigVersion
255
+ ? settings
256
+ : settingsWithPackSandboxImage(settings, legacy.sandboxImage, legacy.sandboxProviderImages);
257
257
  return {
258
258
  settings: {
259
259
  ...logicalSettings,