@opengeni/api-router 2.4.2-canary.0 → 2.5.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.
package/src/mcp/server.ts CHANGED
@@ -45,6 +45,16 @@ import {
45
45
  TASK_NOTE_MAX_LIFETIME_DAYS,
46
46
  TASK_NOTE_REASON_MAX_BYTES,
47
47
  TASK_NOTE_TEXT_MAX_BYTES,
48
+ WORK_CLAIM_CANONICAL_KEY_MAX_BYTES,
49
+ WORK_CLAIM_DISPLAY_LABEL_MAX_BYTES,
50
+ WORK_CLAIM_DISCOVERY_LIMIT,
51
+ WORK_CLAIM_NAMESPACE_MAX_BYTES,
52
+ WORK_CLAIM_VERSION_VALUE_MAX_BYTES,
53
+ WORK_DISCOVERY_QUERY_MAX_CHARS,
54
+ WORK_DISCOVERY_RECENT_HOURS_MAX,
55
+ WorkClaimSubjectType,
56
+ type WorkClaimSubjectFilter,
57
+ type SessionStatus,
48
58
  SubmitHumanInputResponseRequest,
49
59
  } from "@opengeni/contracts";
50
60
  import {
@@ -104,6 +114,8 @@ import {
104
114
  createTaskNote,
105
115
  listTaskNotes,
106
116
  replaceTaskNote,
117
+ releaseWorkClaim,
118
+ upsertWorkClaim,
107
119
  acceptSessionHumanInputResponse,
108
120
  HumanInputResponseValidationError,
109
121
  } from "@opengeni/db";
@@ -251,6 +263,7 @@ import { registerCompanyProfileAgentAdminTools } from "./company-profile-agent-a
251
263
  import { registerRememberTools } from "./remember";
252
264
  import { mintSandboxCodemodeToken } from "@opengeni/runtime/sandbox";
253
265
  import { deleteScheduledTaskWithDurableCleanup } from "../scheduled-task-deletion";
266
+ import { observeWorkDiscovery, summarizeWorkDiscoveryRows } from "../work-discovery-observability";
254
267
 
255
268
  export type McpServerOptions = {
256
269
  // Origin of the HTTP request that reached the MCP route. Browser-oriented
@@ -410,6 +423,8 @@ const FIRST_PARTY_TOOL_AUTHORIZATION = {
410
423
  task_note_save: { sessionRequired: true, allOf: ["sessions:control"] },
411
424
  task_note_archive: { sessionRequired: true, allOf: ["sessions:control"] },
412
425
  task_note_replace: { sessionRequired: true, allOf: ["sessions:control"] },
426
+ work_claim_upsert: { sessionRequired: true, allOf: ["sessions:control"] },
427
+ work_claim_release: { sessionRequired: true, allOf: ["sessions:control"] },
413
428
  knowledge_propose: { sessionRequired: true, allOf: ["documents:search"] },
414
429
  knowledge_correct: { sessionRequired: true, allOf: ["documents:search"] },
415
430
  task_note_promote_knowledge: {
@@ -791,6 +806,9 @@ export function buildOpenGeniMcpServer(
791
806
  if (sessionId !== null && exactAgentAttemptClaims(grant) !== null) {
792
807
  registerPreferenceRegistryTools(server, deps, grant, json);
793
808
  registerTaskNoteTools(server, deps, grant, sessionId, json);
809
+ if (deps.settings.workClaimMutationsEnabled) {
810
+ registerWorkClaimTools(server, deps, grant, sessionId, json);
811
+ }
794
812
  const attempt = exactAgentAttemptClaims(grant)!;
795
813
  registerCompanyBrainGovernedWriteTools({
796
814
  server,
@@ -3241,6 +3259,133 @@ function registerTaskNoteTools(
3241
3259
  );
3242
3260
  }
3243
3261
 
3262
+ function registerWorkClaimTools(
3263
+ server: McpServer,
3264
+ deps: ApiRouteDeps,
3265
+ grant: AccessGrant,
3266
+ sessionId: string,
3267
+ json: JsonResult,
3268
+ ): void {
3269
+ const attemptClaims = () => {
3270
+ const resolved = exactAgentAttemptClaims(grant);
3271
+ if (!resolved || resolved.sessionId !== sessionId) {
3272
+ throw new Error("Exact signed work-claim attempt authority is required.");
3273
+ }
3274
+ return {
3275
+ accountId: grant.accountId,
3276
+ workspaceId: grant.workspaceId,
3277
+ ...resolved,
3278
+ };
3279
+ };
3280
+ const authorize = async () => {
3281
+ await authorizeFirstPartySession(deps, grant, sessionId, "session.first_party_mcp.call");
3282
+ };
3283
+
3284
+ server.registerTool(
3285
+ "work_claim_upsert",
3286
+ {
3287
+ description:
3288
+ "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.",
3289
+ inputSchema: {
3290
+ operationId: z4.string().uuid(),
3291
+ expectedRevision: z4.number().int().min(0),
3292
+ subjectNamespace: z4.string().min(1).max(WORK_CLAIM_NAMESPACE_MAX_BYTES),
3293
+ subjectType: z4.enum([
3294
+ "repository",
3295
+ "branch",
3296
+ "pull_request",
3297
+ "issue",
3298
+ "artifact",
3299
+ "release",
3300
+ "ci_run",
3301
+ "other",
3302
+ ]),
3303
+ canonicalKey: z4.string().min(1).max(WORK_CLAIM_CANONICAL_KEY_MAX_BYTES),
3304
+ displayLabel: z4.string().min(1).max(WORK_CLAIM_DISPLAY_LABEL_MAX_BYTES).optional(),
3305
+ role: z4.enum(["working", "reviewing", "monitoring", "delivering"]),
3306
+ versionKind: z4
3307
+ .enum([
3308
+ "git_commit",
3309
+ "branch_head",
3310
+ "pull_request_head",
3311
+ "artifact_version",
3312
+ "release_version",
3313
+ "ci_run",
3314
+ "other",
3315
+ ])
3316
+ .optional(),
3317
+ versionValue: z4.string().min(1).max(WORK_CLAIM_VERSION_VALUE_MAX_BYTES).optional(),
3318
+ },
3319
+ },
3320
+ async ({
3321
+ operationId,
3322
+ expectedRevision,
3323
+ subjectNamespace,
3324
+ subjectType,
3325
+ canonicalKey,
3326
+ displayLabel,
3327
+ role,
3328
+ versionKind,
3329
+ versionValue,
3330
+ }) => {
3331
+ await authorize();
3332
+ if ((versionKind === undefined) !== (versionValue === undefined)) {
3333
+ throw new Error("work_claim_upsert versionKind and versionValue must be supplied together");
3334
+ }
3335
+ return json(
3336
+ await upsertWorkClaim(deps.db, {
3337
+ ...attemptClaims(),
3338
+ operationId,
3339
+ expectedRevision,
3340
+ subjectNamespace,
3341
+ subjectType,
3342
+ canonicalKey,
3343
+ ...(displayLabel === undefined ? {} : { displayLabel }),
3344
+ role,
3345
+ ...(versionKind && versionValue
3346
+ ? { version: { kind: versionKind, value: versionValue } }
3347
+ : {}),
3348
+ }),
3349
+ );
3350
+ },
3351
+ );
3352
+
3353
+ server.registerTool(
3354
+ "work_claim_release",
3355
+ {
3356
+ description:
3357
+ "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.",
3358
+ inputSchema: {
3359
+ operationId: z4.string().uuid(),
3360
+ claimId: z4.string().uuid(),
3361
+ expectedRevision: z4.number().int().min(1),
3362
+ reason: z4.enum([
3363
+ "completed",
3364
+ "cancelled",
3365
+ "failed",
3366
+ "superseded",
3367
+ "no_longer_active",
3368
+ "corrected",
3369
+ "external_state_changed",
3370
+ "other",
3371
+ ]),
3372
+ },
3373
+ },
3374
+ async ({ operationId, claimId, expectedRevision, reason }) => {
3375
+ await authorize();
3376
+ return json(
3377
+ await releaseWorkClaim(deps.db, {
3378
+ ...attemptClaims(),
3379
+ operationId,
3380
+ claimId,
3381
+ expectedRevision,
3382
+ reason,
3383
+ }),
3384
+ );
3385
+ },
3386
+ );
3387
+ }
3388
+
3244
3389
  function registerPreferenceRegistryTools(
3245
3390
  server: McpServer,
3246
3391
  deps: ApiRouteDeps,
@@ -4003,46 +4148,145 @@ function registerWorkspaceOrchestrationTools(
4003
4148
  server.registerTool(
4004
4149
  "sessions_list",
4005
4150
  {
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.`,
4151
+ 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
4152
  inputSchema: {
4008
4153
  limit: z4.number().int().positive().max(100).optional(),
4009
4154
  cursor: z4.string().max(512).optional(),
4010
4155
  includeLastMessage: z4.boolean().optional(),
4011
- orderBy: z4.enum(["createdAt", "updatedAt"]).optional(),
4156
+ orderBy: z4.enum(["createdAt", "updatedAt", "relevance"]).optional(),
4012
4157
  updatedAfter: z4.string().max(64).optional(),
4158
+ query: z4.string().max(WORK_DISCOVERY_QUERY_MAX_CHARS).optional(),
4159
+ statuses: z4
4160
+ .array(
4161
+ z4.enum([
4162
+ "queued",
4163
+ "running",
4164
+ "idle",
4165
+ "requires_action",
4166
+ "recovering",
4167
+ "waiting_capacity",
4168
+ "failed",
4169
+ "cancelled",
4170
+ ]),
4171
+ )
4172
+ .max(8)
4173
+ .optional(),
4174
+ activeOnly: z4.boolean().optional(),
4175
+ recentHours: z4.number().int().positive().max(WORK_DISCOVERY_RECENT_HOURS_MAX).optional(),
4176
+ rootSessionId: z4.string().uuid().optional(),
4177
+ parentSessionId: z4.string().uuid().nullable().optional(),
4178
+ subject: z4
4179
+ .object({
4180
+ namespace: z4.string().min(1).max(WORK_CLAIM_NAMESPACE_MAX_BYTES),
4181
+ type: WorkClaimSubjectType,
4182
+ canonicalKey: z4.string().min(1).max(WORK_CLAIM_CANONICAL_KEY_MAX_BYTES),
4183
+ })
4184
+ .strict()
4185
+ .optional(),
4186
+ claimLimit: z4.number().int().positive().max(WORK_CLAIM_DISCOVERY_LIMIT).optional(),
4013
4187
  },
4014
4188
  },
4015
- async ({ limit, cursor, includeLastMessage, orderBy: requestedOrderBy, updatedAfter }) => {
4189
+ async ({
4190
+ limit,
4191
+ cursor,
4192
+ includeLastMessage,
4193
+ orderBy: requestedOrderBy,
4194
+ updatedAfter,
4195
+ query,
4196
+ statuses,
4197
+ activeOnly,
4198
+ recentHours,
4199
+ rootSessionId,
4200
+ parentSessionId,
4201
+ subject,
4202
+ claimLimit,
4203
+ }) => {
4016
4204
  const authorizationScope = await requireSessionAuthorizationListScope(
4017
4205
  deps,
4018
4206
  grant,
4019
4207
  "first_party_mcp",
4020
4208
  );
4209
+ const startedAtMs = performance.now();
4210
+ const mode = subject ? "subject" : query?.trim() ? "query" : "browse";
4211
+ const metricAuthorizationScope =
4212
+ authorizationScope?.kind === "scoped" ? "scoped" : "workspace";
4021
4213
  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");
4214
+ const relevanceRequested = Boolean(query?.trim() || subject);
4215
+ if (relevanceRequested && !deps.settings.workDiscoveryEnabled) {
4216
+ observeWorkDiscovery(deps.observability, {
4217
+ surface: "first_party_mcp",
4218
+ mode,
4219
+ outcome: "disabled",
4220
+ authorizationScope: metricAuthorizationScope,
4221
+ durationMs: performance.now() - startedAtMs,
4222
+ responseBytes: 0,
4223
+ resultCount: 0,
4224
+ overlapCount: 0,
4225
+ matchCounts: {},
4226
+ });
4227
+ throw new Error("sessions_list work discovery is disabled by the operator");
4033
4228
  }
4034
- if (decodedCursor && decodedCursor.updatedAfter !== (normalizedUpdatedAfter ?? null)) {
4035
- throw new Error("sessions_list cursor does not match updatedAfter");
4229
+ try {
4230
+ const orderBy: SessionDiscoveryOrderBy =
4231
+ requestedOrderBy ??
4232
+ decodedCursor?.orderBy ??
4233
+ (relevanceRequested ? "relevance" : "createdAt");
4234
+ if (decodedCursor && decodedCursor.orderBy !== orderBy) {
4235
+ throw new Error("sessions_list cursor order does not match orderBy");
4236
+ }
4237
+ const normalizedUpdatedAfter =
4238
+ updatedAfter !== undefined
4239
+ ? normalizeSessionDiscoveryRevision(updatedAfter, "updatedAfter")
4240
+ : (decodedCursor?.updatedAfter ?? undefined);
4241
+ if (normalizedUpdatedAfter !== undefined && orderBy !== "updatedAt") {
4242
+ throw new Error("sessions_list updatedAfter requires orderBy=updatedAt");
4243
+ }
4244
+ if (decodedCursor && decodedCursor.updatedAfter !== (normalizedUpdatedAfter ?? null)) {
4245
+ throw new Error("sessions_list cursor does not match updatedAfter");
4246
+ }
4247
+ const page = await listSessionDiscoverySummaries(deps.db, grant.workspaceId, {
4248
+ limit: boundedSessionDiscoveryLimit(limit),
4249
+ ...(decodedCursor ? { cursor: decodedCursor } : {}),
4250
+ includeLastMessage: includeLastMessage === true,
4251
+ orderBy,
4252
+ ...(normalizedUpdatedAfter ? { updatedAfter: normalizedUpdatedAfter } : {}),
4253
+ ...(query?.trim() ? { query } : {}),
4254
+ ...(statuses ? { statuses: statuses as SessionStatus[] } : {}),
4255
+ activeOnly: activeOnly === true,
4256
+ ...(recentHours !== undefined ? { recentHours } : {}),
4257
+ ...(rootSessionId ? { rootSessionId } : {}),
4258
+ ...(parentSessionId !== undefined ? { parentSessionId } : {}),
4259
+ ...(subject ? { subject: subject as WorkClaimSubjectFilter } : {}),
4260
+ ...(claimLimit !== undefined ? { claimLimit } : {}),
4261
+ includeWorkDiscovery: deps.settings.workDiscoveryEnabled,
4262
+ subjectId: grant.subjectId,
4263
+ ...(authorizationScope ? { authorizationScope } : {}),
4264
+ });
4265
+ const result = capSessionDiscoveryPage(page, includeLastMessage === true);
4266
+ observeWorkDiscovery(deps.observability, {
4267
+ surface: "first_party_mcp",
4268
+ mode,
4269
+ outcome: result.sessions.length === 0 ? "empty" : "ok",
4270
+ authorizationScope: metricAuthorizationScope,
4271
+ durationMs: performance.now() - startedAtMs,
4272
+ responseBytes: result.bytes,
4273
+ ...summarizeWorkDiscoveryRows(result.sessions),
4274
+ });
4275
+ return json(result);
4276
+ } catch (error) {
4277
+ observeWorkDiscovery(deps.observability, {
4278
+ surface: "first_party_mcp",
4279
+ mode,
4280
+ outcome: "error",
4281
+ authorizationScope: metricAuthorizationScope,
4282
+ durationMs: performance.now() - startedAtMs,
4283
+ responseBytes: 0,
4284
+ resultCount: 0,
4285
+ overlapCount: 0,
4286
+ matchCounts: {},
4287
+ });
4288
+ throw error;
4036
4289
  }
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
4290
  },
4047
4291
  );
4048
4292
 
@@ -5470,10 +5714,12 @@ function boundedSessionDiscoveryLimit(limit: number | undefined): number {
5470
5714
  }
5471
5715
 
5472
5716
  export function encodeSessionDiscoveryCursor(cursor: SessionDiscoveryCursor): string {
5717
+ const relevance = cursor.orderBy === "relevance";
5473
5718
  return Buffer.from(
5474
5719
  JSON.stringify({
5475
- v: 2,
5720
+ v: relevance ? 3 : 2,
5476
5721
  orderBy: cursor.orderBy,
5722
+ ...(relevance ? { sortRank: cursor.sortRank, filterHash: cursor.filterHash } : {}),
5477
5723
  sortRevision: cursor.sortRevision,
5478
5724
  sortAt: cursor.sortAt,
5479
5725
  id: cursor.id,
@@ -5523,6 +5769,8 @@ export function decodeSessionDiscoveryCursor(value: string): SessionDiscoveryCur
5523
5769
  snapshotAt?: unknown;
5524
5770
  snapshotRevision?: unknown;
5525
5771
  updatedAfter?: unknown;
5772
+ sortRank?: unknown;
5773
+ filterHash?: unknown;
5526
5774
  };
5527
5775
  if (
5528
5776
  parsed.v === undefined &&
@@ -5536,12 +5784,14 @@ export function decodeSessionDiscoveryCursor(value: string): SessionDiscoveryCur
5536
5784
  );
5537
5785
  return {
5538
5786
  orderBy: "createdAt",
5787
+ sortRank: null,
5539
5788
  sortRevision: "0",
5540
5789
  sortAt: createdAt,
5541
5790
  id: parsed.id,
5542
5791
  snapshotAt: createdAt,
5543
5792
  snapshotRevision: "0",
5544
5793
  updatedAfter: null,
5794
+ filterHash: null,
5545
5795
  };
5546
5796
  }
5547
5797
  // The timestamp-fenced v1 format was never safe for updated-order
@@ -5558,17 +5808,22 @@ export function decodeSessionDiscoveryCursor(value: string): SessionDiscoveryCur
5558
5808
  ) {
5559
5809
  return {
5560
5810
  orderBy: "createdAt",
5811
+ sortRank: null,
5561
5812
  sortRevision: "0",
5562
5813
  sortAt: normalizeSessionDiscoveryTimestamp(parsed.sortAt, "cursor sortAt"),
5563
5814
  id: parsed.id,
5564
5815
  snapshotAt: normalizeSessionDiscoveryTimestamp(parsed.snapshotAt, "cursor snapshotAt"),
5565
5816
  snapshotRevision: "0",
5566
5817
  updatedAfter: null,
5818
+ filterHash: null,
5567
5819
  };
5568
5820
  }
5821
+ const isV2 = parsed.v === 2;
5822
+ const isV3 = parsed.v === 3;
5569
5823
  if (
5570
- parsed.v !== 2 ||
5571
- (parsed.orderBy !== "createdAt" && parsed.orderBy !== "updatedAt") ||
5824
+ (!isV2 && !isV3) ||
5825
+ (isV2 && parsed.orderBy !== "createdAt" && parsed.orderBy !== "updatedAt") ||
5826
+ (isV3 && parsed.orderBy !== "relevance") ||
5572
5827
  typeof parsed.sortRevision !== "string" ||
5573
5828
  typeof parsed.sortAt !== "string" ||
5574
5829
  typeof parsed.snapshotAt !== "string" ||
@@ -5579,6 +5834,18 @@ export function decodeSessionDiscoveryCursor(value: string): SessionDiscoveryCur
5579
5834
  ) {
5580
5835
  throw new Error("invalid cursor fields");
5581
5836
  }
5837
+ if (
5838
+ isV3 &&
5839
+ (!Number.isSafeInteger(parsed.sortRank) ||
5840
+ (parsed.sortRank as number) < 0 ||
5841
+ typeof parsed.filterHash !== "string" ||
5842
+ !/^[0-9a-f]{64}$/.test(parsed.filterHash))
5843
+ ) {
5844
+ throw new Error("invalid relevance cursor fields");
5845
+ }
5846
+ if (isV2 && (parsed.sortRank !== undefined || parsed.filterHash !== undefined)) {
5847
+ throw new Error("chronological cursor cannot carry relevance fields");
5848
+ }
5582
5849
  const sortAt = normalizeSessionDiscoveryTimestamp(parsed.sortAt, "cursor sortAt");
5583
5850
  const snapshotAt = normalizeSessionDiscoveryTimestamp(parsed.snapshotAt, "cursor snapshotAt");
5584
5851
  const sortRevision = normalizeSessionDiscoveryRevision(
@@ -5599,14 +5866,19 @@ export function decodeSessionDiscoveryCursor(value: string): SessionDiscoveryCur
5599
5866
  if (parsed.orderBy === "createdAt" && (sortRevision !== "0" || snapshotRevision !== "0")) {
5600
5867
  throw new Error("creation cursor cannot carry activity revisions");
5601
5868
  }
5869
+ const orderBy: SessionDiscoveryOrderBy = isV3
5870
+ ? "relevance"
5871
+ : (parsed.orderBy as "createdAt" | "updatedAt");
5602
5872
  return {
5603
- orderBy: parsed.orderBy,
5873
+ orderBy,
5874
+ sortRank: isV3 ? (parsed.sortRank as number) : null,
5604
5875
  sortRevision,
5605
5876
  sortAt,
5606
5877
  id: parsed.id,
5607
5878
  snapshotAt,
5608
5879
  snapshotRevision,
5609
5880
  updatedAfter: normalizedUpdatedAfter,
5881
+ filterHash: isV3 ? (parsed.filterHash as string) : null,
5610
5882
  };
5611
5883
  } catch {
5612
5884
  throw new Error("sessions_list cursor is invalid");
@@ -5697,6 +5969,7 @@ export function capSessionDiscoveryPage(
5697
5969
  : null,
5698
5970
  queuedPromptCount: session.queuedPromptCount,
5699
5971
  children: session.treeStats,
5972
+ relatedWork: session.workDiscovery,
5700
5973
  ...(includeLastMessage
5701
5974
  ? {
5702
5975
  latestMessage: session.latestMessage
@@ -5774,12 +6047,14 @@ export function capSessionDiscoveryPage(
5774
6047
  ? sourceLast
5775
6048
  ? encodeSessionDiscoveryCursor({
5776
6049
  orderBy: page.orderBy,
6050
+ sortRank: sourceLast.sortRank,
5777
6051
  sortRevision: sourceLast.sortRevision,
5778
6052
  sortAt: sourceLast.sortAt,
5779
6053
  id: sourceLast.id,
5780
6054
  snapshotAt: page.snapshotAt,
5781
6055
  snapshotRevision: page.snapshotRevision,
5782
6056
  updatedAfter: page.updatedAfter,
6057
+ filterHash: page.filterHash,
5783
6058
  })
5784
6059
  : null
5785
6060
  : page.nextCursor