@opengeni/api-router 0.5.7 → 0.7.3
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.d.ts +21 -3
- package/dist/app.js +3 -1
- package/dist/{chunk-HBEJMWD3.js → chunk-EYYTFA7N.js} +2396 -676
- package/dist/chunk-EYYTFA7N.js.map +1 -0
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/package.json +11 -11
- package/src/app.ts +47 -4
- package/src/github-access.ts +46 -0
- package/src/github-browser-flow.ts +83 -0
- package/src/http/auth.ts +12 -4
- package/src/http/sse.ts +526 -92
- package/src/index.ts +2 -1
- package/src/mcp/server.ts +774 -146
- package/src/mcp/session-view.ts +622 -203
- package/src/mcp/toolspace.ts +110 -25
- package/src/routes/codex.ts +17 -14
- package/src/routes/enrollments.ts +2 -2
- package/src/routes/github.ts +63 -202
- package/src/routes/install.ts +1 -1
- package/src/routes/machines.ts +2 -2
- package/src/routes/sessions.ts +639 -76
- package/src/routes/workspace-capture.ts +56 -38
- package/src/routes/workspaces.ts +30 -11
- package/src/sandbox/access.ts +1 -1
- package/src/sandbox/auth-callout.ts +1 -1
- package/src/sandbox/channel-a.ts +14 -1
- package/src/sandbox/enrollment.ts +4 -4
- package/src/sandbox/machines.ts +1 -1
- package/src/sandbox/metrics-ingestion.ts +1 -1
- package/src/sandbox/viewer.ts +1 -1
- package/dist/chunk-HBEJMWD3.js.map +0 -1
package/src/mcp/server.ts
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CreateScheduledTaskRequest,
|
|
3
|
+
defaultRepositoryMountPath,
|
|
4
|
+
SESSION_EVENT_RAW_DELTA_TYPES,
|
|
5
|
+
SessionEventPayloadMode,
|
|
6
|
+
SessionEventReadDirection,
|
|
7
|
+
SessionEventReadMode,
|
|
8
|
+
SessionEventSemanticClass,
|
|
9
|
+
SessionEventType,
|
|
3
10
|
SessionMcpCredentialUpdateInput,
|
|
4
11
|
VariableSetVariableName,
|
|
5
12
|
type AccessGrant,
|
|
6
13
|
type GitHubRepository,
|
|
7
14
|
type Permission,
|
|
8
15
|
type ResourceRef,
|
|
16
|
+
type SessionAuthorizationOperation,
|
|
9
17
|
UpdateScheduledTaskRequest,
|
|
10
18
|
} from "@opengeni/contracts";
|
|
11
19
|
import {
|
|
@@ -21,12 +29,18 @@ import {
|
|
|
21
29
|
getSessionTurn,
|
|
22
30
|
getVariableSet,
|
|
23
31
|
getVariableSetByName,
|
|
24
|
-
|
|
32
|
+
areGitHubRepositoriesAllowedForWorkspace,
|
|
25
33
|
listScheduledTaskRuns,
|
|
26
34
|
listScheduledTasks,
|
|
27
|
-
|
|
28
|
-
|
|
35
|
+
listSessionEventPage,
|
|
36
|
+
listSessionDiscoverySummaries,
|
|
37
|
+
projectEffectiveControlForRelatedAccess,
|
|
38
|
+
projectSessionForRelatedAccess,
|
|
39
|
+
type SessionDiscoveryCursor,
|
|
40
|
+
type SessionDiscoveryOrderBy,
|
|
29
41
|
listRigs,
|
|
42
|
+
listRigChangeMonitoringSummaries,
|
|
43
|
+
listRigVersionMonitoringSummaries,
|
|
30
44
|
listSocialConnections,
|
|
31
45
|
listSocialPosts,
|
|
32
46
|
listVariableSets,
|
|
@@ -38,6 +52,7 @@ import {
|
|
|
38
52
|
requireSession,
|
|
39
53
|
saveWorkspaceMemory,
|
|
40
54
|
searchWorkspaceMemories,
|
|
55
|
+
serializeEffectiveSessionControl,
|
|
41
56
|
setSessionGoalStatus,
|
|
42
57
|
setVariableSetVariable,
|
|
43
58
|
updateScheduledTask,
|
|
@@ -49,20 +64,21 @@ import {
|
|
|
49
64
|
import { appendAndPublishEvents } from "@opengeni/events";
|
|
50
65
|
import {
|
|
51
66
|
createGitHubAppInstallationToken,
|
|
52
|
-
createSignedState,
|
|
53
67
|
GitHubAppConfigurationError,
|
|
54
68
|
githubAppMissingSettings,
|
|
55
|
-
listGitHubAppRepositories,
|
|
56
|
-
stateMaxAgeSeconds,
|
|
57
69
|
} from "@opengeni/github";
|
|
58
70
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
59
71
|
import * as z4 from "zod/v4";
|
|
60
|
-
import {
|
|
72
|
+
import {
|
|
73
|
+
hasPermission,
|
|
74
|
+
requireSessionAuthorization,
|
|
75
|
+
requireSessionAuthorizationListScope,
|
|
76
|
+
type ResolvedSessionAuthorization,
|
|
77
|
+
} from "@opengeni/core";
|
|
61
78
|
import { recordWorkspaceUsage, requireLimit } from "@opengeni/core";
|
|
62
79
|
import type { ApiRouteDeps } from "@opengeni/core";
|
|
80
|
+
import { listWorkspaceGitHubRepositories } from "../github-access";
|
|
63
81
|
import {
|
|
64
|
-
listRigChangesForApi,
|
|
65
|
-
listRigVersionsForApi,
|
|
66
82
|
promoteVerifiedDefinitionEditChangeForApi,
|
|
67
83
|
proposeRigChangeForApi,
|
|
68
84
|
requireRigChangeForApi,
|
|
@@ -103,13 +119,18 @@ import {
|
|
|
103
119
|
type FleetServices,
|
|
104
120
|
type RunOnOp,
|
|
105
121
|
} from "@opengeni/core";
|
|
106
|
-
import {
|
|
122
|
+
import {
|
|
123
|
+
boundSessionEventMcpPage,
|
|
124
|
+
boundSessionDetailMcp,
|
|
125
|
+
boundRigDetailMcp,
|
|
126
|
+
SESSION_EVENT_MCP_MAX_BYTES,
|
|
127
|
+
} from "./session-view";
|
|
107
128
|
import type { ToolspaceMcpSurface } from "./toolspace";
|
|
108
129
|
|
|
109
130
|
export type McpServerOptions = {
|
|
110
|
-
// Origin of the HTTP request that reached the MCP route
|
|
111
|
-
//
|
|
112
|
-
//
|
|
131
|
+
// Origin of the HTTP request that reached the MCP route. Retained in the
|
|
132
|
+
// options ABI for browser-oriented tools; github_connect_link does not use
|
|
133
|
+
// it or mint state while new installation binding is disabled.
|
|
113
134
|
requestOrigin?: string | null;
|
|
114
135
|
toolspace?: ToolspaceMcpSurface | null;
|
|
115
136
|
workspaceMemoryEnabled?: boolean | undefined;
|
|
@@ -148,8 +169,13 @@ export function buildOpenGeniMcpServer(
|
|
|
148
169
|
inputSchema: { title: z4.string().min(1).max(200) },
|
|
149
170
|
},
|
|
150
171
|
async ({ title }) => {
|
|
151
|
-
|
|
152
|
-
|
|
172
|
+
await authorizeFirstPartySession(deps, grant, sessionId, "session.title.write");
|
|
173
|
+
const result = await updateSessionTitle(deps, grant, sessionId, title, "agent");
|
|
174
|
+
return json({
|
|
175
|
+
ok: true,
|
|
176
|
+
updated: result.updated,
|
|
177
|
+
title: result.title ?? title,
|
|
178
|
+
});
|
|
153
179
|
},
|
|
154
180
|
);
|
|
155
181
|
}
|
|
@@ -178,19 +204,20 @@ export function buildOpenGeniMcpServer(
|
|
|
178
204
|
registerRigTools(server, deps, grant, can, sessionId, json);
|
|
179
205
|
}
|
|
180
206
|
|
|
181
|
-
// Orchestration, variableSet, and GitHub
|
|
207
|
+
// Orchestration, variableSet, and GitHub status/token tools are permission-gated
|
|
182
208
|
// at registration: a grant without the permission does not see the tool.
|
|
183
209
|
// Sandboxed workers reach this server with the first-party delegated
|
|
184
210
|
// permission set (firstPartyMcpPermissions in @opengeni/runtime), which is
|
|
185
211
|
// POWERFUL BY DEFAULT — it carries sessions:*, variable sets:*, and github:use,
|
|
186
|
-
// so agents can spawn/read sessions, manage variable set variables,
|
|
187
|
-
//
|
|
212
|
+
// so agents can spawn/read sessions, manage variable set variables, inspect
|
|
213
|
+
// GitHub connection availability, and refresh scoped tokens for already-bound
|
|
214
|
+
// repositories out of the box. A user DEMOTES a specific
|
|
188
215
|
// session by setting a narrower session.firstPartyMcpPermissions (capped to
|
|
189
216
|
// the creator's own grant); operators still cap what any session can be given.
|
|
190
217
|
registerWorkspaceOrchestrationTools(server, deps, grant, can, sessionId, toolspaceMode, json);
|
|
191
218
|
registerVariableSetTools(server, deps, grant, can, json);
|
|
192
219
|
if (can("github:use")) {
|
|
193
|
-
registerGitHubConnectTool(server, deps,
|
|
220
|
+
registerGitHubConnectTool(server, deps, json);
|
|
194
221
|
// TOKEN-BROKER (B1): the agent-refreshable git token. Session-scoped (keys off the
|
|
195
222
|
// worker-signed sessionId claim so it mints for THIS session's repos), gated on
|
|
196
223
|
// the same github:use capability as github_connect_link.
|
|
@@ -214,7 +241,9 @@ export function buildOpenGeniMcpServer(
|
|
|
214
241
|
if (file.status !== "ready") {
|
|
215
242
|
throw new Error(`file is ${file.status}`);
|
|
216
243
|
}
|
|
217
|
-
const signed = await deps.objectStorage.createGetUrl({
|
|
244
|
+
const signed = await deps.objectStorage.createGetUrl({
|
|
245
|
+
key: file.objectKey,
|
|
246
|
+
});
|
|
218
247
|
return json({
|
|
219
248
|
file: {
|
|
220
249
|
id: file.id,
|
|
@@ -246,11 +275,7 @@ export function buildOpenGeniMcpServer(
|
|
|
246
275
|
},
|
|
247
276
|
async ({ limit }) => {
|
|
248
277
|
try {
|
|
249
|
-
const
|
|
250
|
-
deps.db,
|
|
251
|
-
grant.workspaceId,
|
|
252
|
-
);
|
|
253
|
-
const repositories = await listGitHubAppRepositories(deps.settings, { installationIds });
|
|
278
|
+
const repositories = await listWorkspaceGitHubRepositories(deps, grant.workspaceId);
|
|
254
279
|
const visible = typeof limit === "number" ? repositories.slice(0, limit) : repositories;
|
|
255
280
|
return json({
|
|
256
281
|
repositories: visible.map((repository) =>
|
|
@@ -380,7 +405,9 @@ export function buildOpenGeniMcpServer(
|
|
|
380
405
|
inputSchema: { limit: z4.number().int().positive().optional() },
|
|
381
406
|
},
|
|
382
407
|
async ({ limit }) =>
|
|
383
|
-
json({
|
|
408
|
+
json({
|
|
409
|
+
tasks: await listScheduledTasks(deps.db, grant.workspaceId, limit ?? 100),
|
|
410
|
+
}),
|
|
384
411
|
);
|
|
385
412
|
|
|
386
413
|
server.registerTool(
|
|
@@ -429,7 +456,11 @@ export function buildOpenGeniMcpServer(
|
|
|
429
456
|
payload,
|
|
430
457
|
toolsProvided: scheduledTaskToolsProvided(args),
|
|
431
458
|
});
|
|
432
|
-
await syncCreatedScheduledTask({
|
|
459
|
+
await syncCreatedScheduledTask({
|
|
460
|
+
db: deps.db,
|
|
461
|
+
workflowClient: deps.workflowClient,
|
|
462
|
+
task,
|
|
463
|
+
});
|
|
433
464
|
return json(task);
|
|
434
465
|
},
|
|
435
466
|
);
|
|
@@ -526,7 +557,10 @@ export function buildOpenGeniMcpServer(
|
|
|
526
557
|
{
|
|
527
558
|
description:
|
|
528
559
|
"Trigger a scheduled task immediately. Pass a stable triggerId to make a retried trigger idempotent (one charge, one run).",
|
|
529
|
-
inputSchema: {
|
|
560
|
+
inputSchema: {
|
|
561
|
+
id: z4.string().uuid(),
|
|
562
|
+
triggerId: z4.string().min(1).max(128).optional(),
|
|
563
|
+
},
|
|
530
564
|
},
|
|
531
565
|
async ({ id, triggerId }) => {
|
|
532
566
|
const task = await requireScheduledTask(deps.db, grant.workspaceId, id);
|
|
@@ -584,7 +618,10 @@ export function buildOpenGeniMcpServer(
|
|
|
584
618
|
"scheduled_task_runs_list",
|
|
585
619
|
{
|
|
586
620
|
description: "List runs for a scheduled task.",
|
|
587
|
-
inputSchema: {
|
|
621
|
+
inputSchema: {
|
|
622
|
+
taskId: z4.string().uuid(),
|
|
623
|
+
limit: z4.number().int().positive().optional(),
|
|
624
|
+
},
|
|
588
625
|
},
|
|
589
626
|
async ({ taskId, limit }) =>
|
|
590
627
|
json({
|
|
@@ -686,6 +723,7 @@ function registerGoalTools(
|
|
|
686
723
|
},
|
|
687
724
|
},
|
|
688
725
|
async ({ text, successCriteria, maxAutoContinuations }) => {
|
|
726
|
+
await authorizeFirstPartySession(deps, grant, sessionId, "session.goal.write");
|
|
689
727
|
await requireSession(deps.db, grant.workspaceId, sessionId);
|
|
690
728
|
const callerTurnId =
|
|
691
729
|
typeof grant.metadata?.["turnId"] === "string"
|
|
@@ -730,6 +768,7 @@ function registerGoalTools(
|
|
|
730
768
|
},
|
|
731
769
|
},
|
|
732
770
|
async ({ text, successCriteria, progressNote }) => {
|
|
771
|
+
await authorizeFirstPartySession(deps, grant, sessionId, "session.goal.write");
|
|
733
772
|
await requireSession(deps.db, grant.workspaceId, sessionId);
|
|
734
773
|
const existing = await getSessionGoal(deps.db, grant.workspaceId, sessionId);
|
|
735
774
|
if (!existing) {
|
|
@@ -767,6 +806,7 @@ function registerGoalTools(
|
|
|
767
806
|
inputSchema: { evidence: z4.string().min(1) },
|
|
768
807
|
},
|
|
769
808
|
async ({ evidence }) => {
|
|
809
|
+
await authorizeFirstPartySession(deps, grant, sessionId, "session.goal.write");
|
|
770
810
|
await requireSession(deps.db, grant.workspaceId, sessionId);
|
|
771
811
|
const existing = await getSessionGoal(deps.db, grant.workspaceId, sessionId);
|
|
772
812
|
if (!existing) {
|
|
@@ -796,6 +836,7 @@ function registerGoalTools(
|
|
|
796
836
|
inputSchema: { rationale: z4.string().min(1) },
|
|
797
837
|
},
|
|
798
838
|
async ({ rationale }) => {
|
|
839
|
+
await authorizeFirstPartySession(deps, grant, sessionId, "session.goal.write");
|
|
799
840
|
await requireSession(deps.db, grant.workspaceId, sessionId);
|
|
800
841
|
const existing = await getSessionGoal(deps.db, grant.workspaceId, sessionId);
|
|
801
842
|
if (!existing) {
|
|
@@ -826,7 +867,22 @@ function registerGoalTools(
|
|
|
826
867
|
);
|
|
827
868
|
}
|
|
828
869
|
|
|
829
|
-
type JsonResult = (value: unknown) => {
|
|
870
|
+
type JsonResult = (value: unknown) => {
|
|
871
|
+
content: Array<{ type: "text"; text: string }>;
|
|
872
|
+
};
|
|
873
|
+
|
|
874
|
+
async function authorizeFirstPartySession(
|
|
875
|
+
deps: ApiRouteDeps,
|
|
876
|
+
grant: AccessGrant,
|
|
877
|
+
sessionId: string,
|
|
878
|
+
operation: SessionAuthorizationOperation,
|
|
879
|
+
): Promise<ResolvedSessionAuthorization | null> {
|
|
880
|
+
return await requireSessionAuthorization(deps, grant, {
|
|
881
|
+
sessionId,
|
|
882
|
+
operation,
|
|
883
|
+
surface: "first_party_mcp",
|
|
884
|
+
});
|
|
885
|
+
}
|
|
830
886
|
|
|
831
887
|
const MemoryKindSchema = z4.enum(["preference", "semantic", "procedural", "decision", "episodic"]);
|
|
832
888
|
|
|
@@ -969,7 +1025,11 @@ function registerFleetTools(
|
|
|
969
1025
|
sessionId: string,
|
|
970
1026
|
json: JsonResult,
|
|
971
1027
|
): void {
|
|
972
|
-
const services: FleetServices = {
|
|
1028
|
+
const services: FleetServices = {
|
|
1029
|
+
db: deps.db,
|
|
1030
|
+
settings: deps.settings,
|
|
1031
|
+
bus: deps.bus,
|
|
1032
|
+
};
|
|
973
1033
|
|
|
974
1034
|
// Resolve the session's group sandbox (the default/home fleet member) at
|
|
975
1035
|
// call-time via the shared helper (same context the user-authenticated swap
|
|
@@ -1026,7 +1086,11 @@ function registerFleetTools(
|
|
|
1026
1086
|
workdir: z4.string().optional(),
|
|
1027
1087
|
}),
|
|
1028
1088
|
z4.object({ kind: z4.literal("read"), path: z4.string().min(1) }),
|
|
1029
|
-
z4.object({
|
|
1089
|
+
z4.object({
|
|
1090
|
+
kind: z4.literal("write"),
|
|
1091
|
+
path: z4.string().min(1),
|
|
1092
|
+
content: z4.string(),
|
|
1093
|
+
}),
|
|
1030
1094
|
]),
|
|
1031
1095
|
},
|
|
1032
1096
|
},
|
|
@@ -1046,7 +1110,10 @@ function registerFleetTools(
|
|
|
1046
1110
|
},
|
|
1047
1111
|
async ({ kind, name }) =>
|
|
1048
1112
|
json(
|
|
1049
|
-
await provisionSandbox(services, await fleetContext(), {
|
|
1113
|
+
await provisionSandbox(services, await fleetContext(), {
|
|
1114
|
+
kind,
|
|
1115
|
+
...(name ? { name } : {}),
|
|
1116
|
+
}),
|
|
1050
1117
|
),
|
|
1051
1118
|
);
|
|
1052
1119
|
}
|
|
@@ -1100,24 +1167,31 @@ function registerRigTools(
|
|
|
1100
1167
|
server.registerTool(
|
|
1101
1168
|
"rig_get",
|
|
1102
1169
|
{
|
|
1103
|
-
description:
|
|
1170
|
+
description:
|
|
1171
|
+
"Get one rig's bounded active definition plus compact historical version/change summaries. Historical setup scripts, checks, payloads, and verification logs are represented by counts/byte facts rather than copied into model context; use the access-controlled REST detail endpoints for exact retained definitions.",
|
|
1104
1172
|
inputSchema: {
|
|
1105
1173
|
rigId: z4.string().uuid(),
|
|
1174
|
+
versionLimit: z4.number().int().positive().optional(),
|
|
1106
1175
|
changeLimit: z4.number().int().positive().optional(),
|
|
1107
1176
|
},
|
|
1108
1177
|
},
|
|
1109
|
-
async ({ rigId, changeLimit }) => {
|
|
1178
|
+
async ({ rigId, versionLimit, changeLimit }) => {
|
|
1110
1179
|
const rig = await requireRigForApi(deps.db, grant.workspaceId, rigId);
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
changes: await listRigChangesForApi(
|
|
1115
|
-
{ db: deps.db },
|
|
1180
|
+
const [versions, changes] = await Promise.all([
|
|
1181
|
+
listRigVersionMonitoringSummaries(
|
|
1182
|
+
deps.db,
|
|
1116
1183
|
grant.workspaceId,
|
|
1117
1184
|
rig.id,
|
|
1118
|
-
|
|
1185
|
+
boundedRigHistoryLimit(versionLimit),
|
|
1119
1186
|
),
|
|
1120
|
-
|
|
1187
|
+
listRigChangeMonitoringSummaries(
|
|
1188
|
+
deps.db,
|
|
1189
|
+
grant.workspaceId,
|
|
1190
|
+
rig.id,
|
|
1191
|
+
boundedRigHistoryLimit(changeLimit),
|
|
1192
|
+
),
|
|
1193
|
+
]);
|
|
1194
|
+
return json(boundRigDetailMcp(rig, versions, changes));
|
|
1121
1195
|
},
|
|
1122
1196
|
);
|
|
1123
1197
|
|
|
@@ -1237,6 +1311,7 @@ function exactAgentCommandContext(
|
|
|
1237
1311
|
return {
|
|
1238
1312
|
accountId: grant.accountId,
|
|
1239
1313
|
workspaceId: grant.workspaceId,
|
|
1314
|
+
subjectId: grant.subjectId,
|
|
1240
1315
|
callerSessionId,
|
|
1241
1316
|
callerTurnId: turnId,
|
|
1242
1317
|
callerAttemptId: attemptId,
|
|
@@ -1257,11 +1332,47 @@ function registerWorkspaceOrchestrationTools(
|
|
|
1257
1332
|
server.registerTool(
|
|
1258
1333
|
"sessions_list",
|
|
1259
1334
|
{
|
|
1260
|
-
description:
|
|
1261
|
-
inputSchema: {
|
|
1335
|
+
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; its 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.`,
|
|
1336
|
+
inputSchema: {
|
|
1337
|
+
limit: z4.number().int().positive().max(100).optional(),
|
|
1338
|
+
cursor: z4.string().max(512).optional(),
|
|
1339
|
+
includeLastMessage: z4.boolean().optional(),
|
|
1340
|
+
orderBy: z4.enum(["createdAt", "updatedAt"]).optional(),
|
|
1341
|
+
updatedAfter: z4.string().max(64).optional(),
|
|
1342
|
+
},
|
|
1343
|
+
},
|
|
1344
|
+
async ({ limit, cursor, includeLastMessage, orderBy: requestedOrderBy, updatedAfter }) => {
|
|
1345
|
+
const authorizationScope = await requireSessionAuthorizationListScope(
|
|
1346
|
+
deps,
|
|
1347
|
+
grant,
|
|
1348
|
+
"first_party_mcp",
|
|
1349
|
+
);
|
|
1350
|
+
const decodedCursor = cursor ? decodeSessionDiscoveryCursor(cursor) : undefined;
|
|
1351
|
+
const orderBy: SessionDiscoveryOrderBy =
|
|
1352
|
+
requestedOrderBy ?? decodedCursor?.orderBy ?? "createdAt";
|
|
1353
|
+
if (decodedCursor && decodedCursor.orderBy !== orderBy) {
|
|
1354
|
+
throw new Error("sessions_list cursor order does not match orderBy");
|
|
1355
|
+
}
|
|
1356
|
+
const normalizedUpdatedAfter =
|
|
1357
|
+
updatedAfter !== undefined
|
|
1358
|
+
? normalizeSessionDiscoveryRevision(updatedAfter, "updatedAfter")
|
|
1359
|
+
: (decodedCursor?.updatedAfter ?? undefined);
|
|
1360
|
+
if (normalizedUpdatedAfter !== undefined && orderBy !== "updatedAt") {
|
|
1361
|
+
throw new Error("sessions_list updatedAfter requires orderBy=updatedAt");
|
|
1362
|
+
}
|
|
1363
|
+
if (decodedCursor && decodedCursor.updatedAfter !== (normalizedUpdatedAfter ?? null)) {
|
|
1364
|
+
throw new Error("sessions_list cursor does not match updatedAfter");
|
|
1365
|
+
}
|
|
1366
|
+
const page = await listSessionDiscoverySummaries(deps.db, grant.workspaceId, {
|
|
1367
|
+
limit: boundedSessionDiscoveryLimit(limit),
|
|
1368
|
+
...(decodedCursor ? { cursor: decodedCursor } : {}),
|
|
1369
|
+
includeLastMessage: includeLastMessage === true,
|
|
1370
|
+
orderBy,
|
|
1371
|
+
...(normalizedUpdatedAfter ? { updatedAfter: normalizedUpdatedAfter } : {}),
|
|
1372
|
+
...(authorizationScope ? { authorizationScope } : {}),
|
|
1373
|
+
});
|
|
1374
|
+
return json(capSessionDiscoveryPage(page, includeLastMessage === true));
|
|
1262
1375
|
},
|
|
1263
|
-
async ({ limit }) =>
|
|
1264
|
-
json({ sessions: await listSessions(deps.db, grant.workspaceId, boundedMcpLimit(limit)) }),
|
|
1265
1376
|
);
|
|
1266
1377
|
|
|
1267
1378
|
server.registerTool(
|
|
@@ -1272,15 +1383,25 @@ function registerWorkspaceOrchestrationTools(
|
|
|
1272
1383
|
inputSchema: { sessionId: z4.string().uuid() },
|
|
1273
1384
|
},
|
|
1274
1385
|
async ({ sessionId }) => {
|
|
1386
|
+
const authorization = await authorizeFirstPartySession(
|
|
1387
|
+
deps,
|
|
1388
|
+
grant,
|
|
1389
|
+
sessionId,
|
|
1390
|
+
"session.read",
|
|
1391
|
+
);
|
|
1275
1392
|
const session = await getSession(deps.db, grant.workspaceId, sessionId);
|
|
1276
1393
|
if (!session) {
|
|
1277
1394
|
throw new Error("session not found");
|
|
1278
1395
|
}
|
|
1279
1396
|
const queue = await getSessionQueueSnapshot(deps.db, grant.workspaceId, sessionId);
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1397
|
+
const projected = projectSessionForRelatedAccess(
|
|
1398
|
+
{
|
|
1399
|
+
...session,
|
|
1400
|
+
effectiveControl: queue?.effectiveControl ?? session.effectiveControl,
|
|
1401
|
+
},
|
|
1402
|
+
authorization?.relatedSessionAccess ?? "root",
|
|
1403
|
+
);
|
|
1404
|
+
return json(boundSessionDetailMcp(projected));
|
|
1284
1405
|
},
|
|
1285
1406
|
);
|
|
1286
1407
|
|
|
@@ -1288,28 +1409,83 @@ function registerWorkspaceOrchestrationTools(
|
|
|
1288
1409
|
"session_events",
|
|
1289
1410
|
{
|
|
1290
1411
|
description:
|
|
1291
|
-
"Read a
|
|
1412
|
+
"Read a compact semantic tail only when session_get status is insufficient. With no cursor, this returns the newest matching events and excludes raw message/reasoning/command/PTY deltas. Use `latest` as an exclusive lookup for the newest event in exactly one semantic class; it cannot be combined with type or class filters. Use nextBefore to page older or explicit after/nextAfter to page forward. Type/class filters run in the RLS-scoped database query. payloadMode none|summary|full controls retained audit payload projection, but every model result is independently byte-capped with explicit truncation and exact covered sequence bounds. Exact retained forensic payloads require the access-controlled REST/SDK events API with mode=forensic&payloadMode=full; generic source bytes never retained by the audit boundary remain unavailable.",
|
|
1292
1413
|
inputSchema: {
|
|
1293
1414
|
sessionId: z4.string().uuid(),
|
|
1294
1415
|
after: z4.number().int().nonnegative().optional(),
|
|
1416
|
+
before: z4.number().int().positive().optional(),
|
|
1295
1417
|
limit: z4.number().int().positive().optional(),
|
|
1418
|
+
direction: z4.enum(SessionEventReadDirection.options).optional(),
|
|
1419
|
+
mode: z4.enum(SessionEventReadMode.options).optional(),
|
|
1420
|
+
payloadMode: z4.enum(SessionEventPayloadMode.options).optional(),
|
|
1421
|
+
includeTypes: z4.array(z4.enum(SessionEventType.options)).max(100).optional(),
|
|
1422
|
+
excludeTypes: z4.array(z4.enum(SessionEventType.options)).max(100).optional(),
|
|
1423
|
+
includeClasses: z4
|
|
1424
|
+
.array(z4.enum(SessionEventSemanticClass.options))
|
|
1425
|
+
.max(SessionEventSemanticClass.options.length)
|
|
1426
|
+
.optional(),
|
|
1427
|
+
excludeClasses: z4
|
|
1428
|
+
.array(z4.enum(SessionEventSemanticClass.options))
|
|
1429
|
+
.max(SessionEventSemanticClass.options.length)
|
|
1430
|
+
.optional(),
|
|
1431
|
+
latest: z4.enum(SessionEventSemanticClass.options).optional(),
|
|
1296
1432
|
},
|
|
1297
1433
|
},
|
|
1298
|
-
async ({
|
|
1434
|
+
async ({
|
|
1435
|
+
sessionId,
|
|
1436
|
+
after,
|
|
1437
|
+
before,
|
|
1438
|
+
limit,
|
|
1439
|
+
direction: requestedDirection,
|
|
1440
|
+
mode: requestedMode,
|
|
1441
|
+
payloadMode: requestedPayloadMode,
|
|
1442
|
+
includeTypes,
|
|
1443
|
+
excludeTypes,
|
|
1444
|
+
includeClasses,
|
|
1445
|
+
excludeClasses,
|
|
1446
|
+
latest,
|
|
1447
|
+
}) => {
|
|
1448
|
+
await authorizeFirstPartySession(deps, grant, sessionId, "session.events.read");
|
|
1299
1449
|
await requireSession(deps.db, grant.workspaceId, sessionId);
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1450
|
+
if (
|
|
1451
|
+
latest &&
|
|
1452
|
+
[includeTypes, excludeTypes, includeClasses, excludeClasses].some(
|
|
1453
|
+
(filter) => filter !== undefined,
|
|
1454
|
+
)
|
|
1455
|
+
) {
|
|
1456
|
+
throw new Error("latest cannot be combined with event filters");
|
|
1457
|
+
}
|
|
1458
|
+
const mode = requestedMode ?? (after !== undefined ? "forensic" : "monitoring");
|
|
1459
|
+
const direction = latest
|
|
1460
|
+
? "before"
|
|
1461
|
+
: (requestedDirection ??
|
|
1462
|
+
(before !== undefined ? "before" : after !== undefined ? "after" : "before"));
|
|
1463
|
+
const payloadMode = requestedPayloadMode ?? (mode === "monitoring" ? "summary" : "full");
|
|
1464
|
+
const dbPage = await listSessionEventPage(deps.db, grant.workspaceId, sessionId, {
|
|
1465
|
+
after: after ?? 0,
|
|
1466
|
+
...(before !== undefined ? { before } : {}),
|
|
1467
|
+
direction,
|
|
1468
|
+
limit: latest ? 1 : boundedSessionEventMcpLimit(limit),
|
|
1469
|
+
payloadMode,
|
|
1470
|
+
includeTypes: includeTypes ?? [],
|
|
1471
|
+
excludeTypes: excludeTypes ?? [],
|
|
1472
|
+
includeClasses: latest ? [latest] : (includeClasses ?? []),
|
|
1473
|
+
excludeClasses: excludeClasses ?? [],
|
|
1474
|
+
...(mode === "monitoring" ? { defaultExcludeTypes: SESSION_EVENT_RAW_DELTA_TYPES } : {}),
|
|
1475
|
+
maxBytes: SESSION_EVENT_MCP_MAX_BYTES * 4,
|
|
1312
1476
|
});
|
|
1477
|
+
return json(
|
|
1478
|
+
boundSessionEventMcpPage({
|
|
1479
|
+
events: dbPage.events,
|
|
1480
|
+
mode,
|
|
1481
|
+
payloadMode,
|
|
1482
|
+
direction,
|
|
1483
|
+
sourceHasMore: dbPage.hasMore,
|
|
1484
|
+
sourceTruncatedBy: dbPage.truncatedBy,
|
|
1485
|
+
after: after ?? 0,
|
|
1486
|
+
before: before ?? null,
|
|
1487
|
+
}),
|
|
1488
|
+
);
|
|
1313
1489
|
},
|
|
1314
1490
|
);
|
|
1315
1491
|
}
|
|
@@ -1360,7 +1536,14 @@ function registerWorkspaceOrchestrationTools(
|
|
|
1360
1536
|
idempotencyKey: z4.string().min(1).max(200).optional(),
|
|
1361
1537
|
// First-party MCP token permissions for the spawned session; every
|
|
1362
1538
|
// permission must be held by this grant (validated in the domain).
|
|
1363
|
-
|
|
1539
|
+
// A goal requires goals:manage in the resulting set; it is never
|
|
1540
|
+
// silently added beyond the inherited or explicit authority.
|
|
1541
|
+
firstPartyMcpPermissions: z4
|
|
1542
|
+
.array(z4.string())
|
|
1543
|
+
.optional()
|
|
1544
|
+
.describe(
|
|
1545
|
+
"Optional first-party capability set for the child. Omit to inherit this session's effective permissions. An explicit set may only narrow capabilities held by this session. A goal-bearing child requires goals:manage in the resulting set; creation fails rather than adding it implicitly.",
|
|
1546
|
+
),
|
|
1364
1547
|
// Shared-sandbox placement (addendum 05 §D). OMIT (default) to SHARE the
|
|
1365
1548
|
// creator's box — one filesystem/repo/desktop, N independent conversations;
|
|
1366
1549
|
// this is the SAFE DEFAULT. Pass "new" for a fresh isolated box (a different
|
|
@@ -1394,7 +1577,12 @@ function registerWorkspaceOrchestrationTools(
|
|
|
1394
1577
|
// arbitrary session's wake channel without sessions:control on it.
|
|
1395
1578
|
},
|
|
1396
1579
|
},
|
|
1397
|
-
async (args) =>
|
|
1580
|
+
async (args) => {
|
|
1581
|
+
if (callerSessionId !== null) {
|
|
1582
|
+
await authorizeFirstPartySession(deps, grant, callerSessionId, "session.child.create");
|
|
1583
|
+
}
|
|
1584
|
+
return json(await createSessionForRequest(deps, grant, grant.workspaceId, args));
|
|
1585
|
+
},
|
|
1398
1586
|
);
|
|
1399
1587
|
}
|
|
1400
1588
|
|
|
@@ -1414,6 +1602,7 @@ function registerWorkspaceOrchestrationTools(
|
|
|
1414
1602
|
},
|
|
1415
1603
|
},
|
|
1416
1604
|
async ({ sessionId: targetSessionId, text, idempotencyKey, mcpCredentialUpdates }) => {
|
|
1605
|
+
await authorizeFirstPartySession(deps, grant, targetSessionId, "session.append");
|
|
1417
1606
|
if (callerSessionId !== null) {
|
|
1418
1607
|
if ((mcpCredentialUpdates?.length ?? 0) > 0) {
|
|
1419
1608
|
throw new Error("internal session updates cannot change MCP credentials");
|
|
@@ -1464,6 +1653,12 @@ function registerWorkspaceOrchestrationTools(
|
|
|
1464
1653
|
},
|
|
1465
1654
|
},
|
|
1466
1655
|
async ({ sessionId, idempotencyKey, reason }) => {
|
|
1656
|
+
const authorization = await authorizeFirstPartySession(
|
|
1657
|
+
deps,
|
|
1658
|
+
grant,
|
|
1659
|
+
sessionId,
|
|
1660
|
+
"session.control",
|
|
1661
|
+
);
|
|
1467
1662
|
if (callerSessionId !== null) {
|
|
1468
1663
|
const controlled = await controlAgentSessionWorkstream(
|
|
1469
1664
|
deps,
|
|
@@ -1477,27 +1672,37 @@ function registerWorkspaceOrchestrationTools(
|
|
|
1477
1672
|
);
|
|
1478
1673
|
return json({
|
|
1479
1674
|
receiptId: controlled.receipt.id,
|
|
1480
|
-
effectiveControl:
|
|
1675
|
+
effectiveControl: projectEffectiveControlForRelatedAccess(
|
|
1676
|
+
serializeEffectiveSessionControl(controlled.control),
|
|
1677
|
+
sessionId,
|
|
1678
|
+
authorization?.relatedSessionAccess ?? "root",
|
|
1679
|
+
),
|
|
1481
1680
|
interruptionCount: controlled.interruptionCount,
|
|
1482
1681
|
replay: controlled.replay,
|
|
1483
1682
|
});
|
|
1484
1683
|
}
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
},
|
|
1499
|
-
),
|
|
1684
|
+
const controlled = await controlHumanSessionWorkstream(
|
|
1685
|
+
deps,
|
|
1686
|
+
{
|
|
1687
|
+
accountId: grant.accountId,
|
|
1688
|
+
workspaceId: grant.workspaceId,
|
|
1689
|
+
sessionId,
|
|
1690
|
+
subjectId: grant.subjectId,
|
|
1691
|
+
},
|
|
1692
|
+
{
|
|
1693
|
+
action: "pause",
|
|
1694
|
+
clientEventId: idempotencyKey,
|
|
1695
|
+
...(reason ? { reason } : {}),
|
|
1696
|
+
},
|
|
1500
1697
|
);
|
|
1698
|
+
return json({
|
|
1699
|
+
...controlled,
|
|
1700
|
+
effectiveControl: projectEffectiveControlForRelatedAccess(
|
|
1701
|
+
controlled.effectiveControl,
|
|
1702
|
+
sessionId,
|
|
1703
|
+
authorization?.relatedSessionAccess ?? "root",
|
|
1704
|
+
),
|
|
1705
|
+
});
|
|
1501
1706
|
},
|
|
1502
1707
|
);
|
|
1503
1708
|
|
|
@@ -1513,6 +1718,12 @@ function registerWorkspaceOrchestrationTools(
|
|
|
1513
1718
|
},
|
|
1514
1719
|
},
|
|
1515
1720
|
async ({ sessionId, idempotencyKey, reason }) => {
|
|
1721
|
+
const authorization = await authorizeFirstPartySession(
|
|
1722
|
+
deps,
|
|
1723
|
+
grant,
|
|
1724
|
+
sessionId,
|
|
1725
|
+
"session.control",
|
|
1726
|
+
);
|
|
1516
1727
|
if (callerSessionId !== null) {
|
|
1517
1728
|
const controlled = await controlAgentSessionWorkstream(
|
|
1518
1729
|
deps,
|
|
@@ -1526,27 +1737,37 @@ function registerWorkspaceOrchestrationTools(
|
|
|
1526
1737
|
);
|
|
1527
1738
|
return json({
|
|
1528
1739
|
receiptId: controlled.receipt.id,
|
|
1529
|
-
effectiveControl:
|
|
1740
|
+
effectiveControl: projectEffectiveControlForRelatedAccess(
|
|
1741
|
+
serializeEffectiveSessionControl(controlled.control),
|
|
1742
|
+
sessionId,
|
|
1743
|
+
authorization?.relatedSessionAccess ?? "root",
|
|
1744
|
+
),
|
|
1530
1745
|
interruptionCount: controlled.interruptionCount,
|
|
1531
1746
|
replay: controlled.replay,
|
|
1532
1747
|
});
|
|
1533
1748
|
}
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
},
|
|
1548
|
-
),
|
|
1749
|
+
const controlled = await controlHumanSessionWorkstream(
|
|
1750
|
+
deps,
|
|
1751
|
+
{
|
|
1752
|
+
accountId: grant.accountId,
|
|
1753
|
+
workspaceId: grant.workspaceId,
|
|
1754
|
+
sessionId,
|
|
1755
|
+
subjectId: grant.subjectId,
|
|
1756
|
+
},
|
|
1757
|
+
{
|
|
1758
|
+
action: "resume",
|
|
1759
|
+
clientEventId: idempotencyKey,
|
|
1760
|
+
...(reason ? { reason } : {}),
|
|
1761
|
+
},
|
|
1549
1762
|
);
|
|
1763
|
+
return json({
|
|
1764
|
+
...controlled,
|
|
1765
|
+
effectiveControl: projectEffectiveControlForRelatedAccess(
|
|
1766
|
+
controlled.effectiveControl,
|
|
1767
|
+
sessionId,
|
|
1768
|
+
authorization?.relatedSessionAccess ?? "root",
|
|
1769
|
+
),
|
|
1770
|
+
});
|
|
1550
1771
|
},
|
|
1551
1772
|
);
|
|
1552
1773
|
|
|
@@ -1563,6 +1784,7 @@ function registerWorkspaceOrchestrationTools(
|
|
|
1563
1784
|
},
|
|
1564
1785
|
},
|
|
1565
1786
|
async ({ sessionId, instruction, idempotencyKey }) => {
|
|
1787
|
+
await authorizeFirstPartySession(deps, grant, sessionId, "session.steer");
|
|
1566
1788
|
const result = await steerAgentSession(
|
|
1567
1789
|
deps,
|
|
1568
1790
|
exactAgentCommandContext(grant, callerSessionId),
|
|
@@ -1590,15 +1812,14 @@ function registerWorkspaceOrchestrationTools(
|
|
|
1590
1812
|
},
|
|
1591
1813
|
},
|
|
1592
1814
|
async ({ session_id, title }) => {
|
|
1815
|
+
await authorizeFirstPartySession(deps, grant, session_id, "session.title.write");
|
|
1593
1816
|
await requireSession(deps.db, grant.workspaceId, session_id);
|
|
1594
|
-
const result = await updateSessionTitle(
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
title,
|
|
1599
|
-
|
|
1600
|
-
);
|
|
1601
|
-
return json({ ok: true, updated: result.updated, title: result.title ?? title });
|
|
1817
|
+
const result = await updateSessionTitle(deps, grant, session_id, title, "agent");
|
|
1818
|
+
return json({
|
|
1819
|
+
ok: true,
|
|
1820
|
+
updated: result.updated,
|
|
1821
|
+
title: result.title ?? title,
|
|
1822
|
+
});
|
|
1602
1823
|
},
|
|
1603
1824
|
);
|
|
1604
1825
|
}
|
|
@@ -1703,7 +1924,11 @@ function registerVariableSetTools(
|
|
|
1703
1924
|
variableSetId: variableSet.id,
|
|
1704
1925
|
variableName: parsedName.data,
|
|
1705
1926
|
});
|
|
1706
|
-
const responseVariableSet = {
|
|
1927
|
+
const responseVariableSet = {
|
|
1928
|
+
id: variableSet.id,
|
|
1929
|
+
name: variableSet.name,
|
|
1930
|
+
created,
|
|
1931
|
+
};
|
|
1707
1932
|
return json({
|
|
1708
1933
|
variableSet: responseVariableSet,
|
|
1709
1934
|
environment: responseVariableSet,
|
|
@@ -1750,22 +1975,15 @@ function registerVariableSetTools(
|
|
|
1750
1975
|
}
|
|
1751
1976
|
}
|
|
1752
1977
|
|
|
1753
|
-
//
|
|
1754
|
-
//
|
|
1755
|
-
//
|
|
1756
|
-
|
|
1757
|
-
function registerGitHubConnectTool(
|
|
1758
|
-
server: McpServer,
|
|
1759
|
-
deps: ApiRouteDeps,
|
|
1760
|
-
grant: AccessGrant,
|
|
1761
|
-
options: McpServerOptions,
|
|
1762
|
-
json: JsonResult,
|
|
1763
|
-
): void {
|
|
1978
|
+
// The tool remains registered for compatibility, but every new installation
|
|
1979
|
+
// binding entry point is fail-closed until a provider-supported authority
|
|
1980
|
+
// proof stronger than installation visibility is available.
|
|
1981
|
+
function registerGitHubConnectTool(server: McpServer, deps: ApiRouteDeps, json: JsonResult): void {
|
|
1764
1982
|
server.registerTool(
|
|
1765
1983
|
"github_connect_link",
|
|
1766
1984
|
{
|
|
1767
1985
|
description:
|
|
1768
|
-
"
|
|
1986
|
+
"Report GitHub App connection availability. New installation binding is disabled until GitHub installation authority can be proven, so installUrl and linkUrl are null.",
|
|
1769
1987
|
inputSchema: {},
|
|
1770
1988
|
},
|
|
1771
1989
|
async () => {
|
|
@@ -1773,28 +1991,19 @@ function registerGitHubConnectTool(
|
|
|
1773
1991
|
const missing = githubAppMissingSettings(settings);
|
|
1774
1992
|
const slug = settings.githubAppSlug?.trim() || null;
|
|
1775
1993
|
if (missing.length > 0 || !slug) {
|
|
1776
|
-
return json({
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
).replace(/\/+$/, "");
|
|
1784
|
-
if (!base) {
|
|
1785
|
-
throw new Error(
|
|
1786
|
-
"github_connect_link requires OPENGENI_PUBLIC_BASE_URL (or OPENGENI_GITHUB_APP_MANIFEST_BASE_URL) so the install link can route through this deployment",
|
|
1787
|
-
);
|
|
1994
|
+
return json({
|
|
1995
|
+
configured: false,
|
|
1996
|
+
appSlug: slug,
|
|
1997
|
+
installUrl: null,
|
|
1998
|
+
linkUrl: null,
|
|
1999
|
+
missing,
|
|
2000
|
+
});
|
|
1788
2001
|
}
|
|
1789
|
-
const state = createSignedState(deps.githubStateSecret, {
|
|
1790
|
-
accountId: grant.accountId,
|
|
1791
|
-
workspaceId: grant.workspaceId,
|
|
1792
|
-
});
|
|
1793
2002
|
return json({
|
|
1794
2003
|
configured: true,
|
|
1795
2004
|
appSlug: slug,
|
|
1796
|
-
installUrl:
|
|
1797
|
-
|
|
2005
|
+
installUrl: null,
|
|
2006
|
+
linkUrl: null,
|
|
1798
2007
|
missing: [],
|
|
1799
2008
|
});
|
|
1800
2009
|
},
|
|
@@ -1845,9 +2054,20 @@ function registerGitHubTokenTool(
|
|
|
1845
2054
|
if (selected.some((item) => item.installationId !== installationId)) {
|
|
1846
2055
|
throw new Error("GitHub App repository resources must belong to one installation");
|
|
1847
2056
|
}
|
|
2057
|
+
const repositoryIds = selected.map((item) => item.repositoryId);
|
|
2058
|
+
if (
|
|
2059
|
+
!(await areGitHubRepositoriesAllowedForWorkspace(
|
|
2060
|
+
deps.db,
|
|
2061
|
+
grant.workspaceId,
|
|
2062
|
+
installationId,
|
|
2063
|
+
repositoryIds,
|
|
2064
|
+
))
|
|
2065
|
+
) {
|
|
2066
|
+
throw new Error("this workspace no longer authorizes the session's GitHub repositories");
|
|
2067
|
+
}
|
|
1848
2068
|
const token = await createGitHubAppInstallationToken(deps.settings, {
|
|
1849
2069
|
installationId,
|
|
1850
|
-
repositoryIds
|
|
2070
|
+
repositoryIds,
|
|
1851
2071
|
});
|
|
1852
2072
|
return json({
|
|
1853
2073
|
token,
|
|
@@ -1881,9 +2101,12 @@ function repositoryWithScheduledTaskResource(
|
|
|
1881
2101
|
kind: "repository",
|
|
1882
2102
|
uri,
|
|
1883
2103
|
ref: repository.defaultBranch,
|
|
1884
|
-
mountPath:
|
|
2104
|
+
mountPath: defaultRepositoryMountPath(uri),
|
|
1885
2105
|
...(repository.private
|
|
1886
|
-
? {
|
|
2106
|
+
? {
|
|
2107
|
+
githubInstallationId: repository.installationId,
|
|
2108
|
+
githubRepositoryId: repository.id,
|
|
2109
|
+
}
|
|
1887
2110
|
: {}),
|
|
1888
2111
|
},
|
|
1889
2112
|
};
|
|
@@ -1892,12 +2115,7 @@ function repositoryWithScheduledTaskResource(
|
|
|
1892
2115
|
function normalizedRepositoryUri(value: string): string {
|
|
1893
2116
|
const url = new URL(value);
|
|
1894
2117
|
const path = url.pathname.replace(/^\/+|\/+$/g, "").replace(/\.git$/, "");
|
|
1895
|
-
return `https://${url.
|
|
1896
|
-
}
|
|
1897
|
-
|
|
1898
|
-
function repositoryMountPath(uri: string): string {
|
|
1899
|
-
const url = new URL(uri);
|
|
1900
|
-
return `repos/${url.pathname.replace(/^\/+|\/+$/g, "").replace(/\.git$/, "")}`;
|
|
2118
|
+
return `https://${url.host.toLowerCase()}/${path}.git`;
|
|
1901
2119
|
}
|
|
1902
2120
|
|
|
1903
2121
|
function boundedMcpLimit(limit: number | undefined): number {
|
|
@@ -1907,6 +2125,416 @@ function boundedMcpLimit(limit: number | undefined): number {
|
|
|
1907
2125
|
return Math.min(500, Math.max(1, Math.floor(limit)));
|
|
1908
2126
|
}
|
|
1909
2127
|
|
|
2128
|
+
function boundedRigHistoryLimit(limit: number | undefined): number {
|
|
2129
|
+
if (!limit || !Number.isFinite(limit)) return 20;
|
|
2130
|
+
return Math.min(100, Math.max(1, Math.floor(limit)));
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
function boundedSessionEventMcpLimit(limit: number | undefined): number {
|
|
2134
|
+
if (!limit || !Number.isFinite(limit)) return 40;
|
|
2135
|
+
return Math.min(250, Math.max(1, Math.floor(limit)));
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
const SESSION_DISCOVERY_DEFAULT_LIMIT = 20;
|
|
2139
|
+
const SESSION_DISCOVERY_MAX_LIMIT = 100;
|
|
2140
|
+
const SESSION_DISCOVERY_TEXT_CHARS = 600;
|
|
2141
|
+
const SESSION_DISCOVERY_PREVIEW_MAX_BYTES = 16_384;
|
|
2142
|
+
const SESSION_DISCOVERY_PREVIEW_OMISSION_REASON = "aggregatePreviewBudget" as const;
|
|
2143
|
+
const SESSION_DISCOVERY_PAGE_MAX_BYTES = 128_000;
|
|
2144
|
+
const SESSION_DISCOVERY_PREVIEW_DRILL_DOWN_TOOL = "session_events" as const;
|
|
2145
|
+
const SESSION_DISCOVERY_PREVIEW_DRILL_DOWN_BASE_INPUT = {
|
|
2146
|
+
direction: "before",
|
|
2147
|
+
limit: 1,
|
|
2148
|
+
mode: "monitoring",
|
|
2149
|
+
payloadMode: "summary",
|
|
2150
|
+
} as const;
|
|
2151
|
+
|
|
2152
|
+
function sessionDiscoveryPreviewDrillDownInput(sessionId: string, type: SessionEventType) {
|
|
2153
|
+
return {
|
|
2154
|
+
sessionId,
|
|
2155
|
+
includeTypes: [type],
|
|
2156
|
+
...SESSION_DISCOVERY_PREVIEW_DRILL_DOWN_BASE_INPUT,
|
|
2157
|
+
};
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
function boundedSessionDiscoveryLimit(limit: number | undefined): number {
|
|
2161
|
+
if (!limit || !Number.isFinite(limit)) return SESSION_DISCOVERY_DEFAULT_LIMIT;
|
|
2162
|
+
return Math.min(SESSION_DISCOVERY_MAX_LIMIT, Math.max(1, Math.floor(limit)));
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
export function encodeSessionDiscoveryCursor(cursor: SessionDiscoveryCursor): string {
|
|
2166
|
+
return Buffer.from(
|
|
2167
|
+
JSON.stringify({
|
|
2168
|
+
v: 2,
|
|
2169
|
+
orderBy: cursor.orderBy,
|
|
2170
|
+
sortRevision: cursor.sortRevision,
|
|
2171
|
+
sortAt: cursor.sortAt,
|
|
2172
|
+
id: cursor.id,
|
|
2173
|
+
snapshotAt: cursor.snapshotAt,
|
|
2174
|
+
snapshotRevision: cursor.snapshotRevision,
|
|
2175
|
+
updatedAfter: cursor.updatedAfter,
|
|
2176
|
+
}),
|
|
2177
|
+
"utf8",
|
|
2178
|
+
).toString("base64url");
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
const SESSION_DISCOVERY_TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?Z$/;
|
|
2182
|
+
const SESSION_DISCOVERY_REVISION = /^(?:0|[1-9]\d*)$/;
|
|
2183
|
+
const SESSION_DISCOVERY_REVISION_MAX = 9_223_372_036_854_775_807n;
|
|
2184
|
+
const SESSION_DISCOVERY_UUID =
|
|
2185
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
2186
|
+
|
|
2187
|
+
function normalizeSessionDiscoveryTimestamp(value: string, label: string): string {
|
|
2188
|
+
if (!SESSION_DISCOVERY_TIMESTAMP.test(value) || Number.isNaN(new Date(value).getTime())) {
|
|
2189
|
+
throw new Error(`sessions_list ${label} must be an ISO UTC date-time`);
|
|
2190
|
+
}
|
|
2191
|
+
// Preserve up to six fractional digits. Converting through JS Date would
|
|
2192
|
+
// discard PostgreSQL microseconds and can skip equal-millisecond rows.
|
|
2193
|
+
return value;
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
function normalizeSessionDiscoveryRevision(value: string, label: string): string {
|
|
2197
|
+
if (!SESSION_DISCOVERY_REVISION.test(value)) {
|
|
2198
|
+
throw new Error(`sessions_list ${label} must be a decimal activity revision`);
|
|
2199
|
+
}
|
|
2200
|
+
const revision = BigInt(value);
|
|
2201
|
+
if (revision > SESSION_DISCOVERY_REVISION_MAX) {
|
|
2202
|
+
throw new Error(`sessions_list ${label} exceeds the database activity revision range`);
|
|
2203
|
+
}
|
|
2204
|
+
return revision.toString();
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
export function decodeSessionDiscoveryCursor(value: string): SessionDiscoveryCursor {
|
|
2208
|
+
try {
|
|
2209
|
+
const parsed = JSON.parse(Buffer.from(value, "base64url").toString("utf8")) as {
|
|
2210
|
+
v?: unknown;
|
|
2211
|
+
orderBy?: unknown;
|
|
2212
|
+
sortRevision?: unknown;
|
|
2213
|
+
sortAt?: unknown;
|
|
2214
|
+
createdAt?: unknown;
|
|
2215
|
+
id?: unknown;
|
|
2216
|
+
snapshotAt?: unknown;
|
|
2217
|
+
snapshotRevision?: unknown;
|
|
2218
|
+
updatedAfter?: unknown;
|
|
2219
|
+
};
|
|
2220
|
+
if (
|
|
2221
|
+
parsed.v === undefined &&
|
|
2222
|
+
typeof parsed.createdAt === "string" &&
|
|
2223
|
+
typeof parsed.id === "string" &&
|
|
2224
|
+
SESSION_DISCOVERY_UUID.test(parsed.id)
|
|
2225
|
+
) {
|
|
2226
|
+
const createdAt = normalizeSessionDiscoveryTimestamp(
|
|
2227
|
+
parsed.createdAt,
|
|
2228
|
+
"legacy cursor createdAt",
|
|
2229
|
+
);
|
|
2230
|
+
return {
|
|
2231
|
+
orderBy: "createdAt",
|
|
2232
|
+
sortRevision: "0",
|
|
2233
|
+
sortAt: createdAt,
|
|
2234
|
+
id: parsed.id,
|
|
2235
|
+
snapshotAt: createdAt,
|
|
2236
|
+
snapshotRevision: "0",
|
|
2237
|
+
updatedAfter: null,
|
|
2238
|
+
};
|
|
2239
|
+
}
|
|
2240
|
+
// The timestamp-fenced v1 format was never safe for updated-order
|
|
2241
|
+
// continuation. Preserve rolling compatibility only for creation cursors,
|
|
2242
|
+
// whose immutable ordering does not need an activity revision.
|
|
2243
|
+
if (
|
|
2244
|
+
parsed.v === 1 &&
|
|
2245
|
+
parsed.orderBy === "createdAt" &&
|
|
2246
|
+
typeof parsed.sortAt === "string" &&
|
|
2247
|
+
typeof parsed.snapshotAt === "string" &&
|
|
2248
|
+
parsed.updatedAfter === null &&
|
|
2249
|
+
typeof parsed.id === "string" &&
|
|
2250
|
+
SESSION_DISCOVERY_UUID.test(parsed.id)
|
|
2251
|
+
) {
|
|
2252
|
+
return {
|
|
2253
|
+
orderBy: "createdAt",
|
|
2254
|
+
sortRevision: "0",
|
|
2255
|
+
sortAt: normalizeSessionDiscoveryTimestamp(parsed.sortAt, "cursor sortAt"),
|
|
2256
|
+
id: parsed.id,
|
|
2257
|
+
snapshotAt: normalizeSessionDiscoveryTimestamp(parsed.snapshotAt, "cursor snapshotAt"),
|
|
2258
|
+
snapshotRevision: "0",
|
|
2259
|
+
updatedAfter: null,
|
|
2260
|
+
};
|
|
2261
|
+
}
|
|
2262
|
+
if (
|
|
2263
|
+
parsed.v !== 2 ||
|
|
2264
|
+
(parsed.orderBy !== "createdAt" && parsed.orderBy !== "updatedAt") ||
|
|
2265
|
+
typeof parsed.sortRevision !== "string" ||
|
|
2266
|
+
typeof parsed.sortAt !== "string" ||
|
|
2267
|
+
typeof parsed.snapshotAt !== "string" ||
|
|
2268
|
+
typeof parsed.snapshotRevision !== "string" ||
|
|
2269
|
+
(parsed.updatedAfter !== null && typeof parsed.updatedAfter !== "string") ||
|
|
2270
|
+
typeof parsed.id !== "string" ||
|
|
2271
|
+
!SESSION_DISCOVERY_UUID.test(parsed.id)
|
|
2272
|
+
) {
|
|
2273
|
+
throw new Error("invalid cursor fields");
|
|
2274
|
+
}
|
|
2275
|
+
const sortAt = normalizeSessionDiscoveryTimestamp(parsed.sortAt, "cursor sortAt");
|
|
2276
|
+
const snapshotAt = normalizeSessionDiscoveryTimestamp(parsed.snapshotAt, "cursor snapshotAt");
|
|
2277
|
+
const sortRevision = normalizeSessionDiscoveryRevision(
|
|
2278
|
+
parsed.sortRevision,
|
|
2279
|
+
"cursor sortRevision",
|
|
2280
|
+
);
|
|
2281
|
+
const snapshotRevision = normalizeSessionDiscoveryRevision(
|
|
2282
|
+
parsed.snapshotRevision,
|
|
2283
|
+
"cursor snapshotRevision",
|
|
2284
|
+
);
|
|
2285
|
+
const normalizedUpdatedAfter =
|
|
2286
|
+
parsed.updatedAfter === null
|
|
2287
|
+
? null
|
|
2288
|
+
: normalizeSessionDiscoveryRevision(parsed.updatedAfter, "cursor updatedAfter");
|
|
2289
|
+
if (normalizedUpdatedAfter !== null && parsed.orderBy !== "updatedAt") {
|
|
2290
|
+
throw new Error("incremental cursor requires updatedAt order");
|
|
2291
|
+
}
|
|
2292
|
+
if (parsed.orderBy === "createdAt" && (sortRevision !== "0" || snapshotRevision !== "0")) {
|
|
2293
|
+
throw new Error("creation cursor cannot carry activity revisions");
|
|
2294
|
+
}
|
|
2295
|
+
return {
|
|
2296
|
+
orderBy: parsed.orderBy,
|
|
2297
|
+
sortRevision,
|
|
2298
|
+
sortAt,
|
|
2299
|
+
id: parsed.id,
|
|
2300
|
+
snapshotAt,
|
|
2301
|
+
snapshotRevision,
|
|
2302
|
+
updatedAfter: normalizedUpdatedAfter,
|
|
2303
|
+
};
|
|
2304
|
+
} catch {
|
|
2305
|
+
throw new Error("sessions_list cursor is invalid");
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
|
|
2309
|
+
function capSessionDiscoveryText(
|
|
2310
|
+
value: string | null,
|
|
2311
|
+
maxChars = SESSION_DISCOVERY_TEXT_CHARS,
|
|
2312
|
+
originalChars?: number | null,
|
|
2313
|
+
) {
|
|
2314
|
+
if (value === null) {
|
|
2315
|
+
return { text: value, truncated: false };
|
|
2316
|
+
}
|
|
2317
|
+
const projectedChars = Array.from(value);
|
|
2318
|
+
const sourceChars = Math.max(projectedChars.length, originalChars ?? projectedChars.length);
|
|
2319
|
+
if (sourceChars <= maxChars) {
|
|
2320
|
+
return { text: value, truncated: false };
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
// The database supplies only a bounded prefix plus the original character
|
|
2324
|
+
// count. Iterate to a stable marker width so the reported omission includes
|
|
2325
|
+
// the characters replaced by the marker itself.
|
|
2326
|
+
let bodyChars = maxChars;
|
|
2327
|
+
let marker = "";
|
|
2328
|
+
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
2329
|
+
const omittedChars = Math.max(0, sourceChars - bodyChars);
|
|
2330
|
+
marker = `…[${omittedChars} chars truncated]…`;
|
|
2331
|
+
const nextBodyChars = Math.max(0, maxChars - Array.from(marker).length);
|
|
2332
|
+
if (nextBodyChars === bodyChars) break;
|
|
2333
|
+
bodyChars = nextBodyChars;
|
|
2334
|
+
}
|
|
2335
|
+
return {
|
|
2336
|
+
text: `${projectedChars.slice(0, bodyChars).join("")}${marker}`,
|
|
2337
|
+
truncated: true,
|
|
2338
|
+
};
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
export function capSessionDiscoveryPage(
|
|
2342
|
+
page: Awaited<ReturnType<typeof listSessionDiscoverySummaries>>,
|
|
2343
|
+
includeLastMessage: boolean,
|
|
2344
|
+
) {
|
|
2345
|
+
const projected = page.sessions.map((session) => {
|
|
2346
|
+
const title = capSessionDiscoveryText(session.title, 200, session.titleOriginalChars);
|
|
2347
|
+
const goal = session.goal
|
|
2348
|
+
? capSessionDiscoveryText(
|
|
2349
|
+
session.goal.text,
|
|
2350
|
+
SESSION_DISCOVERY_TEXT_CHARS,
|
|
2351
|
+
session.goal.textOriginalChars,
|
|
2352
|
+
)
|
|
2353
|
+
: null;
|
|
2354
|
+
const preview = includeLastMessage
|
|
2355
|
+
? capSessionDiscoveryText(
|
|
2356
|
+
session.latestMessage?.preview ?? null,
|
|
2357
|
+
SESSION_DISCOVERY_TEXT_CHARS,
|
|
2358
|
+
session.latestMessage?.previewOriginalChars,
|
|
2359
|
+
)
|
|
2360
|
+
: null;
|
|
2361
|
+
const blocker = session.effectiveControl.primaryBlocker;
|
|
2362
|
+
const blockerDisplayName = blocker
|
|
2363
|
+
? capSessionDiscoveryText(blocker.displayName, 200, blocker.displayNameOriginalChars)
|
|
2364
|
+
: null;
|
|
2365
|
+
return {
|
|
2366
|
+
id: session.id,
|
|
2367
|
+
title: title.text,
|
|
2368
|
+
titleTruncated: title.truncated,
|
|
2369
|
+
parentSessionId: session.parentSessionId,
|
|
2370
|
+
isRoot: session.parentSessionId === null,
|
|
2371
|
+
status: session.status,
|
|
2372
|
+
pause: {
|
|
2373
|
+
state: session.effectiveControl.state,
|
|
2374
|
+
additionalBlockerCount: session.effectiveControl.additionalBlockerCount,
|
|
2375
|
+
source: blocker
|
|
2376
|
+
? {
|
|
2377
|
+
kind: blocker.kind,
|
|
2378
|
+
...(blocker.sessionId ? { sessionId: blocker.sessionId } : {}),
|
|
2379
|
+
displayName: blockerDisplayName!.text,
|
|
2380
|
+
displayNameTruncated: blockerDisplayName!.truncated,
|
|
2381
|
+
}
|
|
2382
|
+
: null,
|
|
2383
|
+
},
|
|
2384
|
+
goal: session.goal
|
|
2385
|
+
? {
|
|
2386
|
+
status: session.goal.status,
|
|
2387
|
+
summary: goal!.text,
|
|
2388
|
+
summaryTruncated: goal!.truncated,
|
|
2389
|
+
}
|
|
2390
|
+
: null,
|
|
2391
|
+
queuedPromptCount: session.queuedPromptCount,
|
|
2392
|
+
children: session.treeStats,
|
|
2393
|
+
...(includeLastMessage
|
|
2394
|
+
? {
|
|
2395
|
+
latestMessage: session.latestMessage
|
|
2396
|
+
? {
|
|
2397
|
+
type: session.latestMessage.type,
|
|
2398
|
+
preview: preview!.text,
|
|
2399
|
+
previewTruncated: preview!.truncated,
|
|
2400
|
+
}
|
|
2401
|
+
: null,
|
|
2402
|
+
}
|
|
2403
|
+
: {}),
|
|
2404
|
+
createdAt: session.createdAt,
|
|
2405
|
+
updatedAt: session.updatedAt,
|
|
2406
|
+
};
|
|
2407
|
+
});
|
|
2408
|
+
|
|
2409
|
+
// The database order is already the useful discovery order. Spend the
|
|
2410
|
+
// separate preview budget in that order so the first page retains previews
|
|
2411
|
+
// for the most relevant rows and later rows remain discoverable by status.
|
|
2412
|
+
let budgetBytes = 0;
|
|
2413
|
+
const budgeted = includeLastMessage
|
|
2414
|
+
? projected.map((session) => {
|
|
2415
|
+
const latestMessage = session.latestMessage;
|
|
2416
|
+
if (!latestMessage || latestMessage.preview === null) return session;
|
|
2417
|
+
const candidateBytes = Buffer.byteLength(latestMessage.preview, "utf8");
|
|
2418
|
+
if (budgetBytes + candidateBytes <= SESSION_DISCOVERY_PREVIEW_MAX_BYTES) {
|
|
2419
|
+
budgetBytes += candidateBytes;
|
|
2420
|
+
return session;
|
|
2421
|
+
}
|
|
2422
|
+
return {
|
|
2423
|
+
...session,
|
|
2424
|
+
latestMessage: {
|
|
2425
|
+
...latestMessage,
|
|
2426
|
+
preview: null,
|
|
2427
|
+
previewOmitted: true,
|
|
2428
|
+
previewOmissionReason: SESSION_DISCOVERY_PREVIEW_OMISSION_REASON,
|
|
2429
|
+
previewDrillDownTool: SESSION_DISCOVERY_PREVIEW_DRILL_DOWN_TOOL,
|
|
2430
|
+
previewDrillDownInput: sessionDiscoveryPreviewDrillDownInput(
|
|
2431
|
+
session.id,
|
|
2432
|
+
latestMessage.type,
|
|
2433
|
+
),
|
|
2434
|
+
},
|
|
2435
|
+
};
|
|
2436
|
+
})
|
|
2437
|
+
: projected;
|
|
2438
|
+
|
|
2439
|
+
let kept = budgeted;
|
|
2440
|
+
const build = () => {
|
|
2441
|
+
const previewBytes = includeLastMessage
|
|
2442
|
+
? kept.reduce(
|
|
2443
|
+
(total, session) =>
|
|
2444
|
+
total +
|
|
2445
|
+
(session.latestMessage?.preview === null || !session.latestMessage
|
|
2446
|
+
? 0
|
|
2447
|
+
: Buffer.byteLength(session.latestMessage.preview, "utf8")),
|
|
2448
|
+
0,
|
|
2449
|
+
)
|
|
2450
|
+
: 0;
|
|
2451
|
+
const previewOmittedCount = includeLastMessage
|
|
2452
|
+
? kept.filter((session) => {
|
|
2453
|
+
const latestMessage = session.latestMessage;
|
|
2454
|
+
return (
|
|
2455
|
+
latestMessage != null &&
|
|
2456
|
+
"previewOmitted" in latestMessage &&
|
|
2457
|
+
latestMessage.previewOmitted === true
|
|
2458
|
+
);
|
|
2459
|
+
}).length
|
|
2460
|
+
: 0;
|
|
2461
|
+
const lastKept = kept.at(-1);
|
|
2462
|
+
const droppedForByteCap = kept.length < projected.length;
|
|
2463
|
+
const sourceLast = lastKept
|
|
2464
|
+
? page.sessions.find((session) => session.id === lastKept.id)
|
|
2465
|
+
: undefined;
|
|
2466
|
+
const nextCursor = droppedForByteCap
|
|
2467
|
+
? sourceLast
|
|
2468
|
+
? encodeSessionDiscoveryCursor({
|
|
2469
|
+
orderBy: page.orderBy,
|
|
2470
|
+
sortRevision: sourceLast.sortRevision,
|
|
2471
|
+
sortAt: sourceLast.sortAt,
|
|
2472
|
+
id: sourceLast.id,
|
|
2473
|
+
snapshotAt: page.snapshotAt,
|
|
2474
|
+
snapshotRevision: page.snapshotRevision,
|
|
2475
|
+
updatedAfter: page.updatedAfter,
|
|
2476
|
+
})
|
|
2477
|
+
: null
|
|
2478
|
+
: page.nextCursor
|
|
2479
|
+
? encodeSessionDiscoveryCursor(page.nextCursor)
|
|
2480
|
+
: null;
|
|
2481
|
+
const result = {
|
|
2482
|
+
sessions: kept,
|
|
2483
|
+
total: page.total,
|
|
2484
|
+
hasMore: page.hasMore || droppedForByteCap,
|
|
2485
|
+
nextCursor,
|
|
2486
|
+
orderBy: page.orderBy,
|
|
2487
|
+
snapshotAt: page.snapshotAt,
|
|
2488
|
+
snapshotRevision: page.snapshotRevision,
|
|
2489
|
+
updatedAfter: page.updatedAfter,
|
|
2490
|
+
updatedThrough: page.updatedThrough,
|
|
2491
|
+
...(includeLastMessage
|
|
2492
|
+
? {
|
|
2493
|
+
latestMessagePreviewBudget: {
|
|
2494
|
+
bytes: previewBytes,
|
|
2495
|
+
maxBytes: SESSION_DISCOVERY_PREVIEW_MAX_BYTES,
|
|
2496
|
+
omittedCount: previewOmittedCount,
|
|
2497
|
+
truncated: previewOmittedCount > 0,
|
|
2498
|
+
omissionReason:
|
|
2499
|
+
previewOmittedCount > 0 ? SESSION_DISCOVERY_PREVIEW_OMISSION_REASON : null,
|
|
2500
|
+
drillDownTool: SESSION_DISCOVERY_PREVIEW_DRILL_DOWN_TOOL,
|
|
2501
|
+
drillDownInput: {
|
|
2502
|
+
includeTypes: ["user.message", "agent.message.completed"] as const,
|
|
2503
|
+
...SESSION_DISCOVERY_PREVIEW_DRILL_DOWN_BASE_INPUT,
|
|
2504
|
+
},
|
|
2505
|
+
},
|
|
2506
|
+
}
|
|
2507
|
+
: {}),
|
|
2508
|
+
responseTruncated: droppedForByteCap,
|
|
2509
|
+
...(droppedForByteCap
|
|
2510
|
+
? {
|
|
2511
|
+
truncationReason: `response exceeded ${SESSION_DISCOVERY_PAGE_MAX_BYTES} bytes; continue with nextCursor`,
|
|
2512
|
+
}
|
|
2513
|
+
: {}),
|
|
2514
|
+
bytes: 0,
|
|
2515
|
+
maxBytes: SESSION_DISCOVERY_PAGE_MAX_BYTES,
|
|
2516
|
+
};
|
|
2517
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
2518
|
+
const measured = Buffer.byteLength(JSON.stringify(result, null, 2), "utf8");
|
|
2519
|
+
if (result.bytes === measured) break;
|
|
2520
|
+
result.bytes = measured;
|
|
2521
|
+
}
|
|
2522
|
+
return result;
|
|
2523
|
+
};
|
|
2524
|
+
|
|
2525
|
+
let result = build();
|
|
2526
|
+
while (result.bytes > SESSION_DISCOVERY_PAGE_MAX_BYTES && kept.length > 1) {
|
|
2527
|
+
kept = kept.slice(0, -1);
|
|
2528
|
+
result = build();
|
|
2529
|
+
}
|
|
2530
|
+
if (result.bytes > SESSION_DISCOVERY_PAGE_MAX_BYTES) {
|
|
2531
|
+
throw new RangeError(
|
|
2532
|
+
`sessions_list metadata exceeds its ${SESSION_DISCOVERY_PAGE_MAX_BYTES}-byte envelope`,
|
|
2533
|
+
);
|
|
2534
|
+
}
|
|
2535
|
+
return result;
|
|
2536
|
+
}
|
|
2537
|
+
|
|
1910
2538
|
function parseMcpDate(raw: string, label: string): Date {
|
|
1911
2539
|
const date = new Date(raw);
|
|
1912
2540
|
if (Number.isNaN(date.getTime())) {
|