@opengeni/api-router 2.4.2-canary.0 → 2.6.3-canary.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/dist/app.js +1 -1
  2. package/dist/auth/managed-auth-attempt-context.d.ts +5 -1
  3. package/dist/auth/managed-auth.d.ts +24 -1
  4. package/dist/{chunk-L7GVVSSQ.js → chunk-XTLI3CBH.js} +2884 -483
  5. package/dist/chunk-XTLI3CBH.js.map +1 -0
  6. package/dist/http/sse.d.ts +9 -0
  7. package/dist/index.js +104 -4
  8. package/dist/index.js.map +1 -1
  9. package/dist/integrations/slack-interactions.d.ts +1 -1
  10. package/dist/interaction-metrics.d.ts +2 -0
  11. package/dist/mcp/company-brain-governed-writes.d.ts +1 -1
  12. package/dist/mcp/company-profile-agent-admin.d.ts +6 -6
  13. package/dist/mcp/remember.d.ts +2 -2
  14. package/dist/mcp/server.d.ts +34 -0
  15. package/dist/mcp/session-view.d.ts +1 -0
  16. package/dist/mcp/session-wait.d.ts +19 -0
  17. package/dist/routes/api-keys.d.ts +2 -0
  18. package/dist/routes/browser-sessions.d.ts +1 -0
  19. package/dist/routes/computer-sessions.d.ts +12 -0
  20. package/dist/routes/managed-auth-session-sets.d.ts +7 -0
  21. package/dist/routes/sessions.d.ts +17 -5
  22. package/dist/routes/workspaces.d.ts +1 -1
  23. package/dist/sandbox/metrics-ingestion.d.ts +16 -0
  24. package/dist/work-discovery-observability.d.ts +33 -0
  25. package/dist/workspace-delete-observability.d.ts +10 -0
  26. package/package.json +19 -19
  27. package/src/app.ts +172 -20
  28. package/src/auth/managed-auth-attempt-context.ts +40 -3
  29. package/src/auth/managed-auth-session-adapter.ts +1 -0
  30. package/src/auth/managed-auth.ts +164 -4
  31. package/src/http/sse.ts +279 -45
  32. package/src/integrations/oauth-client.ts +8 -1
  33. package/src/integrations/provider-oauth.ts +12 -2
  34. package/src/interaction-metrics.ts +30 -0
  35. package/src/mcp/company-brain-governed-writes.ts +38 -23
  36. package/src/mcp/company-profile-agent-admin.ts +7 -7
  37. package/src/mcp/remember.ts +19 -8
  38. package/src/mcp/server.ts +621 -54
  39. package/src/mcp/session-wait.ts +56 -8
  40. package/src/routes/api-integrations.ts +2 -2
  41. package/src/routes/api-keys.ts +149 -7
  42. package/src/routes/browser-sessions.ts +10 -2
  43. package/src/routes/capabilities.ts +3 -3
  44. package/src/routes/codex.ts +483 -74
  45. package/src/routes/company-profile.ts +64 -0
  46. package/src/routes/computer-sessions.ts +103 -1
  47. package/src/routes/integration-facets.ts +8 -5
  48. package/src/routes/interaction-resources.ts +7 -1
  49. package/src/routes/managed-auth-session-sets.ts +199 -2
  50. package/src/routes/organization-memberships.ts +28 -8
  51. package/src/routes/packs.ts +5 -5
  52. package/src/routes/plugins.ts +2 -2
  53. package/src/routes/scheduled-tasks.ts +9 -0
  54. package/src/routes/sessions.ts +380 -87
  55. package/src/routes/skills.ts +3 -3
  56. package/src/routes/workspaces.ts +293 -54
  57. package/src/sandbox/channel-a.ts +10 -4
  58. package/src/sandbox/machines.ts +13 -6
  59. package/src/sandbox/metrics-ingestion.ts +157 -3
  60. package/src/sandbox/viewer.ts +20 -1
  61. package/src/work-discovery-observability.ts +121 -0
  62. package/src/workspace-delete-observability.ts +75 -0
  63. package/dist/chunk-L7GVVSSQ.js.map +0 -1
package/src/mcp/server.ts CHANGED
@@ -14,6 +14,7 @@ import {
14
14
  stableJson,
15
15
  compactSessionEventResult,
16
16
  sessionEventLatestClassToSemanticClass,
17
+ MemorySlackPublicationDistribution,
17
18
  SessionMcpCredentialUpdateInput,
18
19
  ToolAuthNeededPayload,
19
20
  VariableSetVariableName,
@@ -38,6 +39,7 @@ import {
38
39
  SESSION_GOAL_SUCCESS_CRITERIA_MAX_BYTES,
39
40
  SESSION_GOAL_TEXT_MAX_BYTES,
40
41
  SESSION_INSTRUCTIONS_MAX_CHARACTERS,
42
+ SESSION_TITLE_MAX_CHARACTERS,
41
43
  MAX_SELECTED_VARIABLE_SETS,
42
44
  sessionGoalUtf8Bytes,
43
45
  TASK_NOTE_LIST_DEFAULT_LIMIT,
@@ -45,6 +47,16 @@ import {
45
47
  TASK_NOTE_MAX_LIFETIME_DAYS,
46
48
  TASK_NOTE_REASON_MAX_BYTES,
47
49
  TASK_NOTE_TEXT_MAX_BYTES,
50
+ WORK_CLAIM_CANONICAL_KEY_MAX_BYTES,
51
+ WORK_CLAIM_DISPLAY_LABEL_MAX_BYTES,
52
+ WORK_CLAIM_DISCOVERY_LIMIT,
53
+ WORK_CLAIM_NAMESPACE_MAX_BYTES,
54
+ WORK_CLAIM_VERSION_VALUE_MAX_BYTES,
55
+ WORK_DISCOVERY_QUERY_MAX_CHARS,
56
+ WORK_DISCOVERY_RECENT_HOURS_MAX,
57
+ WorkClaimSubjectType,
58
+ type WorkClaimSubjectFilter,
59
+ type SessionStatus,
48
60
  SubmitHumanInputResponseRequest,
49
61
  } from "@opengeni/contracts";
50
62
  import {
@@ -80,6 +92,8 @@ import {
80
92
  readVariableSetSecretAtomically,
81
93
  recordSyncedSocialPosts,
82
94
  listVariableSets,
95
+ MEMORY_CORRECT_TOOL_DESCRIPTION,
96
+ MEMORY_SAVE_TOOL_DESCRIPTION,
83
97
  MEMORY_SEARCH_TOOL_DESCRIPTION,
84
98
  requireScheduledTask,
85
99
  requireSession,
@@ -104,10 +118,16 @@ import {
104
118
  createTaskNote,
105
119
  listTaskNotes,
106
120
  replaceTaskNote,
121
+ releaseWorkClaim,
122
+ upsertWorkClaim,
107
123
  acceptSessionHumanInputResponse,
108
124
  HumanInputResponseValidationError,
109
125
  } from "@opengeni/db";
110
- import { appendAndPublishTurnEventsFenced, publishDurableSessionEvents } from "@opengeni/events";
126
+ import {
127
+ appendAndPublishEvents,
128
+ appendAndPublishTurnEventsFenced,
129
+ publishDurableSessionEvents,
130
+ } from "@opengeni/events";
111
131
  import { allowedFirstPartyMcpToolsForSession, codemodeWorkspaceUrl } from "@opengeni/config";
112
132
  import {
113
133
  createSignedState,
@@ -129,12 +149,14 @@ import {
129
149
  authorizedSocialConnectionsForGrant,
130
150
  authorizedAtlassianConnectionsForGrant,
131
151
  buildCapabilityCatalog,
152
+ correctWorkspaceMemoryWithSlackPublication,
132
153
  nativeConnectionCapabilityRecommendations,
133
154
  requireLiveAgentAttemptAuthorization,
134
155
  requireSessionAuthorization,
135
156
  requireSessionAuthorizationListScope,
136
157
  SessionAuthorizationDeniedError,
137
158
  SessionAuthorizationUnavailableError,
159
+ saveWorkspaceMemoryWithSlackPublication,
138
160
  searchCapabilityCatalogItems,
139
161
  type ResolvedSessionAuthorization,
140
162
  } from "@opengeni/core";
@@ -179,6 +201,7 @@ import {
179
201
  ScheduledTaskSyncError,
180
202
  syncCreatedScheduledTask,
181
203
  syncUpdatedScheduledTask,
204
+ validateScheduledTaskMachineTarget,
182
205
  validateScheduledTaskTarget,
183
206
  updateScheduledTaskForApi,
184
207
  validatedScheduledTaskUpdate,
@@ -216,11 +239,13 @@ import {
216
239
  SESSION_EVENT_MCP_MAX_BYTES,
217
240
  } from "./session-view";
218
241
  import {
242
+ SESSION_WAIT_COMPLETION_EVENT_TYPES,
219
243
  SESSION_WAIT_DEFAULT_SECONDS,
220
244
  SESSION_WAIT_EVENT_TYPES,
221
245
  SESSION_WAIT_EVENTS_PER_TARGET,
222
246
  SESSION_WAIT_MAX_SECONDS,
223
247
  SESSION_WAIT_MAX_TARGETS,
248
+ sessionWaitCompletionEventMatches,
224
249
  waitForSessionChanges,
225
250
  } from "./session-wait";
226
251
  import {
@@ -251,6 +276,7 @@ import { registerCompanyProfileAgentAdminTools } from "./company-profile-agent-a
251
276
  import { registerRememberTools } from "./remember";
252
277
  import { mintSandboxCodemodeToken } from "@opengeni/runtime/sandbox";
253
278
  import { deleteScheduledTaskWithDurableCleanup } from "../scheduled-task-deletion";
279
+ import { observeWorkDiscovery, summarizeWorkDiscoveryRows } from "../work-discovery-observability";
254
280
 
255
281
  export type McpServerOptions = {
256
282
  // Origin of the HTTP request that reached the MCP route. Browser-oriented
@@ -262,6 +288,12 @@ export type McpServerOptions = {
262
288
 
263
289
  const ORCHESTRATION_FAILURE_CODE_MAX_LENGTH = 128;
264
290
  const ORCHESTRATION_FAILURE_MESSAGE_MAX_UTF8_BYTES = 1_024;
291
+ // Keep pathological raw MCP payloads away from Unicode normalization while
292
+ // leaving the shared DB normalizer authoritative for the exact post-NFKC
293
+ // code-point limit. The multiplier admits supplementary-plane characters,
294
+ // decomposed forms, and ordinary whitespace folding without reopening the
295
+ // API-wide request-body ceiling for this 200-code-point field.
296
+ const MCP_DISCOVERY_QUERY_MAX_UTF16_CODE_UNITS = WORK_DISCOVERY_QUERY_MAX_CHARS * 8;
265
297
 
266
298
  type OrchestrationToolName = "session_create" | "session_send_message";
267
299
 
@@ -396,9 +428,6 @@ const FIRST_PARTY_TOOL_AUTHORIZATION = {
396
428
  goal_complete: { sessionRequired: true, allOf: ["goals:manage"] },
397
429
  goal_pause: { sessionRequired: true, allOf: ["goals:manage"] },
398
430
  memory_search: { sessionRequired: true, allOf: ["documents:search"] },
399
- // Retired: never registered, so these are never consulted. The map must stay
400
- // total over the tool-name union, which still carries both names so that
401
- // previously written scheduled-task snapshots keep parsing.
402
431
  memory_save: { sessionRequired: true, allOf: ["documents:search"] },
403
432
  memory_correct: { sessionRequired: true, allOf: ["documents:search"] },
404
433
  preference_registry_summary: {
@@ -410,6 +439,8 @@ const FIRST_PARTY_TOOL_AUTHORIZATION = {
410
439
  task_note_save: { sessionRequired: true, allOf: ["sessions:control"] },
411
440
  task_note_archive: { sessionRequired: true, allOf: ["sessions:control"] },
412
441
  task_note_replace: { sessionRequired: true, allOf: ["sessions:control"] },
442
+ work_claim_upsert: { sessionRequired: true, allOf: ["sessions:control"] },
443
+ work_claim_release: { sessionRequired: true, allOf: ["sessions:control"] },
413
444
  knowledge_propose: { sessionRequired: true, allOf: ["documents:search"] },
414
445
  knowledge_correct: { sessionRequired: true, allOf: ["documents:search"] },
415
446
  task_note_promote_knowledge: {
@@ -791,6 +822,9 @@ export function buildOpenGeniMcpServer(
791
822
  if (sessionId !== null && exactAgentAttemptClaims(grant) !== null) {
792
823
  registerPreferenceRegistryTools(server, deps, grant, json);
793
824
  registerTaskNoteTools(server, deps, grant, sessionId, json);
825
+ if (deps.settings.workClaimMutationsEnabled) {
826
+ registerWorkClaimTools(server, deps, grant, sessionId, json);
827
+ }
794
828
  const attempt = exactAgentAttemptClaims(grant)!;
795
829
  registerCompanyBrainGovernedWriteTools({
796
830
  server,
@@ -1696,6 +1730,14 @@ export function buildOpenGeniMcpServer(
1696
1730
  agentConfig: task.agentConfig,
1697
1731
  missingTargetStatus: 404,
1698
1732
  });
1733
+ await validateScheduledTaskMachineTarget({
1734
+ settings: deps.settings,
1735
+ db: deps.db,
1736
+ grant,
1737
+ runMode: task.runMode,
1738
+ agentConfig: task.agentConfig,
1739
+ requireOnline: true,
1740
+ });
1699
1741
  await requireLimit(deps, {
1700
1742
  accountId: grant.accountId,
1701
1743
  workspaceId: grant.workspaceId,
@@ -3241,6 +3283,133 @@ function registerTaskNoteTools(
3241
3283
  );
3242
3284
  }
3243
3285
 
3286
+ function registerWorkClaimTools(
3287
+ server: McpServer,
3288
+ deps: ApiRouteDeps,
3289
+ grant: AccessGrant,
3290
+ sessionId: string,
3291
+ json: JsonResult,
3292
+ ): void {
3293
+ const attemptClaims = () => {
3294
+ const resolved = exactAgentAttemptClaims(grant);
3295
+ if (!resolved || resolved.sessionId !== sessionId) {
3296
+ throw new Error("Exact signed work-claim attempt authority is required.");
3297
+ }
3298
+ return {
3299
+ accountId: grant.accountId,
3300
+ workspaceId: grant.workspaceId,
3301
+ ...resolved,
3302
+ };
3303
+ };
3304
+ const authorize = async () => {
3305
+ await authorizeFirstPartySession(deps, grant, sessionId, "session.first_party_mcp.call");
3306
+ };
3307
+
3308
+ server.registerTool(
3309
+ "work_claim_upsert",
3310
+ {
3311
+ description:
3312
+ "Create or refresh one typed, non-exclusive claim describing this session's current external work. Claims are advisory evidence, never locks or authority. Use stable public identifiers only; never put credentials, tokens, or other secrets in claim fields. expectedRevision=0 creates a new active claim; refreshing an existing active claim requires its exact revision. Use a fresh operationId; an exact retry from a replacement attempt on the same logical turn replays safely.",
3313
+ inputSchema: {
3314
+ operationId: z4.string().uuid(),
3315
+ expectedRevision: z4.number().int().min(0),
3316
+ subjectNamespace: z4.string().min(1).max(WORK_CLAIM_NAMESPACE_MAX_BYTES),
3317
+ subjectType: z4.enum([
3318
+ "repository",
3319
+ "branch",
3320
+ "pull_request",
3321
+ "issue",
3322
+ "artifact",
3323
+ "release",
3324
+ "ci_run",
3325
+ "other",
3326
+ ]),
3327
+ canonicalKey: z4.string().min(1).max(WORK_CLAIM_CANONICAL_KEY_MAX_BYTES),
3328
+ displayLabel: z4.string().min(1).max(WORK_CLAIM_DISPLAY_LABEL_MAX_BYTES).optional(),
3329
+ role: z4.enum(["working", "reviewing", "monitoring", "delivering"]),
3330
+ versionKind: z4
3331
+ .enum([
3332
+ "git_commit",
3333
+ "branch_head",
3334
+ "pull_request_head",
3335
+ "artifact_version",
3336
+ "release_version",
3337
+ "ci_run",
3338
+ "other",
3339
+ ])
3340
+ .optional(),
3341
+ versionValue: z4.string().min(1).max(WORK_CLAIM_VERSION_VALUE_MAX_BYTES).optional(),
3342
+ },
3343
+ },
3344
+ async ({
3345
+ operationId,
3346
+ expectedRevision,
3347
+ subjectNamespace,
3348
+ subjectType,
3349
+ canonicalKey,
3350
+ displayLabel,
3351
+ role,
3352
+ versionKind,
3353
+ versionValue,
3354
+ }) => {
3355
+ await authorize();
3356
+ if ((versionKind === undefined) !== (versionValue === undefined)) {
3357
+ throw new Error("work_claim_upsert versionKind and versionValue must be supplied together");
3358
+ }
3359
+ return json(
3360
+ await upsertWorkClaim(deps.db, {
3361
+ ...attemptClaims(),
3362
+ operationId,
3363
+ expectedRevision,
3364
+ subjectNamespace,
3365
+ subjectType,
3366
+ canonicalKey,
3367
+ ...(displayLabel === undefined ? {} : { displayLabel }),
3368
+ role,
3369
+ ...(versionKind && versionValue
3370
+ ? { version: { kind: versionKind, value: versionValue } }
3371
+ : {}),
3372
+ }),
3373
+ );
3374
+ },
3375
+ );
3376
+
3377
+ server.registerTool(
3378
+ "work_claim_release",
3379
+ {
3380
+ description:
3381
+ "Release one exact active claim owned by this session. This records an immutable receipt and does not affect other sessions claiming the same subject. Use a fresh operationId and the claim's exact revision.",
3382
+ inputSchema: {
3383
+ operationId: z4.string().uuid(),
3384
+ claimId: z4.string().uuid(),
3385
+ expectedRevision: z4.number().int().min(1),
3386
+ reason: z4.enum([
3387
+ "completed",
3388
+ "cancelled",
3389
+ "failed",
3390
+ "superseded",
3391
+ "no_longer_active",
3392
+ "corrected",
3393
+ "external_state_changed",
3394
+ "other",
3395
+ ]),
3396
+ },
3397
+ },
3398
+ async ({ operationId, claimId, expectedRevision, reason }) => {
3399
+ await authorize();
3400
+ return json(
3401
+ await releaseWorkClaim(deps.db, {
3402
+ ...attemptClaims(),
3403
+ operationId,
3404
+ claimId,
3405
+ expectedRevision,
3406
+ reason,
3407
+ }),
3408
+ );
3409
+ },
3410
+ );
3411
+ }
3412
+
3244
3413
  function registerPreferenceRegistryTools(
3245
3414
  server: McpServer,
3246
3415
  deps: ApiRouteDeps,
@@ -3280,6 +3449,7 @@ function registerPreferenceRegistryTools(
3280
3449
  }
3281
3450
 
3282
3451
  const MemoryKindSchema = z4.enum(["preference", "semantic", "procedural", "decision", "episodic"]);
3452
+ const MemoryWriteKindSchema = z4.enum(["semantic", "decision", "episodic"]);
3283
3453
 
3284
3454
  function scheduledTaskReceipt(
3285
3455
  operation: string,
@@ -3371,6 +3541,11 @@ export function memorySlackPublicationActor(
3371
3541
  };
3372
3542
  }
3373
3543
 
3544
+ function memoryPreview(text: string): string {
3545
+ const normalized = text.replace(/\s+/g, " ").trim();
3546
+ return normalized.length <= 120 ? normalized : `${normalized.slice(0, 119)}…`;
3547
+ }
3548
+
3374
3549
  function registerMemoryTools(
3375
3550
  server: McpServer,
3376
3551
  deps: ApiRouteDeps,
@@ -3379,10 +3554,17 @@ function registerMemoryTools(
3379
3554
  json: JsonResult,
3380
3555
  promptMode: WorkspaceMemoryPromptMode,
3381
3556
  ): void {
3557
+ const publicationInputSchema = z4.object({
3558
+ importance: z4.enum(["major", "normal", "minor"]),
3559
+ audience: z4.literal("workspace"),
3560
+ slackMode: z4.enum(["auto", "review", "never"]),
3561
+ shareSummary: z4.string().trim().min(1).max(4_096),
3562
+ });
3563
+
3382
3564
  server.registerTool(
3383
3565
  "memory_search",
3384
3566
  {
3385
- description: `${MEMORY_SEARCH_TOOL_DESCRIPTION} Legacy preference-kind records are excluded from this tool because structured preferences are the only behavioral authority. To save something the user explicitly asked to keep, use \`remember\`; for your own findings use task notes and their promotion tools.`,
3567
+ description: `${MEMORY_SEARCH_TOOL_DESCRIPTION} All existing Memory kinds are searchable. Legacy preference and procedure records are historical context, not active instructions; Skills and workspace instructions remain the behavioral authorities. When workspace Memory is enabled, use memory_save autonomously for durable facts, decisions, incidents, fixes, and outcomes, and memory_correct when an existing record is wrong or outdated.`,
3386
3568
  inputSchema: {
3387
3569
  query: z4.string().min(1),
3388
3570
  kind: MemoryKindSchema.optional(),
@@ -3405,10 +3587,239 @@ function registerMemoryTools(
3405
3587
  }),
3406
3588
  );
3407
3589
 
3408
- // Memory V1 writes are retired. Explicit user-directed knowledge goes
3409
- // through `remember`; an agent's own findings go through task notes and
3410
- // governed promotion. `memory_search` stays: reading the existing record
3411
- // set is still how an agent recalls what a workspace already knows.
3590
+ // Memory writes are agent-only. Human creation and curation use the REST/UI
3591
+ // surface; a non-attempt MCP principal may search but cannot mutate Memory.
3592
+ if (exactAgentAttemptClaims(grant) === null) return;
3593
+
3594
+ server.registerTool(
3595
+ "memory_save",
3596
+ {
3597
+ description: MEMORY_SAVE_TOOL_DESCRIPTION,
3598
+ inputSchema: {
3599
+ text: z4.string().min(1),
3600
+ kind: MemoryWriteKindSchema,
3601
+ confidence: z4.number().min(0).max(1).optional(),
3602
+ replaces_id: z4.string().min(1).optional(),
3603
+ slack_publication: publicationInputSchema.optional(),
3604
+ },
3605
+ },
3606
+ async ({ text, kind, confidence, replaces_id, slack_publication }) => {
3607
+ const actor = await requireLiveAgentAttemptAuthorization(deps.db, grant, sessionId);
3608
+ const result = await saveWorkspaceMemoryWithSlackPublication(
3609
+ deps.db,
3610
+ {
3611
+ accountId: grant.accountId,
3612
+ workspaceId: grant.workspaceId,
3613
+ sessionId,
3614
+ text,
3615
+ kind,
3616
+ ...(confidence !== undefined ? { confidence } : {}),
3617
+ ...(replaces_id ? { replacesId: replaces_id } : {}),
3618
+ origin: "agent",
3619
+ },
3620
+ slack_publication
3621
+ ? {
3622
+ distribution: MemorySlackPublicationDistribution.parse(slack_publication),
3623
+ actor: memorySlackPublicationActor(actor, sessionId, grant.subjectLabel ?? null)
3624
+ .actor,
3625
+ ownerLabel: actor.initiator.label ?? grant.subjectLabel ?? null,
3626
+ }
3627
+ : null,
3628
+ deps.getDocumentServices().embedder,
3629
+ );
3630
+ let timelineWarning: string | null = null;
3631
+ try {
3632
+ await appendAndPublishEvents(deps.db, deps.bus, grant.workspaceId, sessionId, [
3633
+ {
3634
+ type: "memory.saved",
3635
+ payload: {
3636
+ memoryId: result.memory.id,
3637
+ kind: result.memory.kind,
3638
+ preview: memoryPreview(result.memory.text),
3639
+ deduped: result.deduped,
3640
+ ...(result.superseded ? { supersededMemoryId: result.superseded.id } : {}),
3641
+ },
3642
+ },
3643
+ ]);
3644
+ } catch {
3645
+ timelineWarning = "Memory committed, but its session timeline event could not be recorded.";
3646
+ console.warn("workspace memory save: committed without session timeline event", {
3647
+ errorClass: "MemoryTimelineOperationError",
3648
+ errorCode: "memory_save_timeline_append_failed",
3649
+ origin: "api",
3650
+ workspaceId: grant.workspaceId,
3651
+ sessionId,
3652
+ memoryId: result.memory.id,
3653
+ });
3654
+ }
3655
+ const changed = !result.deduped || result.updated || result.superseded !== null;
3656
+ const outcome =
3657
+ result.updated || result.superseded !== null
3658
+ ? "updated"
3659
+ : result.deduped
3660
+ ? "unchanged"
3661
+ : "created";
3662
+ return json(
3663
+ mcpMutationReceipt({
3664
+ operation: "memory_save",
3665
+ committed: true,
3666
+ outcome,
3667
+ changed,
3668
+ resource: {
3669
+ type: "knowledge_memory",
3670
+ id: result.memory.id,
3671
+ state: result.memory.status,
3672
+ },
3673
+ relatedResources: result.superseded
3674
+ ? [
3675
+ {
3676
+ type: "knowledge_memory",
3677
+ id: result.superseded.id,
3678
+ state: result.superseded.status,
3679
+ },
3680
+ ]
3681
+ : undefined,
3682
+ timestamp: result.memory.updatedAt,
3683
+ idempotency: { status: "not_supported" },
3684
+ warnings: [
3685
+ ...(!result.embedded
3686
+ ? ["Memory committed without a vector embedding; keyword search remains available."]
3687
+ : []),
3688
+ ...(timelineWarning ? [timelineWarning] : []),
3689
+ ],
3690
+ facts: {
3691
+ deduped: result.deduped,
3692
+ dedupeReason: result.dedupeReason,
3693
+ updatedInPlace: result.updated,
3694
+ embedded: result.embedded,
3695
+ slackPublicationDecision: result.slackPublication.decision?.eligible
3696
+ ? "eligible"
3697
+ : (result.slackPublication.decision?.reason ?? "not_requested"),
3698
+ slackPublicationId:
3699
+ result.slackPublication.enqueue?.kind === "enqueued" ||
3700
+ result.slackPublication.enqueue?.kind === "replayed"
3701
+ ? result.slackPublication.enqueue.publication.id
3702
+ : null,
3703
+ slackPublicationState:
3704
+ result.slackPublication.enqueue?.kind === "enqueued" ||
3705
+ result.slackPublication.enqueue?.kind === "replayed"
3706
+ ? result.slackPublication.enqueue.publication.state
3707
+ : null,
3708
+ },
3709
+ }),
3710
+ );
3711
+ },
3712
+ );
3713
+
3714
+ server.registerTool(
3715
+ "memory_correct",
3716
+ {
3717
+ description: MEMORY_CORRECT_TOOL_DESCRIPTION,
3718
+ inputSchema: {
3719
+ id: z4.string().min(1),
3720
+ reason: z4.string().min(1).optional(),
3721
+ replacement_text: z4.string().min(1).optional(),
3722
+ slack_publication: publicationInputSchema.optional(),
3723
+ },
3724
+ },
3725
+ async ({ id, reason, replacement_text, slack_publication }) => {
3726
+ const actor = await requireLiveAgentAttemptAuthorization(deps.db, grant, sessionId);
3727
+ const result = await correctWorkspaceMemoryWithSlackPublication(
3728
+ deps.db,
3729
+ {
3730
+ accountId: grant.accountId,
3731
+ workspaceId: grant.workspaceId,
3732
+ sessionId,
3733
+ id,
3734
+ ...(reason ? { reason } : {}),
3735
+ ...(replacement_text ? { replacementText: replacement_text } : {}),
3736
+ origin: "agent",
3737
+ },
3738
+ slack_publication
3739
+ ? {
3740
+ distribution: MemorySlackPublicationDistribution.parse(slack_publication),
3741
+ actor: memorySlackPublicationActor(actor, sessionId, grant.subjectLabel ?? null)
3742
+ .actor,
3743
+ ownerLabel: actor.initiator.label ?? grant.subjectLabel ?? null,
3744
+ }
3745
+ : null,
3746
+ deps.getDocumentServices().embedder,
3747
+ );
3748
+ let timelineWarning: string | null = null;
3749
+ try {
3750
+ await appendAndPublishEvents(deps.db, deps.bus, grant.workspaceId, sessionId, [
3751
+ {
3752
+ type: "memory.corrected",
3753
+ payload: {
3754
+ memoryId: result.memory.id,
3755
+ kind: result.memory.kind,
3756
+ preview: memoryPreview(result.memory.text),
3757
+ action: result.action,
3758
+ ...(reason ? { reason: memoryPreview(reason) } : {}),
3759
+ ...(result.replacement
3760
+ ? {
3761
+ replacementMemoryId: result.replacement.id,
3762
+ replacementPreview: memoryPreview(result.replacement.text),
3763
+ }
3764
+ : {}),
3765
+ },
3766
+ },
3767
+ ]);
3768
+ } catch {
3769
+ timelineWarning =
3770
+ "Memory correction committed, but its session timeline event could not be recorded.";
3771
+ console.warn("workspace memory correction: committed without session timeline event", {
3772
+ errorClass: "MemoryTimelineOperationError",
3773
+ errorCode: "memory_correct_timeline_append_failed",
3774
+ origin: "api",
3775
+ workspaceId: grant.workspaceId,
3776
+ sessionId,
3777
+ memoryId: result.memory.id,
3778
+ });
3779
+ }
3780
+ return json(
3781
+ mcpMutationReceipt({
3782
+ operation: "memory_correct",
3783
+ committed: true,
3784
+ outcome: "updated",
3785
+ changed: true,
3786
+ resource: {
3787
+ type: "knowledge_memory",
3788
+ id: result.memory.id,
3789
+ state: result.memory.status,
3790
+ },
3791
+ relatedResources: result.replacement
3792
+ ? [
3793
+ {
3794
+ type: "knowledge_memory",
3795
+ id: result.replacement.id,
3796
+ state: result.replacement.status,
3797
+ },
3798
+ ]
3799
+ : undefined,
3800
+ timestamp: (result.replacement ?? result.memory).updatedAt,
3801
+ idempotency: { status: "not_supported" },
3802
+ warnings: timelineWarning ? [timelineWarning] : [],
3803
+ facts: {
3804
+ correctionAction: result.action,
3805
+ slackPublicationDecision: result.slackPublication.decision?.eligible
3806
+ ? "eligible"
3807
+ : (result.slackPublication.decision?.reason ?? "not_requested"),
3808
+ slackPublicationId:
3809
+ result.slackPublication.enqueue?.kind === "enqueued" ||
3810
+ result.slackPublication.enqueue?.kind === "replayed"
3811
+ ? result.slackPublication.enqueue.publication.id
3812
+ : null,
3813
+ slackPublicationState:
3814
+ result.slackPublication.enqueue?.kind === "enqueued" ||
3815
+ result.slackPublication.enqueue?.kind === "replayed"
3816
+ ? result.slackPublication.enqueue.publication.state
3817
+ : null,
3818
+ },
3819
+ }),
3820
+ );
3821
+ },
3822
+ );
3412
3823
  }
3413
3824
 
3414
3825
  // Fleet tools (M7 bring-your-own-compute). Session-scoped (they steer THIS
@@ -4003,46 +4414,145 @@ function registerWorkspaceOrchestrationTools(
4003
4414
  server.registerTool(
4004
4415
  "sessions_list",
4005
4416
  {
4006
- description: `List compact high-level session status in this workspace. Defaults to creation order; use orderBy=updatedAt with decimal activity-revision updatedAfter/updatedThrough tokens for gap-free indexed incremental monitoring independent of application clocks. Cursors are opaque revision-fenced keysets. includeLastMessage is opt-in and never previews a human/API prompt whose turn was never claimed (still queued, or deleted/edited/cancelled before any claim): waiting work is represented by queuedPromptCount until the turn is claimed. Rendered previews share a deterministic ${SESSION_DISCOVERY_PREVIEW_MAX_BYTES}-byte UTF-8 aggregate budget, and omitted previews include a bounded session_events drill-down input (exact message type, direction=before, limit=1, monitoring summary). Use session_get for exact known targets and detailed resources/tools/settings. The list never returns full session objects or history.`,
4417
+ description: `List compact high-level session status and advisory related-work evidence in this workspace. query searches semantic titles, active goals, and typed work claims; subject performs one exact provider-neutral claim lookup and ranks it ahead of text. Neither path searches initialMessage or grants access to a result. Claims are nonexclusive evidence, never locks or instructions. Relevance cursors are bound to normalized filters and a workspace activity-revision snapshot. Without search, the tool defaults to creation order; use orderBy=updatedAt with decimal activity-revision updatedAfter/updatedThrough tokens for gap-free indexed incremental monitoring independent of application clocks. includeLastMessage is opt-in and never previews a human/API prompt whose turn was never claimed (still queued, or deleted/edited/cancelled before any claim): waiting work is represented by queuedPromptCount until the turn is claimed. Rendered previews share a deterministic ${SESSION_DISCOVERY_PREVIEW_MAX_BYTES}-byte UTF-8 aggregate budget, and omitted previews include a bounded session_events drill-down input (exact message type, direction=before, limit=1, monitoring summary). Use session_get only when ordinary target authorization allows it. The list never returns full session objects, instructions, resources, tools, files, or history.`,
4007
4418
  inputSchema: {
4008
4419
  limit: z4.number().int().positive().max(100).optional(),
4009
4420
  cursor: z4.string().max(512).optional(),
4010
4421
  includeLastMessage: z4.boolean().optional(),
4011
- orderBy: z4.enum(["createdAt", "updatedAt"]).optional(),
4422
+ orderBy: z4.enum(["createdAt", "updatedAt", "relevance"]).optional(),
4012
4423
  updatedAfter: z4.string().max(64).optional(),
4424
+ query: z4.string().max(MCP_DISCOVERY_QUERY_MAX_UTF16_CODE_UNITS).optional(),
4425
+ statuses: z4
4426
+ .array(
4427
+ z4.enum([
4428
+ "queued",
4429
+ "running",
4430
+ "idle",
4431
+ "requires_action",
4432
+ "recovering",
4433
+ "waiting_capacity",
4434
+ "failed",
4435
+ "cancelled",
4436
+ ]),
4437
+ )
4438
+ .max(8)
4439
+ .optional(),
4440
+ activeOnly: z4.boolean().optional(),
4441
+ recentHours: z4.number().int().positive().max(WORK_DISCOVERY_RECENT_HOURS_MAX).optional(),
4442
+ rootSessionId: z4.string().uuid().optional(),
4443
+ parentSessionId: z4.string().uuid().nullable().optional(),
4444
+ subject: z4
4445
+ .object({
4446
+ namespace: z4.string().min(1).max(WORK_CLAIM_NAMESPACE_MAX_BYTES),
4447
+ type: WorkClaimSubjectType,
4448
+ canonicalKey: z4.string().min(1).max(WORK_CLAIM_CANONICAL_KEY_MAX_BYTES),
4449
+ })
4450
+ .strict()
4451
+ .optional(),
4452
+ claimLimit: z4.number().int().positive().max(WORK_CLAIM_DISCOVERY_LIMIT).optional(),
4013
4453
  },
4014
4454
  },
4015
- async ({ limit, cursor, includeLastMessage, orderBy: requestedOrderBy, updatedAfter }) => {
4455
+ async ({
4456
+ limit,
4457
+ cursor,
4458
+ includeLastMessage,
4459
+ orderBy: requestedOrderBy,
4460
+ updatedAfter,
4461
+ query,
4462
+ statuses,
4463
+ activeOnly,
4464
+ recentHours,
4465
+ rootSessionId,
4466
+ parentSessionId,
4467
+ subject,
4468
+ claimLimit,
4469
+ }) => {
4016
4470
  const authorizationScope = await requireSessionAuthorizationListScope(
4017
4471
  deps,
4018
4472
  grant,
4019
4473
  "first_party_mcp",
4020
4474
  );
4475
+ const startedAtMs = performance.now();
4476
+ const mode = subject ? "subject" : query?.trim() ? "query" : "browse";
4477
+ const metricAuthorizationScope =
4478
+ authorizationScope?.kind === "scoped" ? "scoped" : "workspace";
4021
4479
  const decodedCursor = cursor ? decodeSessionDiscoveryCursor(cursor) : undefined;
4022
- const orderBy: SessionDiscoveryOrderBy =
4023
- requestedOrderBy ?? decodedCursor?.orderBy ?? "createdAt";
4024
- if (decodedCursor && decodedCursor.orderBy !== orderBy) {
4025
- throw new Error("sessions_list cursor order does not match orderBy");
4026
- }
4027
- const normalizedUpdatedAfter =
4028
- updatedAfter !== undefined
4029
- ? normalizeSessionDiscoveryRevision(updatedAfter, "updatedAfter")
4030
- : (decodedCursor?.updatedAfter ?? undefined);
4031
- if (normalizedUpdatedAfter !== undefined && orderBy !== "updatedAt") {
4032
- throw new Error("sessions_list updatedAfter requires orderBy=updatedAt");
4480
+ const relevanceRequested = Boolean(query?.trim() || subject);
4481
+ if (relevanceRequested && !deps.settings.workDiscoveryEnabled) {
4482
+ observeWorkDiscovery(deps.observability, {
4483
+ surface: "first_party_mcp",
4484
+ mode,
4485
+ outcome: "disabled",
4486
+ authorizationScope: metricAuthorizationScope,
4487
+ durationMs: performance.now() - startedAtMs,
4488
+ responseBytes: 0,
4489
+ resultCount: 0,
4490
+ overlapCount: 0,
4491
+ matchCounts: {},
4492
+ });
4493
+ throw new Error("sessions_list work discovery is disabled by the operator");
4033
4494
  }
4034
- if (decodedCursor && decodedCursor.updatedAfter !== (normalizedUpdatedAfter ?? null)) {
4035
- throw new Error("sessions_list cursor does not match updatedAfter");
4495
+ try {
4496
+ const orderBy: SessionDiscoveryOrderBy =
4497
+ requestedOrderBy ??
4498
+ decodedCursor?.orderBy ??
4499
+ (relevanceRequested ? "relevance" : "createdAt");
4500
+ if (decodedCursor && decodedCursor.orderBy !== orderBy) {
4501
+ throw new Error("sessions_list cursor order does not match orderBy");
4502
+ }
4503
+ const normalizedUpdatedAfter =
4504
+ updatedAfter !== undefined
4505
+ ? normalizeSessionDiscoveryRevision(updatedAfter, "updatedAfter")
4506
+ : (decodedCursor?.updatedAfter ?? undefined);
4507
+ if (normalizedUpdatedAfter !== undefined && orderBy !== "updatedAt") {
4508
+ throw new Error("sessions_list updatedAfter requires orderBy=updatedAt");
4509
+ }
4510
+ if (decodedCursor && decodedCursor.updatedAfter !== (normalizedUpdatedAfter ?? null)) {
4511
+ throw new Error("sessions_list cursor does not match updatedAfter");
4512
+ }
4513
+ const page = await listSessionDiscoverySummaries(deps.db, grant.workspaceId, {
4514
+ limit: boundedSessionDiscoveryLimit(limit),
4515
+ ...(decodedCursor ? { cursor: decodedCursor } : {}),
4516
+ includeLastMessage: includeLastMessage === true,
4517
+ orderBy,
4518
+ ...(normalizedUpdatedAfter ? { updatedAfter: normalizedUpdatedAfter } : {}),
4519
+ ...(query?.trim() ? { query } : {}),
4520
+ ...(statuses ? { statuses: statuses as SessionStatus[] } : {}),
4521
+ activeOnly: activeOnly === true,
4522
+ ...(recentHours !== undefined ? { recentHours } : {}),
4523
+ ...(rootSessionId ? { rootSessionId } : {}),
4524
+ ...(parentSessionId !== undefined ? { parentSessionId } : {}),
4525
+ ...(subject ? { subject: subject as WorkClaimSubjectFilter } : {}),
4526
+ ...(claimLimit !== undefined ? { claimLimit } : {}),
4527
+ includeWorkDiscovery: deps.settings.workDiscoveryEnabled,
4528
+ subjectId: grant.subjectId,
4529
+ ...(authorizationScope ? { authorizationScope } : {}),
4530
+ });
4531
+ const result = capSessionDiscoveryPage(page, includeLastMessage === true);
4532
+ observeWorkDiscovery(deps.observability, {
4533
+ surface: "first_party_mcp",
4534
+ mode,
4535
+ outcome: result.sessions.length === 0 ? "empty" : "ok",
4536
+ authorizationScope: metricAuthorizationScope,
4537
+ durationMs: performance.now() - startedAtMs,
4538
+ responseBytes: result.bytes,
4539
+ ...summarizeWorkDiscoveryRows(result.sessions),
4540
+ });
4541
+ return json(result);
4542
+ } catch (error) {
4543
+ observeWorkDiscovery(deps.observability, {
4544
+ surface: "first_party_mcp",
4545
+ mode,
4546
+ outcome: "error",
4547
+ authorizationScope: metricAuthorizationScope,
4548
+ durationMs: performance.now() - startedAtMs,
4549
+ responseBytes: 0,
4550
+ resultCount: 0,
4551
+ overlapCount: 0,
4552
+ matchCounts: {},
4553
+ });
4554
+ throw error;
4036
4555
  }
4037
- const page = await listSessionDiscoverySummaries(deps.db, grant.workspaceId, {
4038
- limit: boundedSessionDiscoveryLimit(limit),
4039
- ...(decodedCursor ? { cursor: decodedCursor } : {}),
4040
- includeLastMessage: includeLastMessage === true,
4041
- orderBy,
4042
- ...(normalizedUpdatedAfter ? { updatedAfter: normalizedUpdatedAfter } : {}),
4043
- ...(authorizationScope ? { authorizationScope } : {}),
4044
- });
4045
- return json(capSessionDiscoveryPage(page, includeLastMessage === true));
4046
4556
  },
4047
4557
  );
4048
4558
 
@@ -4199,7 +4709,7 @@ function registerWorkspaceOrchestrationTools(
4199
4709
  server.registerTool(
4200
4710
  "session_wait",
4201
4711
  {
4202
- description: `Block until a watched session has new durable events after your cursor, until your own session has pending machine input (a child result, an agent message, a steer), or until maxWaitSeconds (default ${SESSION_WAIT_DEFAULT_SECONDS}, max ${SESSION_WAIT_MAX_SECONDS}) elapses. Use this for short waits inside the current turn instead of sleeping and polling session_events/session_get/sessions_list while a child or peer session works; for long waits end this turn with goal_wait rather than looping session_wait for hours while holding the turn and sandbox. Pass each target's sessionId and afterSequence (its last seen sequence, 0 for a new session); returns immediately when anything already changed. Only turn lifecycle, agent.message.completed, blocking failures, goal facts, and session status/control changes count as a change; raw deltas, tool receipts, and sandbox diagnostics never wake it. Each changed target returns a bounded compact summary of up to ${SESSION_WAIT_EVENTS_PER_TARGET} exact durable events plus latestSequence (pass it back as the next afterSequence) and hasMore (drill down with session_events after=latestSequence). ownPendingUpdates > 0 means your own session has machine input that is delivered only when your next turn is claimed: finish this turn to receive it, or pass includeOwnPendingUpdates=false to keep waiting on the targets. timedOut=true means nothing changed; liveFanout=false means the live bus was unavailable and the wait relied on the deadline re-check. The whole result is byte-bounded: summaries are shortened first, then newest rows dropped, so a changed target may come back with events=[] and hasMore=true; read those rows with session_events after=latestSequence. The wait cannot exceed ${SESSION_WAIT_MAX_SECONDS} seconds because the MCP client request timeout is 60 seconds.`,
4712
+ description: `Block until a watched session has new durable events after your cursor, until your own session has pending machine input (a child result, an agent message, a steer), or until maxWaitSeconds (default ${SESSION_WAIT_DEFAULT_SECONDS}, max ${SESSION_WAIT_MAX_SECONDS}) elapses. Use this for short waits inside the current turn instead of sleeping and polling session_events/session_get/sessions_list while a child or peer session works; for long waits end this turn with goal_wait rather than looping session_wait for hours while holding the turn and sandbox. Pass each target's sessionId and afterSequence (its last seen sequence, 0 for a new session). waitFor=change is the backward-compatible default and returns on turn lifecycle, agent.message.completed, blocking failures, goal facts, or session status/control changes. waitFor=completion is the child-result join: it ignores progress, completed commentary messages, goal facts, maintenance turns, and continuation segment settlements and returns only for a result-bearing final turn or a blocking state. A goal.completed event records goal state but is not a terminal child result. Raw deltas, tool receipts, sandbox diagnostics, and unrelated progress never wake either mode. Each changed target returns a bounded compact summary of up to ${SESSION_WAIT_EVENTS_PER_TARGET} exact durable events plus latestSequence (pass it back as the next afterSequence) and hasMore (drill down with session_events after=latestSequence). ownPendingUpdates > 0 means your own session has machine input that is delivered only when your next turn is claimed: finish this turn to receive it, or pass includeOwnPendingUpdates=false to keep waiting on the targets. timedOut=true means nothing changed; liveFanout=false means the live bus was unavailable and the wait relied on the deadline re-check. The whole result is byte-bounded: summaries are shortened first, then newest rows dropped, so a changed target may come back with events=[] and hasMore=true; read those rows with session_events after=latestSequence. The wait cannot exceed ${SESSION_WAIT_MAX_SECONDS} seconds because the MCP client request timeout is 60 seconds.`,
4203
4713
  inputSchema: {
4204
4714
  targets: z4
4205
4715
  .array(
@@ -4216,10 +4726,16 @@ function registerWorkspaceOrchestrationTools(
4216
4726
  .describe(
4217
4727
  "Also return when your own session has pending machine input (default true).",
4218
4728
  ),
4729
+ waitFor: z4
4730
+ .enum(["change", "completion"])
4731
+ .optional()
4732
+ .describe(
4733
+ "change (default) returns on relevant activity; completion ignores messages, goal/progress, maintenance, and continuation segments until a result-bearing final turn or blocker.",
4734
+ ),
4219
4735
  maxWaitSeconds: z4.number().int().min(1).max(SESSION_WAIT_MAX_SECONDS).optional(),
4220
4736
  },
4221
4737
  },
4222
- async ({ targets, includeOwnPendingUpdates, maxWaitSeconds }, extra) => {
4738
+ async ({ targets, includeOwnPendingUpdates, waitFor, maxWaitSeconds }, extra) => {
4223
4739
  const distinct = new Set(targets.map((target) => target.sessionId));
4224
4740
  if (distinct.size !== targets.length) {
4225
4741
  throw new Error("session_wait targets must name distinct sessions");
@@ -4231,6 +4747,8 @@ function registerWorkspaceOrchestrationTools(
4231
4747
  await requireSession(deps.db, grant.workspaceId, target.sessionId);
4232
4748
  }
4233
4749
  const ownSessionId = includeOwnPendingUpdates === false ? null : callerSessionId;
4750
+ const targetEventTypes =
4751
+ waitFor === "completion" ? SESSION_WAIT_COMPLETION_EVENT_TYPES : SESSION_WAIT_EVENT_TYPES;
4234
4752
  // The API serves one transport per POST, so the worker's MCP cancel
4235
4753
  // notification never reaches this handler; the route binds the HTTP
4236
4754
  // request's abort to transport.close() (mcp/request-abort.ts), which
@@ -4247,6 +4765,9 @@ function registerWorkspaceOrchestrationTools(
4247
4765
  targets,
4248
4766
  ownSessionId,
4249
4767
  maxWaitMs: (maxWaitSeconds ?? SESSION_WAIT_DEFAULT_SECONDS) * 1_000,
4768
+ targetEventTypes,
4769
+ targetEventMatches:
4770
+ waitFor === "completion" ? sessionWaitCompletionEventMatches : undefined,
4250
4771
  signal,
4251
4772
  source: {
4252
4773
  reauthorizeTargets: async (sessionIds) => {
@@ -4265,7 +4786,7 @@ function registerWorkspaceOrchestrationTools(
4265
4786
  direction: "after",
4266
4787
  limit: SESSION_WAIT_EVENTS_PER_TARGET,
4267
4788
  payloadMode: "full",
4268
- includeTypes: SESSION_WAIT_EVENT_TYPES,
4789
+ includeTypes: targetEventTypes,
4269
4790
  maxBytes: SESSION_EVENT_MCP_MAX_BYTES * 4,
4270
4791
  });
4271
4792
  return { events: page.events, hasMore: page.hasMore };
@@ -4294,6 +4815,14 @@ function registerWorkspaceOrchestrationTools(
4294
4815
  const sessionCreateInput = z4
4295
4816
  .object({
4296
4817
  initialMessage: z4.string().min(1),
4818
+ title: z4
4819
+ .string()
4820
+ .min(1)
4821
+ .max(SESSION_TITLE_MAX_CHARACTERS)
4822
+ .optional()
4823
+ .describe(
4824
+ "Concise semantic title for the child session. Omit only when the delegated goal or initial message already provides a suitable title; OpenGeni derives a sensitive-safe bounded fallback from that text.",
4825
+ ),
4297
4826
  instructions: z4.string().min(1).max(SESSION_INSTRUCTIONS_MAX_CHARACTERS).optional(),
4298
4827
  goal: z4.unknown().optional(),
4299
4828
  resources: z4.array(z4.unknown()).optional(),
@@ -4376,7 +4905,7 @@ function registerWorkspaceOrchestrationTools(
4376
4905
  "session_create",
4377
4906
  {
4378
4907
  description:
4379
- "Spawn a new agent session (a worker). The child inherits this session's visibility; a private session can only create a same-owner private child. Give a goal-bearing child its delegated objective. Its goal.rootConstraints may be an exact applicable subset of this accepted turn's frozen root constraints; omit that field to inherit all of them. Omit sandbox for the safe default: compatible children share the creator's box, while a different Variable Set, Rig, or machineTarget gets its own box. Use 'new' for deliberate isolation or {groupId} for a strict compatible sibling join. Put targetSandboxId and its optional workingDir together inside machineTarget; a machineTarget is always an own-box create even when the parent is backend none. To create a non-delegating leaf, pass a narrowed firstPartyMcpTools list that omits session_create; do not use a child-local depth override. Public REST/SDK callers retain advanced absolute depth and explicit shared-placement controls.",
4908
+ "Spawn a new agent session (a worker) only for a concrete, bounded subtask that can run independently and has a defined integration point in your current work. Do not delegate work you will also perform yourself; track the child and join its actual result before completing dependent work. Give the child a concise semantic title; if omitted, OpenGeni derives one from its delegated goal or initial message. The child inherits this session's visibility; a private session can only create a same-owner private child. Give a goal-bearing child its delegated objective. Its goal.rootConstraints may be an exact applicable subset of this accepted turn's frozen root constraints; omit that field to inherit all of them. Omit sandbox for the safe default: compatible children share the creator's box, while a different Variable Set, Rig, or machineTarget gets its own box. Use 'new' for deliberate isolation or {groupId} for a strict compatible sibling join. Put targetSandboxId and its optional workingDir together inside machineTarget; a machineTarget is always an own-box create even when the parent is backend none. To create a non-delegating leaf, pass a narrowed firstPartyMcpTools list that omits session_create; do not use a child-local depth override. Public REST/SDK callers retain advanced absolute depth and explicit shared-placement controls.",
4380
4909
  inputSchema: sessionCreateInput,
4381
4910
  },
4382
4911
  async (args) => {
@@ -4389,18 +4918,25 @@ function registerWorkspaceOrchestrationTools(
4389
4918
  if (callerSessionId !== null) {
4390
4919
  await authorizeFirstPartySession(deps, grant, callerSessionId, "session.child.create");
4391
4920
  }
4392
- const { machineTarget, ...request } = args;
4393
- const result = await createSessionForRequestWithOutcome(deps, grant, grant.workspaceId, {
4394
- ...request,
4395
- ...(machineTarget
4396
- ? {
4397
- targetSandboxId: machineTarget.targetSandboxId,
4398
- ...(machineTarget.workingDir !== undefined
4399
- ? { workingDir: machineTarget.workingDir }
4400
- : {}),
4401
- }
4402
- : {}),
4403
- });
4921
+ const { machineTarget, title, ...request } = args;
4922
+ const result = await createSessionForRequestWithOutcome(
4923
+ deps,
4924
+ grant,
4925
+ grant.workspaceId,
4926
+ {
4927
+ ...request,
4928
+ ...(machineTarget
4929
+ ? {
4930
+ targetSandboxId: machineTarget.targetSandboxId,
4931
+ ...(machineTarget.workingDir !== undefined
4932
+ ? { workingDir: machineTarget.workingDir }
4933
+ : {}),
4934
+ }
4935
+ : {}),
4936
+ },
4937
+ undefined,
4938
+ title === undefined ? {} : { automaticTitleCandidate: title },
4939
+ );
4404
4940
  return json(sessionCreateMutationReceipt(result, Boolean(request.idempotencyKey)));
4405
4941
  } catch (error) {
4406
4942
  return orchestrationFailureResult("session_create", error);
@@ -5470,10 +6006,12 @@ function boundedSessionDiscoveryLimit(limit: number | undefined): number {
5470
6006
  }
5471
6007
 
5472
6008
  export function encodeSessionDiscoveryCursor(cursor: SessionDiscoveryCursor): string {
6009
+ const relevance = cursor.orderBy === "relevance";
5473
6010
  return Buffer.from(
5474
6011
  JSON.stringify({
5475
- v: 2,
6012
+ v: relevance ? 3 : 2,
5476
6013
  orderBy: cursor.orderBy,
6014
+ ...(relevance ? { sortRank: cursor.sortRank, filterHash: cursor.filterHash } : {}),
5477
6015
  sortRevision: cursor.sortRevision,
5478
6016
  sortAt: cursor.sortAt,
5479
6017
  id: cursor.id,
@@ -5523,6 +6061,8 @@ export function decodeSessionDiscoveryCursor(value: string): SessionDiscoveryCur
5523
6061
  snapshotAt?: unknown;
5524
6062
  snapshotRevision?: unknown;
5525
6063
  updatedAfter?: unknown;
6064
+ sortRank?: unknown;
6065
+ filterHash?: unknown;
5526
6066
  };
5527
6067
  if (
5528
6068
  parsed.v === undefined &&
@@ -5536,12 +6076,14 @@ export function decodeSessionDiscoveryCursor(value: string): SessionDiscoveryCur
5536
6076
  );
5537
6077
  return {
5538
6078
  orderBy: "createdAt",
6079
+ sortRank: null,
5539
6080
  sortRevision: "0",
5540
6081
  sortAt: createdAt,
5541
6082
  id: parsed.id,
5542
6083
  snapshotAt: createdAt,
5543
6084
  snapshotRevision: "0",
5544
6085
  updatedAfter: null,
6086
+ filterHash: null,
5545
6087
  };
5546
6088
  }
5547
6089
  // The timestamp-fenced v1 format was never safe for updated-order
@@ -5558,17 +6100,22 @@ export function decodeSessionDiscoveryCursor(value: string): SessionDiscoveryCur
5558
6100
  ) {
5559
6101
  return {
5560
6102
  orderBy: "createdAt",
6103
+ sortRank: null,
5561
6104
  sortRevision: "0",
5562
6105
  sortAt: normalizeSessionDiscoveryTimestamp(parsed.sortAt, "cursor sortAt"),
5563
6106
  id: parsed.id,
5564
6107
  snapshotAt: normalizeSessionDiscoveryTimestamp(parsed.snapshotAt, "cursor snapshotAt"),
5565
6108
  snapshotRevision: "0",
5566
6109
  updatedAfter: null,
6110
+ filterHash: null,
5567
6111
  };
5568
6112
  }
6113
+ const isV2 = parsed.v === 2;
6114
+ const isV3 = parsed.v === 3;
5569
6115
  if (
5570
- parsed.v !== 2 ||
5571
- (parsed.orderBy !== "createdAt" && parsed.orderBy !== "updatedAt") ||
6116
+ (!isV2 && !isV3) ||
6117
+ (isV2 && parsed.orderBy !== "createdAt" && parsed.orderBy !== "updatedAt") ||
6118
+ (isV3 && parsed.orderBy !== "relevance") ||
5572
6119
  typeof parsed.sortRevision !== "string" ||
5573
6120
  typeof parsed.sortAt !== "string" ||
5574
6121
  typeof parsed.snapshotAt !== "string" ||
@@ -5579,6 +6126,18 @@ export function decodeSessionDiscoveryCursor(value: string): SessionDiscoveryCur
5579
6126
  ) {
5580
6127
  throw new Error("invalid cursor fields");
5581
6128
  }
6129
+ if (
6130
+ isV3 &&
6131
+ (!Number.isSafeInteger(parsed.sortRank) ||
6132
+ (parsed.sortRank as number) < 0 ||
6133
+ typeof parsed.filterHash !== "string" ||
6134
+ !/^[0-9a-f]{64}$/.test(parsed.filterHash))
6135
+ ) {
6136
+ throw new Error("invalid relevance cursor fields");
6137
+ }
6138
+ if (isV2 && (parsed.sortRank !== undefined || parsed.filterHash !== undefined)) {
6139
+ throw new Error("chronological cursor cannot carry relevance fields");
6140
+ }
5582
6141
  const sortAt = normalizeSessionDiscoveryTimestamp(parsed.sortAt, "cursor sortAt");
5583
6142
  const snapshotAt = normalizeSessionDiscoveryTimestamp(parsed.snapshotAt, "cursor snapshotAt");
5584
6143
  const sortRevision = normalizeSessionDiscoveryRevision(
@@ -5599,14 +6158,19 @@ export function decodeSessionDiscoveryCursor(value: string): SessionDiscoveryCur
5599
6158
  if (parsed.orderBy === "createdAt" && (sortRevision !== "0" || snapshotRevision !== "0")) {
5600
6159
  throw new Error("creation cursor cannot carry activity revisions");
5601
6160
  }
6161
+ const orderBy: SessionDiscoveryOrderBy = isV3
6162
+ ? "relevance"
6163
+ : (parsed.orderBy as "createdAt" | "updatedAt");
5602
6164
  return {
5603
- orderBy: parsed.orderBy,
6165
+ orderBy,
6166
+ sortRank: isV3 ? (parsed.sortRank as number) : null,
5604
6167
  sortRevision,
5605
6168
  sortAt,
5606
6169
  id: parsed.id,
5607
6170
  snapshotAt,
5608
6171
  snapshotRevision,
5609
6172
  updatedAfter: normalizedUpdatedAfter,
6173
+ filterHash: isV3 ? (parsed.filterHash as string) : null,
5610
6174
  };
5611
6175
  } catch {
5612
6176
  throw new Error("sessions_list cursor is invalid");
@@ -5697,6 +6261,7 @@ export function capSessionDiscoveryPage(
5697
6261
  : null,
5698
6262
  queuedPromptCount: session.queuedPromptCount,
5699
6263
  children: session.treeStats,
6264
+ relatedWork: session.workDiscovery,
5700
6265
  ...(includeLastMessage
5701
6266
  ? {
5702
6267
  latestMessage: session.latestMessage
@@ -5774,12 +6339,14 @@ export function capSessionDiscoveryPage(
5774
6339
  ? sourceLast
5775
6340
  ? encodeSessionDiscoveryCursor({
5776
6341
  orderBy: page.orderBy,
6342
+ sortRank: sourceLast.sortRank,
5777
6343
  sortRevision: sourceLast.sortRevision,
5778
6344
  sortAt: sourceLast.sortAt,
5779
6345
  id: sourceLast.id,
5780
6346
  snapshotAt: page.snapshotAt,
5781
6347
  snapshotRevision: page.snapshotRevision,
5782
6348
  updatedAfter: page.updatedAfter,
6349
+ filterHash: page.filterHash,
5783
6350
  })
5784
6351
  : null
5785
6352
  : page.nextCursor