@opengeni/core 2.10.0-canary.0 → 2.10.0-canary.2

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 (50) hide show
  1. package/dist/application/external-workspace-members.d.ts +16 -0
  2. package/dist/application/host-mcp-resolvers.d.ts +13 -0
  3. package/dist/application/new-session-drafts.d.ts +4 -2
  4. package/dist/chunk-5WZYFL45.js +266 -0
  5. package/dist/chunk-5WZYFL45.js.map +1 -0
  6. package/dist/domain/file-owner.d.ts +8 -0
  7. package/dist/domain/knowledge-files.d.ts +20 -0
  8. package/dist/domain/knowledge-messages.d.ts +39 -0
  9. package/dist/domain/knowledge-messages.test.d.ts +1 -0
  10. package/dist/domain/knowledge-search.d.ts +80 -0
  11. package/dist/domain/knowledge-search.test.d.ts +1 -0
  12. package/dist/domain/knowledge.d.ts +9 -0
  13. package/dist/domain/pr-review.d.ts +2 -2
  14. package/dist/domain/resources.d.ts +2 -2
  15. package/dist/domain/scheduled-tasks.d.ts +12 -2
  16. package/dist/domain/sessions.d.ts +6 -2
  17. package/dist/domain/skills.d.ts +16 -0
  18. package/dist/index.d.ts +6 -4
  19. package/dist/index.js +3948 -3939
  20. package/dist/index.js.map +1 -1
  21. package/dist/remote-mcp-credentials.d.ts +16 -1
  22. package/dist/remote-mcp-credentials.js +7 -215
  23. package/dist/remote-mcp-credentials.js.map +1 -1
  24. package/package.json +12 -12
  25. package/src/application/external-workspace-members.ts +89 -0
  26. package/src/application/host-mcp-resolvers.ts +85 -0
  27. package/src/application/new-session-drafts.ts +37 -3
  28. package/src/domain/file-owner.ts +52 -0
  29. package/src/domain/host-mcp-task-admission.ts +27 -17
  30. package/src/domain/knowledge-files.ts +39 -0
  31. package/src/domain/knowledge-messages.test.ts +91 -0
  32. package/src/domain/knowledge-messages.ts +105 -0
  33. package/src/domain/knowledge-search.test.ts +45 -0
  34. package/src/domain/knowledge-search.ts +55 -0
  35. package/src/domain/knowledge.ts +133 -0
  36. package/src/domain/product-integration-skill.gen.ts +7 -7
  37. package/src/domain/resources.ts +36 -25
  38. package/src/domain/scheduled-tasks.ts +362 -139
  39. package/src/domain/sessions.ts +117 -20
  40. package/src/domain/skills.ts +8 -0
  41. package/src/index.ts +9 -4
  42. package/src/remote-mcp-credentials.ts +76 -8
  43. package/dist/domain/company-brain-governed-writes.d.ts +0 -79
  44. package/dist/domain/governed-learning-activation.d.ts +0 -16
  45. package/dist/domain/governed-learning-evaluator.d.ts +0 -6
  46. package/dist/domain/remember.d.ts +0 -72
  47. package/src/domain/company-brain-governed-writes.ts +0 -346
  48. package/src/domain/governed-learning-activation.ts +0 -44
  49. package/src/domain/governed-learning-evaluator.ts +0 -49
  50. package/src/domain/remember.ts +0 -577
@@ -1,3 +1,7 @@
1
+ import {
2
+ scheduledTaskKnowledgeSource,
3
+ requireScheduledTaskKnowledgeSource,
4
+ } from "@opengeni/contracts";
1
5
  import {
2
6
  allowedFirstPartyMcpToolsForSession,
3
7
  resolveFirstPartyMcpToolPolicy,
@@ -28,6 +32,10 @@ import {
28
32
  } from "@opengeni/contracts";
29
33
  import {
30
34
  createScheduledTask,
35
+ getAgentLearningSettings,
36
+ saveAgentLearningSettings,
37
+ type KnowledgeContext,
38
+ dbSql,
31
39
  deleteScheduledTask,
32
40
  getConnectionMetadata,
33
41
  getEnrollment,
@@ -42,6 +50,8 @@ import {
42
50
  getSandbox,
43
51
  getSessionTurnXaiProviderAccountAuthoritySnapshot,
44
52
  getSession,
53
+ getSessionAuthorityProjection,
54
+ withSessionRlsActorContext,
45
55
  nestedPostgresSqlState,
46
56
  requireWorkspace,
47
57
  scopedKnowledgeScopeKey,
@@ -54,6 +64,8 @@ import {
54
64
  type UpdateScheduledTaskInput,
55
65
  } from "@opengeni/db";
56
66
  import { HTTPException } from "hono/http-exception";
67
+ import { knowledgeContextForAccess } from "./knowledge";
68
+ import { fileOwnerContextForAccess, fileOwnerContextForAgent } from "./file-owner";
57
69
  import { isDeepStrictEqual } from "node:util";
58
70
  import { hasPermission, requirePermission, type AccessGrantAuthorization } from "../access";
59
71
  import {
@@ -173,6 +185,22 @@ export async function createValidatedScheduledTask(input: {
173
185
  sessionAuthorization?: SessionAuthorizationPort | null | undefined;
174
186
  authorizationSurface?: SessionAuthorizationSurface | undefined;
175
187
  }): Promise<ScheduledTask> {
188
+ const learning = "agentLearning" in input.payload ? input.payload.agentLearning : undefined;
189
+ const learningContext =
190
+ learning && input.authorization
191
+ ? await knowledgeContextForAccess(
192
+ { db: input.db },
193
+ input.authorization,
194
+ "scheduled_tasks:manage",
195
+ )
196
+ : null;
197
+ if (learning && learningContext?.actor.kind !== "human") {
198
+ throw new HTTPException(403, {
199
+ message: "Only an authenticated person can configure Agent learning",
200
+ });
201
+ }
202
+ if (learningContext?.actor.kind === "human" && learning)
203
+ learningContext.actor.settingsScopes = [learning.scope];
176
204
  // API parsing fills this default, but pack installers and older internal
177
205
  // callers can still invoke the shared validator with the pre-action shape.
178
206
  const action = input.payload.action ?? ({ kind: "agent_turn" } as const);
@@ -180,7 +208,7 @@ export async function createValidatedScheduledTask(input: {
180
208
  "selectedHostMcpDelegations" in input.payload
181
209
  ? input.payload.selectedHostMcpDelegations
182
210
  : undefined;
183
- const knowledgeAction = action.kind === "knowledge_source_sync" ? action : null;
211
+ const knowledgeAction = input.payload.agentConfig.knowledgeSource ?? null;
184
212
  if (knowledgeAction) {
185
213
  await validateKnowledgeSourceSyncAction({
186
214
  db: input.db,
@@ -188,27 +216,46 @@ export async function createValidatedScheduledTask(input: {
188
216
  action: knowledgeAction,
189
217
  });
190
218
  }
191
- const agentConfig: ScheduledTaskAgentConfig = knowledgeAction
192
- ? input.payload.agentConfig
193
- : await validateScheduledTaskAgentConfig({
194
- ...input,
195
- workspaceId: input.grant.workspaceId,
196
- });
219
+ const agentConfig: ScheduledTaskAgentConfig = await validateScheduledTaskAgentConfig({
220
+ ...input,
221
+ workspaceId: input.grant.workspaceId,
222
+ });
223
+ if (knowledgeAction && input.payload.overlapPolicy === "allow_concurrent")
224
+ throw new HTTPException(422, { message: "Source tasks require skip or buffer_one overlap" });
197
225
  const id = crypto.randomUUID();
198
226
  validateScheduledTaskSchedule(input.payload.schedule);
199
- const target = knowledgeAction
200
- ? null
201
- : await validateScheduledTaskTarget({
202
- db: input.db,
203
- sessionAuthorization: input.sessionAuthorization,
204
- authorizationSurface: input.authorizationSurface,
205
- grant: input.grant,
206
- targetSessionId: input.payload.targetSessionId,
207
- runMode: input.payload.runMode,
208
- variableSetId: input.payload.variableSetId,
209
- rigId: input.payload.rigId,
210
- agentConfig,
227
+ const target = await validateScheduledTaskTarget({
228
+ db: input.db,
229
+ sessionAuthorization: input.sessionAuthorization,
230
+ authorizationSurface: input.authorizationSurface,
231
+ grant: input.grant,
232
+ targetSessionId: input.payload.targetSessionId,
233
+ runMode: input.payload.runMode,
234
+ variableSetId: input.payload.variableSetId,
235
+ rigId: input.payload.rigId,
236
+ agentConfig,
237
+ });
238
+ if (
239
+ knowledgeAction?.destination.kind === "personal" &&
240
+ input.payload.runMode !== "new_session_per_run"
241
+ )
242
+ throw new HTTPException(422, {
243
+ message: "Personal source tasks create a private session for each run",
244
+ });
245
+ if (learning) {
246
+ const workspace = await requireWorkspace(input.db, input.grant.workspaceId);
247
+ const destination =
248
+ knowledgeAction?.destination.kind === "personal" ||
249
+ workspace.kind === "personal" ||
250
+ target?.tenancy?.visibility === "private" ||
251
+ target?.memoryScope === "user"
252
+ ? "personal"
253
+ : "workspace";
254
+ if (learning.scope !== destination)
255
+ throw new HTTPException(422, {
256
+ message: `This task saves Knowledge in the ${destination} scope. Set its Agent learning override for that scope.`,
211
257
  });
258
+ }
212
259
  if (!knowledgeAction) {
213
260
  await validateScheduledTaskMachineTarget({
214
261
  settings: input.settings,
@@ -218,7 +265,7 @@ export async function createValidatedScheduledTask(input: {
218
265
  agentConfig,
219
266
  });
220
267
  }
221
- if (!knowledgeAction && input.payload.variableSetId) {
268
+ if (input.payload.variableSetId) {
222
269
  await validateVariableSetAttachment(
223
270
  { settings: input.settings, db: input.db },
224
271
  input.grant,
@@ -290,7 +337,7 @@ export async function createValidatedScheduledTask(input: {
290
337
  subjectId: input.grant.subjectId,
291
338
  });
292
339
  const beforeCreateCommit =
293
- !knowledgeAction && input.payload.runMode !== "existing_session"
340
+ input.payload.runMode !== "existing_session"
294
341
  ? workspaceCustomModelCommitGuard({
295
342
  settings: input.settings,
296
343
  accountId: input.grant.accountId,
@@ -299,49 +346,60 @@ export async function createValidatedScheduledTask(input: {
299
346
  })
300
347
  : undefined;
301
348
  return await withScheduledTaskAuthorityWriteErrors(() =>
302
- createScheduledTask(input.db, {
303
- id,
304
- accountId: input.grant.accountId,
305
- workspaceId: input.grant.workspaceId,
306
- name: trimmedScheduledTaskName(input.payload.name),
307
- status: input.payload.status,
308
- schedule: input.payload.schedule,
309
- temporalScheduleId: scheduledTaskTemporalScheduleId(id),
310
- runMode: input.payload.runMode,
311
- overlapPolicy: input.payload.overlapPolicy,
312
- action,
313
- agentConfig,
314
- ...(creationInitiator.initiator ? { createdBy: creationInitiator.initiator } : {}),
315
- ...(creationInitiator.context ? { createdByContext: creationInitiator.context } : {}),
316
- createdByActor: creationInitiator.actor ?? null,
317
- ...(captureLinkAuthority ? { captureLinkAuthority } : {}),
318
- personalConnectionDelegations,
319
- xaiProviderAccountAuthoritySnapshot,
320
- creatorPolicy,
321
- targetSessionId: target?.id ?? null,
322
- variableSetId: input.payload.variableSetId ?? null,
323
- rigId: input.payload.rigId ?? null,
324
- metadata: input.payload.metadata,
325
- ...(hostSelections?.length && runtimeSettings
326
- ? {
327
- captureHostAuthority: prepareHostMcpTaskAdmission({
328
- settings: runtimeSettings,
329
- tools: target?.tools ?? agentConfig.tools,
330
- grant: input.grant,
331
- ...(input.authorization ? { authorization: input.authorization } : {}),
332
- selections: hostSelections,
333
- }),
334
- }
335
- : hostSelections === undefined && creationInitiator.actor && runtimeSettings
349
+ input.db.transaction(async (transaction) => {
350
+ const task = await createScheduledTask(transaction, {
351
+ id,
352
+ accountId: input.grant.accountId,
353
+ workspaceId: input.grant.workspaceId,
354
+ name: trimmedScheduledTaskName(input.payload.name),
355
+ status: input.payload.status,
356
+ schedule: input.payload.schedule,
357
+ temporalScheduleId: scheduledTaskTemporalScheduleId(id),
358
+ runMode: input.payload.runMode,
359
+ overlapPolicy: input.payload.overlapPolicy,
360
+ action,
361
+ agentConfig,
362
+ ...(creationInitiator.initiator ? { createdBy: creationInitiator.initiator } : {}),
363
+ ...(creationInitiator.context ? { createdByContext: creationInitiator.context } : {}),
364
+ createdByActor: creationInitiator.actor ?? null,
365
+ ...(captureLinkAuthority ? { captureLinkAuthority } : {}),
366
+ personalConnectionDelegations,
367
+ xaiProviderAccountAuthoritySnapshot,
368
+ creatorPolicy,
369
+ targetSessionId: target?.id ?? null,
370
+ variableSetId: input.payload.variableSetId ?? null,
371
+ rigId: input.payload.rigId ?? null,
372
+ metadata: input.payload.metadata,
373
+ ...(hostSelections?.length && runtimeSettings
336
374
  ? {
337
- captureHostAuthority: prepareInheritedHostMcpTaskAdmission(
338
- runtimeSettings,
339
- target?.tools ?? agentConfig.tools,
340
- creationInitiator.actor,
341
- ),
375
+ captureHostAuthority: prepareHostMcpTaskAdmission({
376
+ settings: runtimeSettings,
377
+ tools: target?.tools ?? agentConfig.tools,
378
+ grant: input.grant,
379
+ ...(input.authorization ? { authorization: input.authorization } : {}),
380
+ selections: hostSelections,
381
+ }),
342
382
  }
343
- : {}),
344
- ...(beforeCreateCommit ? { beforeCreateCommit } : {}),
383
+ : hostSelections === undefined && creationInitiator.actor && runtimeSettings
384
+ ? {
385
+ captureHostAuthority: prepareInheritedHostMcpTaskAdmission(
386
+ runtimeSettings,
387
+ target?.tools ?? agentConfig.tools,
388
+ creationInitiator.actor,
389
+ ),
390
+ }
391
+ : {}),
392
+ ...(beforeCreateCommit ? { beforeCreateCommit } : {}),
393
+ });
394
+ if (learning && learningContext)
395
+ await saveAgentLearningSettings(transaction, learningContext, {
396
+ scope: learning.scope,
397
+ source: { kind: "scheduled_task", id: task.id },
398
+ operationId: crypto.randomUUID(),
399
+ expectedVersion: 0,
400
+ settings: learning.settings,
401
+ });
402
+ return task;
345
403
  }),
346
404
  );
347
405
  }
@@ -429,6 +487,10 @@ export async function withScheduledTaskAuthorityWriteErrors<T>(run: () => Promis
429
487
  try {
430
488
  return await run();
431
489
  } catch (error) {
490
+ if (nestedPostgresSqlState(error) === "40001")
491
+ throw new HTTPException(409, {
492
+ message: "Scheduled task learning settings changed. Reload the task before saving.",
493
+ });
432
494
  if (nestedPostgresSqlState(error) === "42501") {
433
495
  const detail = nestedPostgresMessage(error);
434
496
  throw new HTTPException(409, {
@@ -447,7 +509,73 @@ export async function updateScheduledTaskForApi(
447
509
  workspaceId: string,
448
510
  taskId: string,
449
511
  update: UpdateScheduledTaskInput,
512
+ learning?: {
513
+ authorization: AccessGrantAuthorization;
514
+ request: NonNullable<UpdateScheduledTaskPayload["agentLearning"]>;
515
+ restoreState: ScheduledTaskRestoreState;
516
+ },
450
517
  ): Promise<ScheduledTask> {
518
+ if (learning) {
519
+ const context = await knowledgeContextForAccess(
520
+ { db },
521
+ learning.authorization,
522
+ "scheduled_tasks:manage",
523
+ );
524
+ if (context.actor.kind !== "human")
525
+ throw new HTTPException(403, {
526
+ message: "Only an authenticated person can configure Agent learning",
527
+ });
528
+ context.actor.settingsScopes = [learning.request.scope];
529
+ return withScheduledTaskAuthorityWriteErrors(() =>
530
+ db.transaction(async (tx) => {
531
+ const source = { kind: "scheduled_task" as const, id: taskId };
532
+ const baselineScope = learning.request.baselineScope ?? learning.request.scope;
533
+ // Lock policy owners in one order before comparing the draft baseline.
534
+ // A destination change must not race an edit in its previous owner layer.
535
+ const subjectId = context.actor.kind === "human" ? context.actor.subjectId : "";
536
+ const owners = [
537
+ ...new Set(
538
+ [baselineScope, learning.request.scope].map((scope) =>
539
+ scope === "personal" ? `personal:${subjectId}` : `workspace:${workspaceId}`,
540
+ ),
541
+ ),
542
+ ].sort();
543
+ for (const owner of owners)
544
+ await tx.execute(
545
+ dbSql`SELECT pg_advisory_xact_lock(hashtextextended(${`agent-learning:${context.accountId}:${owner}`},0))`,
546
+ );
547
+ const baseline = await getAgentLearningSettings(tx, context, baselineScope, source);
548
+ if (baseline.version !== learning.request.expectedVersion)
549
+ throw new HTTPException(409, {
550
+ message: "Scheduled task learning settings changed. Reload the task before saving.",
551
+ });
552
+ const task = await updateScheduledTask(tx, workspaceId, taskId, update);
553
+ const prior = await getAgentLearningSettings(tx, context, learning.request.scope, source);
554
+ const saved = await saveAgentLearningSettings(tx, context, {
555
+ scope: learning.request.scope,
556
+ operationId: learning.request.operationId,
557
+ expectedVersion:
558
+ baselineScope === learning.request.scope
559
+ ? learning.request.expectedVersion
560
+ : prior.version,
561
+ source,
562
+ settings: {
563
+ knowledge: "inherit",
564
+ instructions: "inherit",
565
+ skills: "inherit",
566
+ ...learning.request.settings,
567
+ },
568
+ });
569
+ learning.restoreState.learning = {
570
+ context,
571
+ scope: learning.request.scope,
572
+ settings: prior.settings,
573
+ expectedVersion: saved.version,
574
+ };
575
+ return task;
576
+ }),
577
+ );
578
+ }
451
579
  return await withScheduledTaskAuthorityWriteErrors(() =>
452
580
  updateScheduledTask(db, workspaceId, taskId, update),
453
581
  );
@@ -693,53 +821,68 @@ export async function validatedScheduledTaskUpdate(input: {
693
821
  sessionAuthorization?: SessionAuthorizationPort | null | undefined;
694
822
  authorizationSurface?: SessionAuthorizationSurface | undefined;
695
823
  }): Promise<UpdateScheduledTaskInput> {
824
+ if (input.payload.agentLearning) {
825
+ const context = input.authorization
826
+ ? await knowledgeContextForAccess(
827
+ { db: input.db },
828
+ input.authorization,
829
+ "scheduled_tasks:manage",
830
+ )
831
+ : null;
832
+ if (context?.actor.kind !== "human")
833
+ throw new HTTPException(403, {
834
+ message: "Only an authenticated person can configure Agent learning",
835
+ });
836
+ }
696
837
  const update: UpdateScheduledTaskInput = {};
697
- const existingKnowledge = input.existing.action.kind === "knowledge_source_sync";
698
- if (input.payload.action && input.payload.action.kind !== input.existing.action.kind) {
699
- throw new HTTPException(409, {
700
- message: "scheduled task action kind is immutable; create a new schedule",
701
- });
838
+ const requestedKnowledgeSource = input.payload.agentConfig?.knowledgeSource ?? null;
839
+ const existingKnowledgeSource = scheduledTaskKnowledgeSource(input.existing);
840
+ // Editing an ordinary source task's prompt/settings must not orphan its
841
+ // connector binding. Deleting the task is the explicit source-disable path.
842
+ if (
843
+ input.payload.agentConfig &&
844
+ input.existing.agentConfig.knowledgeSource &&
845
+ !input.payload.agentConfig.knowledgeSource
846
+ ) {
847
+ input = {
848
+ ...input,
849
+ payload: {
850
+ ...input.payload,
851
+ agentConfig: {
852
+ ...input.payload.agentConfig,
853
+ knowledgeSource: input.existing.agentConfig.knowledgeSource,
854
+ },
855
+ },
856
+ };
702
857
  }
703
- if (existingKnowledge) {
704
- if (
705
- input.payload.agentConfig !== undefined ||
706
- input.payload.runMode !== undefined ||
707
- input.payload.targetSessionId !== undefined ||
708
- input.payload.variableSetId !== undefined ||
709
- input.payload.rigId !== undefined ||
710
- input.payload.connectionAuthorities !== undefined ||
711
- input.payload.selectedHostMcpDelegations !== undefined
712
- ) {
713
- throw new HTTPException(422, {
714
- message: "knowledge source schedules do not accept agent/session configuration",
715
- });
716
- }
717
- if (input.payload.overlapPolicy === "allow_concurrent") {
718
- throw new HTTPException(422, {
719
- message: "knowledge source schedules require skip or buffer_one overlap",
720
- });
721
- }
722
- if (input.payload.action?.kind === "knowledge_source_sync") {
723
- await validateKnowledgeSourceSyncAction({
724
- db: input.db,
725
- grant: input.grant,
726
- action: input.payload.action,
727
- });
728
- update.action = input.payload.action;
729
- }
730
- if (input.payload.name !== undefined)
731
- update.name = trimmedScheduledTaskName(input.payload.name);
732
- if (input.payload.status !== undefined) update.status = input.payload.status;
733
- if (input.payload.schedule !== undefined) {
734
- validateScheduledTaskSchedule(input.payload.schedule);
735
- update.schedule = input.payload.schedule;
736
- }
737
- if (input.payload.overlapPolicy !== undefined) {
738
- update.overlapPolicy = input.payload.overlapPolicy;
739
- }
740
- if (input.payload.metadata !== undefined) update.metadata = input.payload.metadata;
741
- return update;
858
+ if (input.payload.action && input.payload.action.kind !== "agent_turn") {
859
+ throw new HTTPException(422, { message: "Source ingestion uses an ordinary agent task" });
742
860
  }
861
+ const knowledgeSource =
862
+ input.payload.agentConfig?.knowledgeSource ?? scheduledTaskKnowledgeSource(input.existing);
863
+ if (
864
+ knowledgeSource &&
865
+ requestedKnowledgeSource &&
866
+ !isDeepStrictEqual(requestedKnowledgeSource, existingKnowledgeSource)
867
+ ) {
868
+ await validateKnowledgeSourceSyncAction({
869
+ db: input.db,
870
+ grant: input.grant,
871
+ action: knowledgeSource,
872
+ });
873
+ }
874
+ if (
875
+ knowledgeSource &&
876
+ (input.payload.overlapPolicy ?? input.existing.overlapPolicy) === "allow_concurrent"
877
+ )
878
+ throw new HTTPException(422, { message: "Source tasks require skip or buffer_one overlap" });
879
+ if (
880
+ knowledgeSource?.destination.kind === "personal" &&
881
+ (input.payload.runMode ?? input.existing.runMode) !== "new_session_per_run"
882
+ )
883
+ throw new HTTPException(422, {
884
+ message: "Personal source tasks create a private session for each run",
885
+ });
743
886
  const existingTarget = input.existing.targetSessionId;
744
887
  const nextRunMode = input.payload.runMode ?? input.existing.runMode;
745
888
  const nextTargetSessionId =
@@ -846,7 +989,15 @@ export async function validatedScheduledTaskUpdate(input: {
846
989
  objectStorage: input.objectStorage,
847
990
  grant: input.grant,
848
991
  workspaceId: input.existing.workspaceId,
849
- payload: { agentConfig: input.payload.agentConfig },
992
+ ...(input.authorization ? { authorization: input.authorization } : {}),
993
+ payload: {
994
+ agentConfig: input.payload.agentConfig,
995
+ runMode: nextRunMode,
996
+ targetSessionId:
997
+ nextRunMode === "existing_session"
998
+ ? nextTargetSessionId
999
+ : input.existing.reusableSessionId,
1000
+ },
850
1001
  ...(input.toolsProvided !== undefined ? { toolsProvided: input.toolsProvided } : {}),
851
1002
  });
852
1003
  if (
@@ -1038,13 +1189,15 @@ export async function validatedScheduledTaskUpdate(input: {
1038
1189
  rigId: input.payload.rigId !== undefined ? input.payload.rigId : input.existing.rigId,
1039
1190
  agentConfig: update.agentConfig ?? input.existing.agentConfig,
1040
1191
  });
1041
- await validateScheduledTaskMachineTarget({
1042
- settings: input.settings,
1043
- db: input.db,
1044
- grant: input.grant,
1045
- runMode: nextRunMode,
1046
- agentConfig: update.agentConfig ?? input.existing.agentConfig,
1047
- });
1192
+ if (!knowledgeSource) {
1193
+ await validateScheduledTaskMachineTarget({
1194
+ settings: input.settings,
1195
+ db: input.db,
1196
+ grant: input.grant,
1197
+ runMode: nextRunMode,
1198
+ agentConfig: update.agentConfig ?? input.existing.agentConfig,
1199
+ });
1200
+ }
1048
1201
  if (
1049
1202
  input.payload.targetSessionId !== undefined ||
1050
1203
  input.existing.runMode === "existing_session" ||
@@ -1099,6 +1252,12 @@ export async function requireScheduledTaskForApi(
1099
1252
  export type ScheduledTaskRestoreState = {
1100
1253
  task: ScheduledTask;
1101
1254
  personalConnectionDelegations: McpPersonalConnectionDelegation[];
1255
+ learning?: {
1256
+ context: KnowledgeContext;
1257
+ scope: "workspace" | "personal";
1258
+ settings: import("@opengeni/contracts").AgentLearningOverridePatch;
1259
+ expectedVersion: number;
1260
+ };
1102
1261
  };
1103
1262
 
1104
1263
  export async function captureScheduledTaskRestoreState(
@@ -1239,29 +1398,28 @@ export async function deleteScheduledTaskLifecycle(input: {
1239
1398
  if (!task) throw new HTTPException(404, { message: "Scheduled task not found" });
1240
1399
  const wasLive = task.deletedAt === null;
1241
1400
  if (
1242
- task.action.kind === "knowledge_source_sync" &&
1243
- (task.action.initiatingSubjectId !== input.subjectId ||
1244
- task.action.connection.ownerSubjectId !== input.subjectId)
1401
+ scheduledTaskKnowledgeSource(task) &&
1402
+ (requireScheduledTaskKnowledgeSource(task).initiatingSubjectId !== input.subjectId ||
1403
+ requireScheduledTaskKnowledgeSource(task).connection.ownerSubjectId !== input.subjectId)
1245
1404
  ) {
1246
1405
  throw new HTTPException(403, {
1247
1406
  message: "knowledge source schedule requires the exact initiating subject",
1248
1407
  });
1249
1408
  }
1250
- if (wasLive && task.action.kind === "knowledge_source_sync") {
1409
+ if (wasLive && scheduledTaskKnowledgeSource(task)) {
1251
1410
  await input.preflightConnectorAuthorization(task);
1252
1411
  }
1253
- const deletion =
1254
- task.action.kind === "knowledge_source_sync"
1255
- ? await (async () => {
1256
- await input.cleanupConnectorAuthorization(tx, task);
1257
- return await deleteScheduledTask(tx, input.workspaceId, task.id, {
1258
- connectorCleanupSubjectId: input.subjectId,
1259
- connectorCleanupCompleted: true,
1260
- expectedAuthorityRevision: task.authorityRevision,
1261
- expectedExecutionDigest: task.executionDigest,
1262
- });
1263
- })()
1264
- : await deleteScheduledTask(tx, input.workspaceId, task.id);
1412
+ const deletion = scheduledTaskKnowledgeSource(task)
1413
+ ? await (async () => {
1414
+ await input.cleanupConnectorAuthorization(tx, task);
1415
+ return await deleteScheduledTask(tx, input.workspaceId, task.id, {
1416
+ connectorCleanupSubjectId: input.subjectId,
1417
+ connectorCleanupCompleted: true,
1418
+ expectedAuthorityRevision: task.authorityRevision,
1419
+ expectedExecutionDigest: task.executionDigest,
1420
+ });
1421
+ })()
1422
+ : await deleteScheduledTask(tx, input.workspaceId, task.id);
1265
1423
  return { task, deletion };
1266
1424
  },
1267
1425
  );
@@ -1305,7 +1463,25 @@ export async function syncUpdatedScheduledTask(input: {
1305
1463
  } catch (error) {
1306
1464
  let persistenceRestored = true;
1307
1465
  try {
1308
- await restoreScheduledTask(input.db, input.previous);
1466
+ await input.db.transaction(async (tx) => {
1467
+ const learning = input.previous.learning;
1468
+ // Compensate only our exact settings version, while the updated task
1469
+ // still names that owner scope. A concurrent edit is never overwritten.
1470
+ if (learning)
1471
+ await saveAgentLearningSettings(tx, learning.context, {
1472
+ scope: learning.scope,
1473
+ source: { kind: "scheduled_task", id: input.task.id },
1474
+ operationId: crypto.randomUUID(),
1475
+ expectedVersion: learning.expectedVersion,
1476
+ settings: {
1477
+ knowledge: "inherit",
1478
+ instructions: "inherit",
1479
+ skills: "inherit",
1480
+ ...learning.settings,
1481
+ },
1482
+ });
1483
+ await restoreScheduledTask(tx, input.previous);
1484
+ });
1309
1485
  } catch {
1310
1486
  persistenceRestored = false;
1311
1487
  }
@@ -1367,7 +1543,12 @@ async function validateScheduledTaskAgentConfig(input: {
1367
1543
  db: Database;
1368
1544
  objectStorage: ObjectStorageDependency;
1369
1545
  grant: AccessGrant;
1370
- payload: { agentConfig: ScheduledTaskAgentConfig };
1546
+ authorization?: AccessGrantAuthorization;
1547
+ payload: {
1548
+ agentConfig: ScheduledTaskAgentConfig;
1549
+ runMode?: string;
1550
+ targetSessionId?: string | null | undefined;
1551
+ };
1371
1552
  workspaceId: string;
1372
1553
  toolsProvided?: boolean;
1373
1554
  }): Promise<ScheduledTaskAgentConfig> {
@@ -1407,6 +1588,17 @@ async function validateScheduledTaskAgentConfig(input: {
1407
1588
  );
1408
1589
  const requestedTools = validateToolRefs(input.payload.agentConfig.tools ?? [], runtimeSettings);
1409
1590
  const workspace = await requireWorkspace(input.db, input.workspaceId);
1591
+ if (
1592
+ parent?.tenancy?.visibility === "private" &&
1593
+ workspace.kind !== "personal" &&
1594
+ input.payload.runMode !== "existing_session" &&
1595
+ !input.payload.targetSessionId
1596
+ ) {
1597
+ throw new HTTPException(422, {
1598
+ message:
1599
+ "Schedule this private chat as the existing-session target, or create the task in your Personal workspace.",
1600
+ });
1601
+ }
1410
1602
  const workspaceSessionToolDefaults = resolveWorkspaceSessionToolDefaults(workspace.settings);
1411
1603
  // A task whose creator did not choose tools gets the workspace's exact
1412
1604
  // session defaults (or the deployment compatibility default), exactly like
@@ -1436,13 +1628,44 @@ async function validateScheduledTaskAgentConfig(input: {
1436
1628
  if (resources.some((resource) => resource.kind === "file") && !input.objectStorage) {
1437
1629
  throw new HTTPException(503, { message: "object storage is not configured" });
1438
1630
  }
1439
- await validateFileResources(
1440
- input.db,
1441
- input.grant.accountId,
1442
- input.workspaceId,
1443
- input.grant.subjectId,
1444
- resources,
1445
- );
1631
+ const fileActor =
1632
+ resources.some((resource) => resource.kind === "file") &&
1633
+ hasPermission(input.grant.permissions, "files:read")
1634
+ ? input.authorization
1635
+ ? await fileOwnerContextForAccess({ db: input.db }, input.authorization, "files:read")
1636
+ : input.grant.principalKind === "agent_attempt"
1637
+ ? await fileOwnerContextForAgent({ db: input.db }, input.grant, "files:read")
1638
+ : { subjectId: input.grant.subjectId, privateFileOwnerSubjectId: null }
1639
+ : { subjectId: input.grant.subjectId, privateFileOwnerSubjectId: null };
1640
+ await withSessionRlsActorContext(fileActor, async () => {
1641
+ const target =
1642
+ input.payload.targetSessionId && input.payload.runMode !== "new_session_per_run"
1643
+ ? await getSessionAuthorityProjection(
1644
+ input.db,
1645
+ input.workspaceId,
1646
+ input.payload.targetSessionId,
1647
+ )
1648
+ : null;
1649
+ const privateDestination =
1650
+ workspace.kind === "personal" ||
1651
+ (target?.visibility === "user_private" &&
1652
+ target.ownerSubjectId === fileActor.privateFileOwnerSubjectId) ||
1653
+ (target?.memoryScope === "user" &&
1654
+ target.scopeSubjectId === fileActor.privateFileOwnerSubjectId);
1655
+ await validateFileResources(
1656
+ input.db,
1657
+ input.grant.accountId,
1658
+ input.workspaceId,
1659
+ input.grant.subjectId,
1660
+ resources,
1661
+ {
1662
+ ...fileActor,
1663
+ privateFileOwnerSubjectId: privateDestination
1664
+ ? (fileActor.privateFileOwnerSubjectId ?? null)
1665
+ : null,
1666
+ },
1667
+ );
1668
+ });
1446
1669
  if (input.payload.agentConfig.slackBotConnectionId) {
1447
1670
  await validateOpenGeniSlackBotConnectionSelection(
1448
1671
  input.db,