@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/dist/app.js +1 -1
- package/dist/{chunk-L7GVVSSQ.js → chunk-QESX7HDK.js} +650 -98
- package/dist/chunk-QESX7HDK.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/integrations/slack-interactions.d.ts +1 -1
- package/dist/mcp/server.d.ts +33 -0
- package/dist/routes/sessions.d.ts +17 -5
- package/dist/work-discovery-observability.d.ts +33 -0
- package/package.json +18 -18
- package/src/mcp/server.ts +304 -29
- package/src/routes/sessions.ts +377 -81
- package/src/work-discovery-observability.ts +121 -0
- package/dist/chunk-L7GVVSSQ.js.map +0 -1
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.
|
|
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 ({
|
|
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
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
:
|
|
4031
|
-
|
|
4032
|
-
|
|
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
|
-
|
|
4035
|
-
|
|
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
|
-
|
|
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
|
|
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
|