@opengeni/api-router 2.3.2-canary.2 → 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/auth/managed-auth-attempt-context.d.ts +4 -0
- package/dist/auth/managed-auth-session-adapter.d.ts +4 -0
- package/dist/{chunk-IBV7Z6F4.js → chunk-QESX7HDK.js} +3645 -470
- package/dist/chunk-QESX7HDK.js.map +1 -0
- package/dist/fatal-process-boundary.d.ts +25 -0
- package/dist/http/sse.d.ts +2 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +168 -6
- package/dist/index.js.map +1 -1
- package/dist/integrations/slack-interactions.d.ts +1 -1
- package/dist/mcp/receipts.d.ts +9 -0
- package/dist/mcp/server.d.ts +33 -0
- package/dist/organization-recovery-notifications.d.ts +41 -0
- package/dist/routes/managed-auth-session-sets.d.ts +19 -0
- package/dist/routes/organization-recovery.d.ts +19 -0
- package/dist/routes/sessions.d.ts +17 -5
- package/dist/routes/workspaces.d.ts +1 -0
- package/dist/work-discovery-observability.d.ts +33 -0
- package/package.json +18 -18
- package/src/app.ts +133 -1
- package/src/auth/managed-auth-attempt-context.ts +24 -0
- package/src/auth/managed-auth-session-adapter.ts +205 -0
- package/src/auth/managed-auth.ts +52 -2
- package/src/fatal-process-boundary.ts +231 -0
- package/src/http/sse.ts +7 -0
- package/src/index.ts +25 -5
- package/src/integrations/slack-interactions.ts +30 -17
- package/src/mcp/receipts.ts +34 -0
- package/src/mcp/server.ts +349 -68
- package/src/organization-recovery-notifications.ts +103 -0
- package/src/routes/canonical-human-identities.ts +29 -14
- package/src/routes/codex.ts +5 -1
- package/src/routes/environments.ts +23 -0
- package/src/routes/interaction-resources.ts +3 -0
- package/src/routes/managed-auth-session-sets.ts +994 -0
- package/src/routes/managed-onboarding.ts +2 -0
- package/src/routes/organization-memberships.ts +2 -0
- package/src/routes/organization-recovery.ts +325 -0
- package/src/routes/sessions.ts +401 -120
- package/src/routes/supergrok.ts +5 -1
- package/src/routes/workspaces.ts +23 -1
- package/src/work-discovery-observability.ts +121 -0
- package/dist/chunk-IBV7Z6F4.js.map +0 -1
package/src/routes/sessions.ts
CHANGED
|
@@ -66,12 +66,23 @@ import {
|
|
|
66
66
|
UpdateSessionToolPolicyRequest,
|
|
67
67
|
ViewerHeartbeatRequest,
|
|
68
68
|
WORKSPACE_CONTROL_ACTOR_MAX_BYTES,
|
|
69
|
+
WORK_CLAIM_CANONICAL_KEY_MAX_BYTES,
|
|
70
|
+
WORK_CLAIM_DISCOVERY_LIMIT,
|
|
71
|
+
WORK_CLAIM_NAMESPACE_MAX_BYTES,
|
|
72
|
+
WORK_DISCOVERY_QUERY_MAX_CHARS,
|
|
73
|
+
WORK_DISCOVERY_RECENT_HOURS_MAX,
|
|
74
|
+
WorkClaimSubjectFilter as WorkClaimSubjectFilterSchema,
|
|
75
|
+
WorkClaimSubjectType,
|
|
76
|
+
normalizeWorkClaimCanonicalKey,
|
|
77
|
+
normalizeWorkClaimNamespace,
|
|
69
78
|
workspaceControlUtf8Bytes,
|
|
70
79
|
type AccessGrant,
|
|
71
80
|
type AttachViewerResponse,
|
|
72
81
|
type SandboxBackend,
|
|
73
82
|
type LineageNode,
|
|
74
83
|
type Session,
|
|
84
|
+
type SessionStatus,
|
|
85
|
+
type WorkClaimSubjectFilter,
|
|
75
86
|
type SessionGoalRevision,
|
|
76
87
|
type AgentTopologyPageResponse,
|
|
77
88
|
type ErrorCode,
|
|
@@ -173,6 +184,7 @@ import {
|
|
|
173
184
|
type SandboxRetainedProcess,
|
|
174
185
|
type Database,
|
|
175
186
|
type SessionDiscoveryCursor,
|
|
187
|
+
type SessionDiscoveryOrderBy,
|
|
176
188
|
type SessionDiscoveryAncestor,
|
|
177
189
|
} from "@opengeni/db";
|
|
178
190
|
import {
|
|
@@ -211,6 +223,7 @@ import type { Context, Hono, MiddlewareHandler } from "hono";
|
|
|
211
223
|
import { HTTPException } from "hono/http-exception";
|
|
212
224
|
import type { ContentfulStatusCode } from "hono/utils/http-status";
|
|
213
225
|
import {
|
|
226
|
+
getManagedAuthRequestActorEpoch,
|
|
214
227
|
hasPermission,
|
|
215
228
|
requireAccessGrant,
|
|
216
229
|
requireAccessGrantAuthorization,
|
|
@@ -243,7 +256,6 @@ import {
|
|
|
243
256
|
import { buildSessionCodexRealtimeBroker, CodexRealtimeBrokerError } from "../codex-realtime";
|
|
244
257
|
import {
|
|
245
258
|
acceptSessionUserMessage,
|
|
246
|
-
acceptSessionUserMessageWithOutcome,
|
|
247
259
|
controlHumanSessionWorkstream,
|
|
248
260
|
createSessionForRequest,
|
|
249
261
|
deleteHumanQueuePrompt,
|
|
@@ -259,6 +271,7 @@ import {
|
|
|
259
271
|
SessionSpawnDeniedError,
|
|
260
272
|
sessionSpawnDenialEnvelope,
|
|
261
273
|
steerHumanQueuePrompt,
|
|
274
|
+
submitComposerDraftForRequest,
|
|
262
275
|
updateSessionMcpApprovalPolicy,
|
|
263
276
|
updateManagedHumanSessionVisibility,
|
|
264
277
|
updateSessionToolPolicy,
|
|
@@ -279,6 +292,7 @@ import {
|
|
|
279
292
|
} from "./workspace-capture";
|
|
280
293
|
import { publishSandboxFileArtifact } from "../sandbox-file-artifacts";
|
|
281
294
|
import { ApiHttpError } from "../http/api-error";
|
|
295
|
+
import { observeWorkDiscovery, summarizeWorkDiscoveryRows } from "../work-discovery-observability";
|
|
282
296
|
|
|
283
297
|
type SessionRouteDeps = ApiRouteDeps & Pick<ViewerServices, "establishSandboxSession">;
|
|
284
298
|
|
|
@@ -699,71 +713,144 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
699
713
|
throw sessionAuthorizationHttpError(error);
|
|
700
714
|
}
|
|
701
715
|
const query = agentTopologyQuery(c.req.query());
|
|
702
|
-
const
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
716
|
+
const startedAtMs = performance.now();
|
|
717
|
+
const mode = query.subject ? "subject" : query.query ? "query" : "browse";
|
|
718
|
+
const metricAuthorizationScope = authorizationScope?.kind === "scoped" ? "scoped" : "workspace";
|
|
719
|
+
if ((query.query || query.subject) && !settings.workDiscoveryEnabled) {
|
|
720
|
+
observeWorkDiscovery(deps.observability, {
|
|
721
|
+
surface: "agent_topology",
|
|
722
|
+
mode,
|
|
723
|
+
outcome: "disabled",
|
|
724
|
+
authorizationScope: metricAuthorizationScope,
|
|
725
|
+
durationMs: performance.now() - startedAtMs,
|
|
726
|
+
responseBytes: 0,
|
|
727
|
+
resultCount: 0,
|
|
728
|
+
overlapCount: 0,
|
|
729
|
+
matchCounts: {},
|
|
730
|
+
});
|
|
731
|
+
throw new HTTPException(503, {
|
|
732
|
+
message: "Agent work discovery is disabled by the operator.",
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
try {
|
|
736
|
+
const orderBy: SessionDiscoveryOrderBy =
|
|
737
|
+
query.query || query.subject ? "relevance" : "updatedAt";
|
|
738
|
+
const page = await listSessionDiscoverySummaries(db, workspaceId, {
|
|
739
|
+
limit: query.limit,
|
|
740
|
+
orderBy,
|
|
741
|
+
subjectId: grant.subjectId,
|
|
742
|
+
...(query.cursor ? { cursor: query.cursor } : {}),
|
|
743
|
+
...(query.parentSessionId !== undefined ? { parentSessionId: query.parentSessionId } : {}),
|
|
744
|
+
...(query.rootSessionId ? { rootSessionId: query.rootSessionId } : {}),
|
|
745
|
+
...(query.query ? { query: query.query } : {}),
|
|
746
|
+
...(query.statuses ? { statuses: query.statuses } : {}),
|
|
747
|
+
activeOnly: query.activeOnly,
|
|
748
|
+
...(query.recentHours !== undefined ? { recentHours: query.recentHours } : {}),
|
|
749
|
+
...(query.subject ? { subject: query.subject } : {}),
|
|
750
|
+
...(query.claimLimit !== undefined ? { claimLimit: query.claimLimit } : {}),
|
|
751
|
+
includeWorkDiscovery: settings.workDiscoveryEnabled,
|
|
752
|
+
...(authorizationScope ? { authorizationScope } : {}),
|
|
753
|
+
});
|
|
754
|
+
const ancestorPaths =
|
|
755
|
+
query.query || query.subject
|
|
756
|
+
? await listSessionDiscoveryAncestorPaths(
|
|
757
|
+
db,
|
|
758
|
+
workspaceId,
|
|
759
|
+
page.sessions.map((session) => session.id),
|
|
760
|
+
authorizationScope ?? undefined,
|
|
761
|
+
grant.subjectId,
|
|
762
|
+
)
|
|
763
|
+
: new Map<string, SessionDiscoveryAncestor[]>();
|
|
764
|
+
const sessions: AgentTopologyPageResponse["sessions"] = page.sessions.map((session) => {
|
|
765
|
+
const blocker = session.effectiveControl.primaryBlocker;
|
|
766
|
+
return {
|
|
767
|
+
id: session.id,
|
|
768
|
+
title: session.title,
|
|
732
769
|
titleTruncated:
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
770
|
+
session.titleOriginalChars !== null &&
|
|
771
|
+
session.titleOriginalChars > Array.from(session.title ?? "").length,
|
|
772
|
+
parentSessionId: session.parentSessionId,
|
|
773
|
+
rootSessionId: session.rootSessionId,
|
|
774
|
+
nestedAgentDepth: session.nestedAgentDepth,
|
|
775
|
+
ancestorPath: (ancestorPaths.get(session.id) ?? []).map((ancestor) => ({
|
|
776
|
+
id: ancestor.id,
|
|
777
|
+
title: ancestor.title,
|
|
778
|
+
titleTruncated:
|
|
779
|
+
ancestor.titleOriginalChars !== null &&
|
|
780
|
+
ancestor.titleOriginalChars > Array.from(ancestor.title ?? "").length,
|
|
781
|
+
})),
|
|
782
|
+
status: session.status,
|
|
783
|
+
goal: session.goal
|
|
741
784
|
? {
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
blocker.displayNameOriginalChars > Array.from(blocker.displayName).length,
|
|
785
|
+
status: session.goal.status,
|
|
786
|
+
summary: session.goal.text,
|
|
787
|
+
summaryTruncated:
|
|
788
|
+
session.goal.textOriginalChars > Array.from(session.goal.text).length,
|
|
747
789
|
}
|
|
748
790
|
: null,
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
791
|
+
pause: {
|
|
792
|
+
state: session.effectiveControl.state,
|
|
793
|
+
additionalBlockerCount: session.effectiveControl.additionalBlockerCount,
|
|
794
|
+
source: blocker
|
|
795
|
+
? {
|
|
796
|
+
kind: blocker.kind,
|
|
797
|
+
...(blocker.sessionId ? { sessionId: blocker.sessionId } : {}),
|
|
798
|
+
displayName: blocker.displayName,
|
|
799
|
+
displayNameTruncated:
|
|
800
|
+
blocker.displayNameOriginalChars > Array.from(blocker.displayName).length,
|
|
801
|
+
}
|
|
802
|
+
: null,
|
|
803
|
+
},
|
|
804
|
+
children: session.treeStats,
|
|
805
|
+
relatedWork: session.workDiscovery,
|
|
806
|
+
createdAt: session.createdAt,
|
|
807
|
+
updatedAt: session.updatedAt,
|
|
808
|
+
};
|
|
809
|
+
});
|
|
810
|
+
const response = {
|
|
811
|
+
sessions,
|
|
812
|
+
total: page.total,
|
|
813
|
+
hasMore: page.hasMore,
|
|
814
|
+
humanAdvisoriesEnabled:
|
|
815
|
+
settings.workDiscoveryEnabled && settings.workDiscoveryHumanAdvisoriesEnabled,
|
|
816
|
+
nextCursor: page.nextCursor
|
|
817
|
+
? encodeAgentTopologyCursor({
|
|
818
|
+
cursor: page.nextCursor,
|
|
819
|
+
parentSessionId: query.parentSessionId === undefined ? "all" : query.parentSessionId,
|
|
820
|
+
rootSessionId: query.rootSessionId ?? null,
|
|
821
|
+
query: query.query ?? null,
|
|
822
|
+
statuses: query.statuses ?? [],
|
|
823
|
+
activeOnly: query.activeOnly,
|
|
824
|
+
recentHours: query.recentHours ?? null,
|
|
825
|
+
subject: query.subject ?? null,
|
|
826
|
+
claimLimit: query.claimLimit ?? null,
|
|
827
|
+
})
|
|
828
|
+
: null,
|
|
829
|
+
} satisfies AgentTopologyPageResponse;
|
|
830
|
+
observeWorkDiscovery(deps.observability, {
|
|
831
|
+
surface: "agent_topology",
|
|
832
|
+
mode,
|
|
833
|
+
outcome: sessions.length === 0 ? "empty" : "ok",
|
|
834
|
+
authorizationScope: metricAuthorizationScope,
|
|
835
|
+
durationMs: performance.now() - startedAtMs,
|
|
836
|
+
responseBytes: Buffer.byteLength(JSON.stringify(response), "utf8"),
|
|
837
|
+
...summarizeWorkDiscoveryRows(sessions),
|
|
838
|
+
});
|
|
839
|
+
return c.json(response);
|
|
840
|
+
} catch (error) {
|
|
841
|
+
observeWorkDiscovery(deps.observability, {
|
|
842
|
+
surface: "agent_topology",
|
|
843
|
+
mode,
|
|
844
|
+
outcome: "error",
|
|
845
|
+
authorizationScope: metricAuthorizationScope,
|
|
846
|
+
durationMs: performance.now() - startedAtMs,
|
|
847
|
+
responseBytes: 0,
|
|
848
|
+
resultCount: 0,
|
|
849
|
+
overlapCount: 0,
|
|
850
|
+
matchCounts: {},
|
|
851
|
+
});
|
|
852
|
+
throw error;
|
|
853
|
+
}
|
|
767
854
|
});
|
|
768
855
|
|
|
769
856
|
app.get("/v1/workspaces/:workspaceId/sessions/:sessionId", async (c) => {
|
|
@@ -1572,7 +1659,13 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1572
1659
|
// while the actively-working label remains independent of acknowledgment.
|
|
1573
1660
|
app.put("/v1/workspaces/:workspaceId/sessions/:sessionId/attention", async (c) => {
|
|
1574
1661
|
const workspaceId = c.req.param("workspaceId");
|
|
1575
|
-
const
|
|
1662
|
+
const authorization = await requireAccessGrantAuthorization(
|
|
1663
|
+
c,
|
|
1664
|
+
deps,
|
|
1665
|
+
workspaceId,
|
|
1666
|
+
"sessions:read",
|
|
1667
|
+
);
|
|
1668
|
+
const grant = authorization.grant;
|
|
1576
1669
|
const sessionId = c.req.param("sessionId");
|
|
1577
1670
|
if (!z.string().uuid().safeParse(sessionId).success) {
|
|
1578
1671
|
throw new HTTPException(404, { message: "session not found" });
|
|
@@ -1586,6 +1679,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1586
1679
|
workspaceId,
|
|
1587
1680
|
subjectId: grant.subjectId,
|
|
1588
1681
|
sessionId,
|
|
1682
|
+
personalWorkspaceOwnerException: authorization.canonicalManagedHumanSession,
|
|
1589
1683
|
...parsed.data,
|
|
1590
1684
|
});
|
|
1591
1685
|
if (!session) throw new HTTPException(404, { message: "session not found" });
|
|
@@ -1618,7 +1712,13 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1618
1712
|
// for this member and remain recoverable through the archived list view.
|
|
1619
1713
|
app.put("/v1/workspaces/:workspaceId/sessions/:sessionId/archive", async (c) => {
|
|
1620
1714
|
const workspaceId = c.req.param("workspaceId");
|
|
1621
|
-
const
|
|
1715
|
+
const authorization = await requireAccessGrantAuthorization(
|
|
1716
|
+
c,
|
|
1717
|
+
deps,
|
|
1718
|
+
workspaceId,
|
|
1719
|
+
"sessions:read",
|
|
1720
|
+
);
|
|
1721
|
+
const grant = authorization.grant;
|
|
1622
1722
|
const sessionId = c.req.param("sessionId");
|
|
1623
1723
|
const parsed = UpdateSessionArchiveRequest.safeParse(await c.req.json().catch(() => null));
|
|
1624
1724
|
if (!parsed.success) {
|
|
@@ -1629,6 +1729,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1629
1729
|
workspaceId,
|
|
1630
1730
|
subjectId: grant.subjectId,
|
|
1631
1731
|
sessionId,
|
|
1732
|
+
personalWorkspaceOwnerException: authorization.canonicalManagedHumanSession,
|
|
1632
1733
|
...parsed.data,
|
|
1633
1734
|
});
|
|
1634
1735
|
if (!session) throw new HTTPException(404, { message: "session not found" });
|
|
@@ -2559,6 +2660,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2559
2660
|
c.req.raw.signal,
|
|
2560
2661
|
{
|
|
2561
2662
|
observability: deps.observability,
|
|
2663
|
+
actorEpoch: getManagedAuthRequestActorEpoch(c.req.raw) ?? undefined,
|
|
2562
2664
|
reauthorizeAfterMs:
|
|
2563
2665
|
authorization?.reauthorizeAfterMs ?? SESSION_AUTHORIZATION_DEFAULT_REAUTHORIZE_MS,
|
|
2564
2666
|
reauthorize: async () => {
|
|
@@ -2832,46 +2934,15 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2832
2934
|
const sessionId = c.req.param("sessionId");
|
|
2833
2935
|
await assertSessionExists(db, workspaceId, sessionId);
|
|
2834
2936
|
const payload = SubmitComposerDraftRequest.parse(await c.req.json().catch(() => null));
|
|
2835
|
-
let result: Awaited<ReturnType<typeof
|
|
2937
|
+
let result: Awaited<ReturnType<typeof submitComposerDraftForRequest>>;
|
|
2836
2938
|
try {
|
|
2837
|
-
result = await
|
|
2838
|
-
text: payload.text,
|
|
2839
|
-
annotations: payload.annotations,
|
|
2840
|
-
modelContext: payload.modelContext ?? null,
|
|
2841
|
-
resources: payload.resources,
|
|
2842
|
-
model: payload.model,
|
|
2843
|
-
reasoningEffort: payload.reasoningEffort,
|
|
2844
|
-
latencyMode: payload.latencyMode,
|
|
2845
|
-
mcpCredentialUpdates: payload.mcpCredentialUpdates ?? [],
|
|
2846
|
-
connectionAuthorities: payload.connectionAuthorities,
|
|
2847
|
-
...(payload.personalResourceAttachment
|
|
2848
|
-
? { personalResourceAttachment: payload.personalResourceAttachment }
|
|
2849
|
-
: {}),
|
|
2939
|
+
result = await submitComposerDraftForRequest(deps, grant, workspaceId, sessionId, payload, {
|
|
2850
2940
|
authorization,
|
|
2851
|
-
delivery: payload.delivery,
|
|
2852
|
-
origin: "human",
|
|
2853
|
-
expectedDraftRevision: payload.expectedDraftRevision,
|
|
2854
|
-
clientEventId: payload.clientEventId,
|
|
2855
|
-
...(payload.controlEtag ? { controlEtag: payload.controlEtag } : {}),
|
|
2856
2941
|
});
|
|
2857
2942
|
} catch (error) {
|
|
2858
2943
|
return commandConflictResponse(c, error);
|
|
2859
2944
|
}
|
|
2860
|
-
|
|
2861
|
-
throw new Error("Accepted composer draft submission did not return its next draft");
|
|
2862
|
-
}
|
|
2863
|
-
return c.json(
|
|
2864
|
-
{
|
|
2865
|
-
accepted: result.accepted,
|
|
2866
|
-
turn: result.turn,
|
|
2867
|
-
draft: result.draft,
|
|
2868
|
-
receipt: result.receipt,
|
|
2869
|
-
routing: result.routing,
|
|
2870
|
-
interruptionCount: result.interruptionCount,
|
|
2871
|
-
replay: result.replay,
|
|
2872
|
-
},
|
|
2873
|
-
202,
|
|
2874
|
-
);
|
|
2945
|
+
return c.json(result, 202);
|
|
2875
2946
|
});
|
|
2876
2947
|
|
|
2877
2948
|
app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/events", async (c) => {
|
|
@@ -2995,10 +3066,10 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
2995
3066
|
}
|
|
2996
3067
|
if (accepted.action === "conflict") {
|
|
2997
3068
|
throw new HTTPException(409, {
|
|
2998
|
-
message:
|
|
3069
|
+
message: "human-input request is not currently actionable",
|
|
2999
3070
|
});
|
|
3000
3071
|
}
|
|
3001
|
-
return c.json(accepted.event, 202);
|
|
3072
|
+
return c.json(accepted.event, accepted.action === "completed" ? 200 : 202);
|
|
3002
3073
|
}
|
|
3003
3074
|
});
|
|
3004
3075
|
|
|
@@ -4524,12 +4595,18 @@ function sessionListQuery(
|
|
|
4524
4595
|
|
|
4525
4596
|
export type AgentTopologyCursorEnvelope = {
|
|
4526
4597
|
cursor: SessionDiscoveryCursor;
|
|
4527
|
-
parentSessionId: string | null;
|
|
4528
|
-
|
|
4598
|
+
parentSessionId: string | null | "all";
|
|
4599
|
+
rootSessionId: string | null;
|
|
4600
|
+
query: string | null;
|
|
4601
|
+
statuses: SessionStatus[];
|
|
4602
|
+
activeOnly: boolean;
|
|
4603
|
+
recentHours: number | null;
|
|
4604
|
+
subject: WorkClaimSubjectFilter | null;
|
|
4605
|
+
claimLimit: number | null;
|
|
4529
4606
|
};
|
|
4530
4607
|
|
|
4531
4608
|
export function encodeAgentTopologyCursor(value: AgentTopologyCursorEnvelope): string {
|
|
4532
|
-
return Buffer.from(JSON.stringify({ v:
|
|
4609
|
+
return Buffer.from(JSON.stringify({ v: 2, ...value }), "utf8").toString("base64url");
|
|
4533
4610
|
}
|
|
4534
4611
|
|
|
4535
4612
|
function decodeAgentTopologyCursor(value: string): AgentTopologyCursorEnvelope {
|
|
@@ -4539,25 +4616,92 @@ function decodeAgentTopologyCursor(value: string): AgentTopologyCursorEnvelope {
|
|
|
4539
4616
|
});
|
|
4540
4617
|
}
|
|
4541
4618
|
try {
|
|
4542
|
-
const
|
|
4619
|
+
const decoded = JSON.parse(Buffer.from(value, "base64url").toString("utf8")) as unknown;
|
|
4620
|
+
const legacy = z
|
|
4543
4621
|
.object({
|
|
4544
4622
|
v: z.literal(1),
|
|
4545
4623
|
parentSessionId: z.string().uuid().nullable(),
|
|
4546
|
-
search: z.string().max(
|
|
4624
|
+
search: z.string().max(WORK_DISCOVERY_QUERY_MAX_CHARS).nullable(),
|
|
4547
4625
|
cursor: z.object({
|
|
4548
|
-
orderBy: z.
|
|
4626
|
+
orderBy: z.literal("updatedAt"),
|
|
4549
4627
|
sortRevision: z.string().max(64),
|
|
4550
4628
|
sortAt: z.string().max(64),
|
|
4551
4629
|
id: z.string().uuid(),
|
|
4552
4630
|
snapshotAt: z.string().max(64),
|
|
4553
4631
|
snapshotRevision: z.string().max(64),
|
|
4554
|
-
updatedAfter: z.
|
|
4632
|
+
updatedAfter: z.null(),
|
|
4555
4633
|
}),
|
|
4556
4634
|
})
|
|
4557
|
-
.
|
|
4635
|
+
.safeParse(decoded);
|
|
4636
|
+
if (legacy.success) {
|
|
4637
|
+
if (legacy.data.search !== null) {
|
|
4638
|
+
throw new Error("legacy search cursor is not relevance-fenced");
|
|
4639
|
+
}
|
|
4640
|
+
const cursor = {
|
|
4641
|
+
...legacy.data.cursor,
|
|
4642
|
+
sortRank: null,
|
|
4643
|
+
filterHash: null,
|
|
4644
|
+
} satisfies SessionDiscoveryCursor;
|
|
4645
|
+
return {
|
|
4646
|
+
cursor,
|
|
4647
|
+
parentSessionId: legacy.data.parentSessionId,
|
|
4648
|
+
rootSessionId: null,
|
|
4649
|
+
query: null,
|
|
4650
|
+
statuses: [],
|
|
4651
|
+
activeOnly: false,
|
|
4652
|
+
recentHours: null,
|
|
4653
|
+
subject: null,
|
|
4654
|
+
claimLimit: null,
|
|
4655
|
+
};
|
|
4656
|
+
}
|
|
4657
|
+
const parsed = z
|
|
4658
|
+
.object({
|
|
4659
|
+
v: z.literal(2),
|
|
4660
|
+
parentSessionId: z.union([z.string().uuid(), z.literal("all"), z.null()]),
|
|
4661
|
+
rootSessionId: z.string().uuid().nullable(),
|
|
4662
|
+
query: z.string().max(WORK_DISCOVERY_QUERY_MAX_CHARS).nullable(),
|
|
4663
|
+
statuses: z
|
|
4664
|
+
.array(
|
|
4665
|
+
z.enum([
|
|
4666
|
+
"queued",
|
|
4667
|
+
"running",
|
|
4668
|
+
"idle",
|
|
4669
|
+
"requires_action",
|
|
4670
|
+
"recovering",
|
|
4671
|
+
"waiting_capacity",
|
|
4672
|
+
"failed",
|
|
4673
|
+
"cancelled",
|
|
4674
|
+
]),
|
|
4675
|
+
)
|
|
4676
|
+
.max(8),
|
|
4677
|
+
activeOnly: z.boolean(),
|
|
4678
|
+
recentHours: z.number().int().positive().max(WORK_DISCOVERY_RECENT_HOURS_MAX).nullable(),
|
|
4679
|
+
subject: z
|
|
4680
|
+
.object({
|
|
4681
|
+
namespace: z.string().min(1).max(WORK_CLAIM_NAMESPACE_MAX_BYTES),
|
|
4682
|
+
type: WorkClaimSubjectType,
|
|
4683
|
+
canonicalKey: z.string().min(1).max(WORK_CLAIM_CANONICAL_KEY_MAX_BYTES),
|
|
4684
|
+
})
|
|
4685
|
+
.strict()
|
|
4686
|
+
.nullable(),
|
|
4687
|
+
claimLimit: z.number().int().positive().max(WORK_CLAIM_DISCOVERY_LIMIT).nullable(),
|
|
4688
|
+
cursor: z.object({
|
|
4689
|
+
orderBy: z.enum(["updatedAt", "relevance"]),
|
|
4690
|
+
sortRank: z.number().int().nonnegative().nullable(),
|
|
4691
|
+
sortRevision: z.string().max(64),
|
|
4692
|
+
sortAt: z.string().max(64),
|
|
4693
|
+
id: z.string().uuid(),
|
|
4694
|
+
snapshotAt: z.string().max(64),
|
|
4695
|
+
snapshotRevision: z.string().max(64),
|
|
4696
|
+
updatedAfter: z.null(),
|
|
4697
|
+
filterHash: z
|
|
4698
|
+
.string()
|
|
4699
|
+
.regex(/^[0-9a-f]{64}$/)
|
|
4700
|
+
.nullable(),
|
|
4701
|
+
}),
|
|
4702
|
+
})
|
|
4703
|
+
.parse(decoded);
|
|
4558
4704
|
if (
|
|
4559
|
-
parsed.cursor.orderBy !== "updatedAt" ||
|
|
4560
|
-
parsed.cursor.updatedAfter !== null ||
|
|
4561
4705
|
!/^(?:0|[1-9]\d*)$/.test(parsed.cursor.sortRevision) ||
|
|
4562
4706
|
!/^(?:0|[1-9]\d*)$/.test(parsed.cursor.snapshotRevision) ||
|
|
4563
4707
|
BigInt(parsed.cursor.sortRevision) > 9_223_372_036_854_775_807n ||
|
|
@@ -4567,6 +4711,14 @@ function decodeAgentTopologyCursor(value: string): AgentTopologyCursorEnvelope {
|
|
|
4567
4711
|
) {
|
|
4568
4712
|
throw new Error("invalid topology cursor fields");
|
|
4569
4713
|
}
|
|
4714
|
+
if (
|
|
4715
|
+
(parsed.cursor.orderBy === "relevance" &&
|
|
4716
|
+
(parsed.cursor.sortRank === null || parsed.cursor.filterHash === null)) ||
|
|
4717
|
+
(parsed.cursor.orderBy === "updatedAt" &&
|
|
4718
|
+
(parsed.cursor.sortRank !== null || parsed.cursor.filterHash !== null))
|
|
4719
|
+
) {
|
|
4720
|
+
throw new Error("invalid topology cursor relevance fields");
|
|
4721
|
+
}
|
|
4570
4722
|
return parsed;
|
|
4571
4723
|
} catch {
|
|
4572
4724
|
throw new HTTPException(400, {
|
|
@@ -4577,8 +4729,14 @@ function decodeAgentTopologyCursor(value: string): AgentTopologyCursorEnvelope {
|
|
|
4577
4729
|
|
|
4578
4730
|
export function agentTopologyQuery(query: Record<string, string>): {
|
|
4579
4731
|
limit: number;
|
|
4580
|
-
parentSessionId: string | null;
|
|
4581
|
-
|
|
4732
|
+
parentSessionId: string | null | undefined;
|
|
4733
|
+
rootSessionId: string | undefined;
|
|
4734
|
+
query: string | undefined;
|
|
4735
|
+
statuses: SessionStatus[] | undefined;
|
|
4736
|
+
activeOnly: boolean;
|
|
4737
|
+
recentHours: number | undefined;
|
|
4738
|
+
subject: WorkClaimSubjectFilter | undefined;
|
|
4739
|
+
claimLimit: number | undefined;
|
|
4582
4740
|
cursor: SessionDiscoveryCursor | undefined;
|
|
4583
4741
|
} {
|
|
4584
4742
|
const rawLimit = query.limit;
|
|
@@ -4598,22 +4756,139 @@ export function agentTopologyQuery(query: Record<string, string>): {
|
|
|
4598
4756
|
message: 'parentSessionId must be a session id or the literal "null"',
|
|
4599
4757
|
});
|
|
4600
4758
|
}
|
|
4601
|
-
const
|
|
4602
|
-
|
|
4603
|
-
|
|
4759
|
+
const rootSessionId = query.rootSessionId?.trim();
|
|
4760
|
+
if (rootSessionId && !z.string().uuid().safeParse(rootSessionId).success) {
|
|
4761
|
+
throw new HTTPException(400, { message: "rootSessionId must be a session id" });
|
|
4762
|
+
}
|
|
4763
|
+
const normalizeSearchQuery = (value: string | undefined): string | undefined => {
|
|
4764
|
+
if (value === undefined) return undefined;
|
|
4765
|
+
const canonical = value.normalize("NFKC");
|
|
4766
|
+
if (/[\u0000-\u001f\u007f-\u009f]/u.test(canonical)) {
|
|
4767
|
+
throw new HTTPException(400, { message: "query must not contain control characters" });
|
|
4768
|
+
}
|
|
4769
|
+
const normalized = canonical.trim().replace(/\s+/gu, " ").toLowerCase();
|
|
4770
|
+
if (!normalized) return undefined;
|
|
4771
|
+
if (Array.from(normalized).length > WORK_DISCOVERY_QUERY_MAX_CHARS) {
|
|
4772
|
+
throw new HTTPException(400, {
|
|
4773
|
+
message: `query must be at most ${WORK_DISCOVERY_QUERY_MAX_CHARS} characters`,
|
|
4774
|
+
});
|
|
4775
|
+
}
|
|
4776
|
+
return normalized;
|
|
4777
|
+
};
|
|
4778
|
+
const requestedQuery = normalizeSearchQuery(query.query);
|
|
4779
|
+
const legacySearch = normalizeSearchQuery(query.search);
|
|
4780
|
+
const searchQuery = requestedQuery ?? legacySearch;
|
|
4781
|
+
if (requestedQuery && legacySearch && requestedQuery !== legacySearch) {
|
|
4782
|
+
throw new HTTPException(400, { message: "query and legacy search must match" });
|
|
4783
|
+
}
|
|
4784
|
+
const statuses = query.statuses
|
|
4785
|
+
? [
|
|
4786
|
+
...new Set(
|
|
4787
|
+
query.statuses
|
|
4788
|
+
.split(",")
|
|
4789
|
+
.map((status) => status.trim())
|
|
4790
|
+
.filter(Boolean),
|
|
4791
|
+
),
|
|
4792
|
+
].sort()
|
|
4793
|
+
: [];
|
|
4794
|
+
const parsedStatuses = z
|
|
4795
|
+
.array(
|
|
4796
|
+
z.enum([
|
|
4797
|
+
"queued",
|
|
4798
|
+
"running",
|
|
4799
|
+
"idle",
|
|
4800
|
+
"requires_action",
|
|
4801
|
+
"recovering",
|
|
4802
|
+
"waiting_capacity",
|
|
4803
|
+
"failed",
|
|
4804
|
+
"cancelled",
|
|
4805
|
+
]),
|
|
4806
|
+
)
|
|
4807
|
+
.max(8)
|
|
4808
|
+
.safeParse(statuses);
|
|
4809
|
+
if (!parsedStatuses.success) {
|
|
4810
|
+
throw new HTTPException(400, { message: "statuses contains an unsupported lifecycle state" });
|
|
4811
|
+
}
|
|
4812
|
+
const activeOnly = query.activeOnly === "true";
|
|
4813
|
+
if (
|
|
4814
|
+
query.activeOnly !== undefined &&
|
|
4815
|
+
query.activeOnly !== "true" &&
|
|
4816
|
+
query.activeOnly !== "false"
|
|
4817
|
+
) {
|
|
4818
|
+
throw new HTTPException(400, { message: "activeOnly must be true or false" });
|
|
4819
|
+
}
|
|
4820
|
+
const recentHours = query.recentHours === undefined ? undefined : Number(query.recentHours);
|
|
4821
|
+
if (
|
|
4822
|
+
recentHours !== undefined &&
|
|
4823
|
+
(!Number.isSafeInteger(recentHours) ||
|
|
4824
|
+
recentHours < 1 ||
|
|
4825
|
+
recentHours > WORK_DISCOVERY_RECENT_HOURS_MAX)
|
|
4826
|
+
) {
|
|
4604
4827
|
throw new HTTPException(400, {
|
|
4605
|
-
message:
|
|
4828
|
+
message: `recentHours must be an integer between 1 and ${WORK_DISCOVERY_RECENT_HOURS_MAX}`,
|
|
4606
4829
|
});
|
|
4607
4830
|
}
|
|
4608
|
-
|
|
4831
|
+
const subjectFields = [query.subjectNamespace, query.subjectType, query.subjectKey];
|
|
4832
|
+
if (subjectFields.some((value) => value !== undefined) && subjectFields.some((value) => !value)) {
|
|
4609
4833
|
throw new HTTPException(400, {
|
|
4610
|
-
message: "
|
|
4834
|
+
message: "subjectNamespace, subjectType, and subjectKey must be supplied together",
|
|
4835
|
+
});
|
|
4836
|
+
}
|
|
4837
|
+
const parsedSubject = subjectFields.every((value) => value !== undefined)
|
|
4838
|
+
? WorkClaimSubjectFilterSchema.safeParse({
|
|
4839
|
+
namespace: normalizeWorkClaimNamespace(query.subjectNamespace!),
|
|
4840
|
+
type: query.subjectType,
|
|
4841
|
+
canonicalKey: normalizeWorkClaimCanonicalKey(query.subjectKey!),
|
|
4842
|
+
})
|
|
4843
|
+
: null;
|
|
4844
|
+
if (parsedSubject && !parsedSubject.success) {
|
|
4845
|
+
throw new HTTPException(400, { message: "exact subject filter is invalid" });
|
|
4846
|
+
}
|
|
4847
|
+
const subject = parsedSubject?.success ? parsedSubject.data : undefined;
|
|
4848
|
+
if (searchQuery && subject) {
|
|
4849
|
+
throw new HTTPException(400, { message: "query cannot be combined with an exact subject" });
|
|
4850
|
+
}
|
|
4851
|
+
const relevanceRequested = Boolean(searchQuery || subject);
|
|
4852
|
+
const parentSessionId =
|
|
4853
|
+
rawParent === undefined
|
|
4854
|
+
? relevanceRequested
|
|
4855
|
+
? undefined
|
|
4856
|
+
: null
|
|
4857
|
+
: rawParent === "null"
|
|
4858
|
+
? null
|
|
4859
|
+
: rawParent;
|
|
4860
|
+
const claimLimit = query.claimLimit === undefined ? undefined : Number(query.claimLimit);
|
|
4861
|
+
if (
|
|
4862
|
+
claimLimit !== undefined &&
|
|
4863
|
+
(!Number.isSafeInteger(claimLimit) || claimLimit < 1 || claimLimit > WORK_CLAIM_DISCOVERY_LIMIT)
|
|
4864
|
+
) {
|
|
4865
|
+
throw new HTTPException(400, {
|
|
4866
|
+
message: `claimLimit must be an integer between 1 and ${WORK_CLAIM_DISCOVERY_LIMIT}`,
|
|
4611
4867
|
});
|
|
4612
4868
|
}
|
|
4613
4869
|
const envelope = query.cursor ? decodeAgentTopologyCursor(query.cursor) : undefined;
|
|
4870
|
+
const expectedEnvelope = {
|
|
4871
|
+
parentSessionId: parentSessionId === undefined ? "all" : parentSessionId,
|
|
4872
|
+
rootSessionId: rootSessionId ?? null,
|
|
4873
|
+
query: searchQuery || null,
|
|
4874
|
+
statuses: parsedStatuses.data,
|
|
4875
|
+
activeOnly,
|
|
4876
|
+
recentHours: recentHours ?? null,
|
|
4877
|
+
subject: subject ?? null,
|
|
4878
|
+
claimLimit: claimLimit ?? null,
|
|
4879
|
+
};
|
|
4614
4880
|
if (
|
|
4615
4881
|
envelope &&
|
|
4616
|
-
|
|
4882
|
+
JSON.stringify({
|
|
4883
|
+
parentSessionId: envelope.parentSessionId,
|
|
4884
|
+
rootSessionId: envelope.rootSessionId,
|
|
4885
|
+
query: envelope.query,
|
|
4886
|
+
statuses: envelope.statuses,
|
|
4887
|
+
activeOnly: envelope.activeOnly,
|
|
4888
|
+
recentHours: envelope.recentHours,
|
|
4889
|
+
subject: envelope.subject,
|
|
4890
|
+
claimLimit: envelope.claimLimit,
|
|
4891
|
+
}) !== JSON.stringify(expectedEnvelope)
|
|
4617
4892
|
) {
|
|
4618
4893
|
throw new HTTPException(400, {
|
|
4619
4894
|
message: "agent topology cursor does not match its filters",
|
|
@@ -4622,7 +4897,13 @@ export function agentTopologyQuery(query: Record<string, string>): {
|
|
|
4622
4897
|
return {
|
|
4623
4898
|
limit,
|
|
4624
4899
|
parentSessionId,
|
|
4625
|
-
|
|
4900
|
+
rootSessionId: rootSessionId || undefined,
|
|
4901
|
+
query: searchQuery || undefined,
|
|
4902
|
+
statuses: parsedStatuses.data.length > 0 ? parsedStatuses.data : undefined,
|
|
4903
|
+
activeOnly,
|
|
4904
|
+
recentHours,
|
|
4905
|
+
subject,
|
|
4906
|
+
claimLimit,
|
|
4626
4907
|
cursor: envelope?.cursor,
|
|
4627
4908
|
};
|
|
4628
4909
|
}
|