@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.
@@ -2164,6 +2164,14 @@ import {
2164
2164
  TASK_NOTE_MAX_LIFETIME_DAYS,
2165
2165
  TASK_NOTE_REASON_MAX_BYTES,
2166
2166
  TASK_NOTE_TEXT_MAX_BYTES,
2167
+ WORK_CLAIM_CANONICAL_KEY_MAX_BYTES,
2168
+ WORK_CLAIM_DISPLAY_LABEL_MAX_BYTES,
2169
+ WORK_CLAIM_DISCOVERY_LIMIT,
2170
+ WORK_CLAIM_NAMESPACE_MAX_BYTES,
2171
+ WORK_CLAIM_VERSION_VALUE_MAX_BYTES,
2172
+ WORK_DISCOVERY_QUERY_MAX_CHARS,
2173
+ WORK_DISCOVERY_RECENT_HOURS_MAX,
2174
+ WorkClaimSubjectType,
2167
2175
  SubmitHumanInputResponseRequest
2168
2176
  } from "@opengeni/contracts";
2169
2177
  import {
@@ -2221,6 +2229,8 @@ import {
2221
2229
  createTaskNote,
2222
2230
  listTaskNotes,
2223
2231
  replaceTaskNote,
2232
+ releaseWorkClaim,
2233
+ upsertWorkClaim,
2224
2234
  acceptSessionHumanInputResponse,
2225
2235
  HumanInputResponseValidationError
2226
2236
  } from "@opengeni/db";
@@ -18354,6 +18364,89 @@ async function deleteScheduledTaskWithDurableCleanup(deps, input) {
18354
18364
  return { task: result.task, changed: result.changed };
18355
18365
  }
18356
18366
 
18367
+ // src/work-discovery-observability.ts
18368
+ var RESULT_BUCKETS = [0, 1, 2, 4, 8, 16, 32, 64, 100];
18369
+ var RESPONSE_BYTE_BUCKETS = [512, 1024, 4096, 16384, 65536, 128e3, 262144];
18370
+ var DURATION_BUCKETS = [5e-3, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5];
18371
+ function summarizeWorkDiscoveryRows(rows) {
18372
+ const matchCounts = {};
18373
+ let overlapCount = 0;
18374
+ for (const row of rows) {
18375
+ if (row.relatedWork.possibleOverlap) overlapCount += 1;
18376
+ const matchClass = row.relatedWork.match?.class;
18377
+ if (matchClass) matchCounts[matchClass] = (matchCounts[matchClass] ?? 0) + 1;
18378
+ }
18379
+ return { resultCount: rows.length, overlapCount, matchCounts };
18380
+ }
18381
+ function observeWorkDiscovery(observability, observation) {
18382
+ if (!observability) return;
18383
+ const labels = {
18384
+ surface: observation.surface,
18385
+ mode: observation.mode,
18386
+ outcome: observation.outcome,
18387
+ authorization_scope: observation.authorizationScope
18388
+ };
18389
+ try {
18390
+ observability.incrementCounter({
18391
+ name: "opengeni_work_discovery_requests_total",
18392
+ help: "Advisory work-discovery requests by bounded surface, mode, outcome, and authorization scope.",
18393
+ labels
18394
+ });
18395
+ observability.observeHistogram({
18396
+ name: "opengeni_work_discovery_duration_seconds",
18397
+ help: "Advisory work-discovery request duration in seconds.",
18398
+ labels,
18399
+ buckets: DURATION_BUCKETS,
18400
+ value: Math.max(0, observation.durationMs) / 1e3
18401
+ });
18402
+ observability.observeHistogram({
18403
+ name: "opengeni_work_discovery_results",
18404
+ help: "Bounded advisory work-discovery result rows per request.",
18405
+ labels: { surface: observation.surface, mode: observation.mode },
18406
+ buckets: RESULT_BUCKETS,
18407
+ value: Math.max(0, Math.floor(observation.resultCount))
18408
+ });
18409
+ observability.observeHistogram({
18410
+ name: "opengeni_work_discovery_response_bytes",
18411
+ help: "Serialized advisory work-discovery response bytes.",
18412
+ labels: { surface: observation.surface, mode: observation.mode },
18413
+ buckets: RESPONSE_BYTE_BUCKETS,
18414
+ value: Math.max(0, Math.floor(observation.responseBytes))
18415
+ });
18416
+ if (observation.overlapCount > 0) {
18417
+ observability.incrementCounter({
18418
+ name: "opengeni_work_discovery_overlap_results_total",
18419
+ help: "Advisory discovery rows carrying a possible-overlap explanation.",
18420
+ labels: { surface: observation.surface, mode: observation.mode },
18421
+ amount: Math.floor(observation.overlapCount)
18422
+ });
18423
+ }
18424
+ for (const matchClass of ["exact_subject", "title", "goal", "fuzzy"]) {
18425
+ const amount = Math.floor(observation.matchCounts[matchClass] ?? 0);
18426
+ if (amount < 1) continue;
18427
+ observability.incrementCounter({
18428
+ name: "opengeni_work_discovery_matches_total",
18429
+ help: "Advisory discovery matches by stable explanation class.",
18430
+ labels: {
18431
+ surface: observation.surface,
18432
+ mode: observation.mode,
18433
+ match_class: matchClass
18434
+ },
18435
+ amount
18436
+ });
18437
+ }
18438
+ } catch {
18439
+ try {
18440
+ observability.incrementCounter({
18441
+ name: "opengeni_observability_observer_errors_total",
18442
+ help: "Observability observer failures isolated from product execution.",
18443
+ labels: { observer: "work_discovery" }
18444
+ });
18445
+ } catch {
18446
+ }
18447
+ }
18448
+ }
18449
+
18357
18450
  // src/mcp/server.ts
18358
18451
  var ORCHESTRATION_FAILURE_CODE_MAX_LENGTH = 128;
18359
18452
  var ORCHESTRATION_FAILURE_MESSAGE_MAX_UTF8_BYTES = 1024;
@@ -18455,6 +18548,8 @@ var FIRST_PARTY_TOOL_AUTHORIZATION = {
18455
18548
  task_note_save: { sessionRequired: true, allOf: ["sessions:control"] },
18456
18549
  task_note_archive: { sessionRequired: true, allOf: ["sessions:control"] },
18457
18550
  task_note_replace: { sessionRequired: true, allOf: ["sessions:control"] },
18551
+ work_claim_upsert: { sessionRequired: true, allOf: ["sessions:control"] },
18552
+ work_claim_release: { sessionRequired: true, allOf: ["sessions:control"] },
18458
18553
  knowledge_propose: { sessionRequired: true, allOf: ["documents:search"] },
18459
18554
  knowledge_correct: { sessionRequired: true, allOf: ["documents:search"] },
18460
18555
  task_note_promote_knowledge: {
@@ -18784,6 +18879,9 @@ function buildOpenGeniMcpServer(deps, grant, options = {}) {
18784
18879
  if (sessionId !== null && exactAgentAttemptClaims(grant) !== null) {
18785
18880
  registerPreferenceRegistryTools(server, deps, grant, json);
18786
18881
  registerTaskNoteTools(server, deps, grant, sessionId, json);
18882
+ if (deps.settings.workClaimMutationsEnabled) {
18883
+ registerWorkClaimTools(server, deps, grant, sessionId, json);
18884
+ }
18787
18885
  const attempt = exactAgentAttemptClaims(grant);
18788
18886
  registerCompanyBrainGovernedWriteTools({
18789
18887
  server,
@@ -20865,6 +20963,118 @@ function registerTaskNoteTools(server, deps, grant, sessionId, json) {
20865
20963
  }
20866
20964
  );
20867
20965
  }
20966
+ function registerWorkClaimTools(server, deps, grant, sessionId, json) {
20967
+ const attemptClaims = () => {
20968
+ const resolved = exactAgentAttemptClaims(grant);
20969
+ if (!resolved || resolved.sessionId !== sessionId) {
20970
+ throw new Error("Exact signed work-claim attempt authority is required.");
20971
+ }
20972
+ return {
20973
+ accountId: grant.accountId,
20974
+ workspaceId: grant.workspaceId,
20975
+ ...resolved
20976
+ };
20977
+ };
20978
+ const authorize = async () => {
20979
+ await authorizeFirstPartySession(deps, grant, sessionId, "session.first_party_mcp.call");
20980
+ };
20981
+ server.registerTool(
20982
+ "work_claim_upsert",
20983
+ {
20984
+ description: "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.",
20985
+ inputSchema: {
20986
+ operationId: z43.string().uuid(),
20987
+ expectedRevision: z43.number().int().min(0),
20988
+ subjectNamespace: z43.string().min(1).max(WORK_CLAIM_NAMESPACE_MAX_BYTES),
20989
+ subjectType: z43.enum([
20990
+ "repository",
20991
+ "branch",
20992
+ "pull_request",
20993
+ "issue",
20994
+ "artifact",
20995
+ "release",
20996
+ "ci_run",
20997
+ "other"
20998
+ ]),
20999
+ canonicalKey: z43.string().min(1).max(WORK_CLAIM_CANONICAL_KEY_MAX_BYTES),
21000
+ displayLabel: z43.string().min(1).max(WORK_CLAIM_DISPLAY_LABEL_MAX_BYTES).optional(),
21001
+ role: z43.enum(["working", "reviewing", "monitoring", "delivering"]),
21002
+ versionKind: z43.enum([
21003
+ "git_commit",
21004
+ "branch_head",
21005
+ "pull_request_head",
21006
+ "artifact_version",
21007
+ "release_version",
21008
+ "ci_run",
21009
+ "other"
21010
+ ]).optional(),
21011
+ versionValue: z43.string().min(1).max(WORK_CLAIM_VERSION_VALUE_MAX_BYTES).optional()
21012
+ }
21013
+ },
21014
+ async ({
21015
+ operationId,
21016
+ expectedRevision,
21017
+ subjectNamespace,
21018
+ subjectType,
21019
+ canonicalKey,
21020
+ displayLabel,
21021
+ role,
21022
+ versionKind,
21023
+ versionValue
21024
+ }) => {
21025
+ await authorize();
21026
+ if (versionKind === void 0 !== (versionValue === void 0)) {
21027
+ throw new Error("work_claim_upsert versionKind and versionValue must be supplied together");
21028
+ }
21029
+ return json(
21030
+ await upsertWorkClaim(deps.db, {
21031
+ ...attemptClaims(),
21032
+ operationId,
21033
+ expectedRevision,
21034
+ subjectNamespace,
21035
+ subjectType,
21036
+ canonicalKey,
21037
+ ...displayLabel === void 0 ? {} : { displayLabel },
21038
+ role,
21039
+ ...versionKind && versionValue ? { version: { kind: versionKind, value: versionValue } } : {}
21040
+ })
21041
+ );
21042
+ }
21043
+ );
21044
+ server.registerTool(
21045
+ "work_claim_release",
21046
+ {
21047
+ description: "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.",
21048
+ inputSchema: {
21049
+ operationId: z43.string().uuid(),
21050
+ claimId: z43.string().uuid(),
21051
+ expectedRevision: z43.number().int().min(1),
21052
+ reason: z43.enum([
21053
+ "completed",
21054
+ "cancelled",
21055
+ "failed",
21056
+ "superseded",
21057
+ "no_longer_active",
21058
+ "corrected",
21059
+ "external_state_changed",
21060
+ "other"
21061
+ ])
21062
+ }
21063
+ },
21064
+ async ({ operationId, claimId, expectedRevision, reason: reason2 }) => {
21065
+ await authorize();
21066
+ return json(
21067
+ await releaseWorkClaim(deps.db, {
21068
+ ...attemptClaims(),
21069
+ operationId,
21070
+ claimId,
21071
+ expectedRevision,
21072
+ reason: reason2
21073
+ })
21074
+ );
21075
+ }
21076
+ );
21077
+ }
20868
21078
  function registerPreferenceRegistryTools(server, deps, grant, json) {
20869
21079
  const attemptClaims = () => {
20870
21080
  const resolved = exactAgentAttemptClaims(grant);
@@ -21451,42 +21661,132 @@ function registerWorkspaceOrchestrationTools(server, deps, grant, can, callerSes
21451
21661
  server.registerTool(
21452
21662
  "sessions_list",
21453
21663
  {
21454
- description: `List compact high-level session status in this workspace. Defaults to creation order; use orderBy=updatedAt with decimal activity-revision updatedAfter/updatedThrough tokens for gap-free indexed incremental monitoring independent of application clocks. Cursors are opaque revision-fenced keysets. includeLastMessage is opt-in and never previews a human/API prompt whose turn was never claimed (still queued, or deleted/edited/cancelled before any claim): waiting work is represented by queuedPromptCount until the turn is claimed. Rendered previews share a deterministic ${SESSION_DISCOVERY_PREVIEW_MAX_BYTES}-byte UTF-8 aggregate budget, and omitted previews include a bounded session_events drill-down input (exact message type, direction=before, limit=1, monitoring summary). Use session_get for exact known targets and detailed resources/tools/settings. The list never returns full session objects or history.`,
21664
+ 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.`,
21455
21665
  inputSchema: {
21456
21666
  limit: z43.number().int().positive().max(100).optional(),
21457
21667
  cursor: z43.string().max(512).optional(),
21458
21668
  includeLastMessage: z43.boolean().optional(),
21459
- orderBy: z43.enum(["createdAt", "updatedAt"]).optional(),
21460
- updatedAfter: z43.string().max(64).optional()
21669
+ orderBy: z43.enum(["createdAt", "updatedAt", "relevance"]).optional(),
21670
+ updatedAfter: z43.string().max(64).optional(),
21671
+ query: z43.string().max(WORK_DISCOVERY_QUERY_MAX_CHARS).optional(),
21672
+ statuses: z43.array(
21673
+ z43.enum([
21674
+ "queued",
21675
+ "running",
21676
+ "idle",
21677
+ "requires_action",
21678
+ "recovering",
21679
+ "waiting_capacity",
21680
+ "failed",
21681
+ "cancelled"
21682
+ ])
21683
+ ).max(8).optional(),
21684
+ activeOnly: z43.boolean().optional(),
21685
+ recentHours: z43.number().int().positive().max(WORK_DISCOVERY_RECENT_HOURS_MAX).optional(),
21686
+ rootSessionId: z43.string().uuid().optional(),
21687
+ parentSessionId: z43.string().uuid().nullable().optional(),
21688
+ subject: z43.object({
21689
+ namespace: z43.string().min(1).max(WORK_CLAIM_NAMESPACE_MAX_BYTES),
21690
+ type: WorkClaimSubjectType,
21691
+ canonicalKey: z43.string().min(1).max(WORK_CLAIM_CANONICAL_KEY_MAX_BYTES)
21692
+ }).strict().optional(),
21693
+ claimLimit: z43.number().int().positive().max(WORK_CLAIM_DISCOVERY_LIMIT).optional()
21461
21694
  }
21462
21695
  },
21463
- async ({ limit, cursor, includeLastMessage, orderBy: requestedOrderBy, updatedAfter }) => {
21696
+ async ({
21697
+ limit,
21698
+ cursor,
21699
+ includeLastMessage,
21700
+ orderBy: requestedOrderBy,
21701
+ updatedAfter,
21702
+ query,
21703
+ statuses,
21704
+ activeOnly,
21705
+ recentHours,
21706
+ rootSessionId,
21707
+ parentSessionId,
21708
+ subject,
21709
+ claimLimit
21710
+ }) => {
21464
21711
  const authorizationScope = await requireSessionAuthorizationListScope(
21465
21712
  deps,
21466
21713
  grant,
21467
21714
  "first_party_mcp"
21468
21715
  );
21716
+ const startedAtMs = performance.now();
21717
+ const mode = subject ? "subject" : query?.trim() ? "query" : "browse";
21718
+ const metricAuthorizationScope = authorizationScope?.kind === "scoped" ? "scoped" : "workspace";
21469
21719
  const decodedCursor = cursor ? decodeSessionDiscoveryCursor(cursor) : void 0;
21470
- const orderBy = requestedOrderBy ?? decodedCursor?.orderBy ?? "createdAt";
21471
- if (decodedCursor && decodedCursor.orderBy !== orderBy) {
21472
- throw new Error("sessions_list cursor order does not match orderBy");
21473
- }
21474
- const normalizedUpdatedAfter = updatedAfter !== void 0 ? normalizeSessionDiscoveryRevision(updatedAfter, "updatedAfter") : decodedCursor?.updatedAfter ?? void 0;
21475
- if (normalizedUpdatedAfter !== void 0 && orderBy !== "updatedAt") {
21476
- throw new Error("sessions_list updatedAfter requires orderBy=updatedAt");
21720
+ const relevanceRequested = Boolean(query?.trim() || subject);
21721
+ if (relevanceRequested && !deps.settings.workDiscoveryEnabled) {
21722
+ observeWorkDiscovery(deps.observability, {
21723
+ surface: "first_party_mcp",
21724
+ mode,
21725
+ outcome: "disabled",
21726
+ authorizationScope: metricAuthorizationScope,
21727
+ durationMs: performance.now() - startedAtMs,
21728
+ responseBytes: 0,
21729
+ resultCount: 0,
21730
+ overlapCount: 0,
21731
+ matchCounts: {}
21732
+ });
21733
+ throw new Error("sessions_list work discovery is disabled by the operator");
21477
21734
  }
21478
- if (decodedCursor && decodedCursor.updatedAfter !== (normalizedUpdatedAfter ?? null)) {
21479
- throw new Error("sessions_list cursor does not match updatedAfter");
21735
+ try {
21736
+ const orderBy = requestedOrderBy ?? decodedCursor?.orderBy ?? (relevanceRequested ? "relevance" : "createdAt");
21737
+ if (decodedCursor && decodedCursor.orderBy !== orderBy) {
21738
+ throw new Error("sessions_list cursor order does not match orderBy");
21739
+ }
21740
+ const normalizedUpdatedAfter = updatedAfter !== void 0 ? normalizeSessionDiscoveryRevision(updatedAfter, "updatedAfter") : decodedCursor?.updatedAfter ?? void 0;
21741
+ if (normalizedUpdatedAfter !== void 0 && orderBy !== "updatedAt") {
21742
+ throw new Error("sessions_list updatedAfter requires orderBy=updatedAt");
21743
+ }
21744
+ if (decodedCursor && decodedCursor.updatedAfter !== (normalizedUpdatedAfter ?? null)) {
21745
+ throw new Error("sessions_list cursor does not match updatedAfter");
21746
+ }
21747
+ const page = await listSessionDiscoverySummaries(deps.db, grant.workspaceId, {
21748
+ limit: boundedSessionDiscoveryLimit(limit),
21749
+ ...decodedCursor ? { cursor: decodedCursor } : {},
21750
+ includeLastMessage: includeLastMessage === true,
21751
+ orderBy,
21752
+ ...normalizedUpdatedAfter ? { updatedAfter: normalizedUpdatedAfter } : {},
21753
+ ...query?.trim() ? { query } : {},
21754
+ ...statuses ? { statuses } : {},
21755
+ activeOnly: activeOnly === true,
21756
+ ...recentHours !== void 0 ? { recentHours } : {},
21757
+ ...rootSessionId ? { rootSessionId } : {},
21758
+ ...parentSessionId !== void 0 ? { parentSessionId } : {},
21759
+ ...subject ? { subject } : {},
21760
+ ...claimLimit !== void 0 ? { claimLimit } : {},
21761
+ includeWorkDiscovery: deps.settings.workDiscoveryEnabled,
21762
+ subjectId: grant.subjectId,
21763
+ ...authorizationScope ? { authorizationScope } : {}
21764
+ });
21765
+ const result = capSessionDiscoveryPage(page, includeLastMessage === true);
21766
+ observeWorkDiscovery(deps.observability, {
21767
+ surface: "first_party_mcp",
21768
+ mode,
21769
+ outcome: result.sessions.length === 0 ? "empty" : "ok",
21770
+ authorizationScope: metricAuthorizationScope,
21771
+ durationMs: performance.now() - startedAtMs,
21772
+ responseBytes: result.bytes,
21773
+ ...summarizeWorkDiscoveryRows(result.sessions)
21774
+ });
21775
+ return json(result);
21776
+ } catch (error) {
21777
+ observeWorkDiscovery(deps.observability, {
21778
+ surface: "first_party_mcp",
21779
+ mode,
21780
+ outcome: "error",
21781
+ authorizationScope: metricAuthorizationScope,
21782
+ durationMs: performance.now() - startedAtMs,
21783
+ responseBytes: 0,
21784
+ resultCount: 0,
21785
+ overlapCount: 0,
21786
+ matchCounts: {}
21787
+ });
21788
+ throw error;
21480
21789
  }
21481
- const page = await listSessionDiscoverySummaries(deps.db, grant.workspaceId, {
21482
- limit: boundedSessionDiscoveryLimit(limit),
21483
- ...decodedCursor ? { cursor: decodedCursor } : {},
21484
- includeLastMessage: includeLastMessage === true,
21485
- orderBy,
21486
- ...normalizedUpdatedAfter ? { updatedAfter: normalizedUpdatedAfter } : {},
21487
- ...authorizationScope ? { authorizationScope } : {}
21488
- });
21489
- return json(capSessionDiscoveryPage(page, includeLastMessage === true));
21490
21790
  }
21491
21791
  );
21492
21792
  server.registerTool(
@@ -22699,10 +22999,12 @@ function boundedSessionDiscoveryLimit(limit) {
22699
22999
  return Math.min(SESSION_DISCOVERY_MAX_LIMIT, Math.max(1, Math.floor(limit)));
22700
23000
  }
22701
23001
  function encodeSessionDiscoveryCursor(cursor) {
23002
+ const relevance = cursor.orderBy === "relevance";
22702
23003
  return Buffer.from(
22703
23004
  JSON.stringify({
22704
- v: 2,
23005
+ v: relevance ? 3 : 2,
22705
23006
  orderBy: cursor.orderBy,
23007
+ ...relevance ? { sortRank: cursor.sortRank, filterHash: cursor.filterHash } : {},
22706
23008
  sortRevision: cursor.sortRevision,
22707
23009
  sortAt: cursor.sortAt,
22708
23010
  id: cursor.id,
@@ -22743,28 +23045,40 @@ function decodeSessionDiscoveryCursor(value) {
22743
23045
  );
22744
23046
  return {
22745
23047
  orderBy: "createdAt",
23048
+ sortRank: null,
22746
23049
  sortRevision: "0",
22747
23050
  sortAt: createdAt,
22748
23051
  id: parsed.id,
22749
23052
  snapshotAt: createdAt,
22750
23053
  snapshotRevision: "0",
22751
- updatedAfter: null
23054
+ updatedAfter: null,
23055
+ filterHash: null
22752
23056
  };
22753
23057
  }
22754
23058
  if (parsed.v === 1 && parsed.orderBy === "createdAt" && typeof parsed.sortAt === "string" && typeof parsed.snapshotAt === "string" && parsed.updatedAfter === null && typeof parsed.id === "string" && SESSION_DISCOVERY_UUID.test(parsed.id)) {
22755
23059
  return {
22756
23060
  orderBy: "createdAt",
23061
+ sortRank: null,
22757
23062
  sortRevision: "0",
22758
23063
  sortAt: normalizeSessionDiscoveryTimestamp(parsed.sortAt, "cursor sortAt"),
22759
23064
  id: parsed.id,
22760
23065
  snapshotAt: normalizeSessionDiscoveryTimestamp(parsed.snapshotAt, "cursor snapshotAt"),
22761
23066
  snapshotRevision: "0",
22762
- updatedAfter: null
23067
+ updatedAfter: null,
23068
+ filterHash: null
22763
23069
  };
22764
23070
  }
22765
- if (parsed.v !== 2 || parsed.orderBy !== "createdAt" && parsed.orderBy !== "updatedAt" || typeof parsed.sortRevision !== "string" || typeof parsed.sortAt !== "string" || typeof parsed.snapshotAt !== "string" || typeof parsed.snapshotRevision !== "string" || parsed.updatedAfter !== null && typeof parsed.updatedAfter !== "string" || typeof parsed.id !== "string" || !SESSION_DISCOVERY_UUID.test(parsed.id)) {
23071
+ const isV2 = parsed.v === 2;
23072
+ const isV3 = parsed.v === 3;
23073
+ if (!isV2 && !isV3 || isV2 && parsed.orderBy !== "createdAt" && parsed.orderBy !== "updatedAt" || isV3 && parsed.orderBy !== "relevance" || typeof parsed.sortRevision !== "string" || typeof parsed.sortAt !== "string" || typeof parsed.snapshotAt !== "string" || typeof parsed.snapshotRevision !== "string" || parsed.updatedAfter !== null && typeof parsed.updatedAfter !== "string" || typeof parsed.id !== "string" || !SESSION_DISCOVERY_UUID.test(parsed.id)) {
22766
23074
  throw new Error("invalid cursor fields");
22767
23075
  }
23076
+ if (isV3 && (!Number.isSafeInteger(parsed.sortRank) || parsed.sortRank < 0 || typeof parsed.filterHash !== "string" || !/^[0-9a-f]{64}$/.test(parsed.filterHash))) {
23077
+ throw new Error("invalid relevance cursor fields");
23078
+ }
23079
+ if (isV2 && (parsed.sortRank !== void 0 || parsed.filterHash !== void 0)) {
23080
+ throw new Error("chronological cursor cannot carry relevance fields");
23081
+ }
22768
23082
  const sortAt = normalizeSessionDiscoveryTimestamp(parsed.sortAt, "cursor sortAt");
22769
23083
  const snapshotAt = normalizeSessionDiscoveryTimestamp(parsed.snapshotAt, "cursor snapshotAt");
22770
23084
  const sortRevision = normalizeSessionDiscoveryRevision(
@@ -22782,14 +23096,17 @@ function decodeSessionDiscoveryCursor(value) {
22782
23096
  if (parsed.orderBy === "createdAt" && (sortRevision !== "0" || snapshotRevision !== "0")) {
22783
23097
  throw new Error("creation cursor cannot carry activity revisions");
22784
23098
  }
23099
+ const orderBy = isV3 ? "relevance" : parsed.orderBy;
22785
23100
  return {
22786
- orderBy: parsed.orderBy,
23101
+ orderBy,
23102
+ sortRank: isV3 ? parsed.sortRank : null,
22787
23103
  sortRevision,
22788
23104
  sortAt,
22789
23105
  id: parsed.id,
22790
23106
  snapshotAt,
22791
23107
  snapshotRevision,
22792
- updatedAfter: normalizedUpdatedAfter
23108
+ updatedAfter: normalizedUpdatedAfter,
23109
+ filterHash: isV3 ? parsed.filterHash : null
22793
23110
  };
22794
23111
  } catch {
22795
23112
  throw new Error("sessions_list cursor is invalid");
@@ -22857,6 +23174,7 @@ function capSessionDiscoveryPage(page, includeLastMessage) {
22857
23174
  } : null,
22858
23175
  queuedPromptCount: session.queuedPromptCount,
22859
23176
  children: session.treeStats,
23177
+ relatedWork: session.workDiscovery,
22860
23178
  ...includeLastMessage ? {
22861
23179
  latestMessage: session.latestMessage ? {
22862
23180
  type: session.latestMessage.type,
@@ -22907,12 +23225,14 @@ function capSessionDiscoveryPage(page, includeLastMessage) {
22907
23225
  const sourceLast = lastKept ? page.sessions.find((session) => session.id === lastKept.id) : void 0;
22908
23226
  const nextCursor = droppedForByteCap ? sourceLast ? encodeSessionDiscoveryCursor({
22909
23227
  orderBy: page.orderBy,
23228
+ sortRank: sourceLast.sortRank,
22910
23229
  sortRevision: sourceLast.sortRevision,
22911
23230
  sortAt: sourceLast.sortAt,
22912
23231
  id: sourceLast.id,
22913
23232
  snapshotAt: page.snapshotAt,
22914
23233
  snapshotRevision: page.snapshotRevision,
22915
- updatedAfter: page.updatedAfter
23234
+ updatedAfter: page.updatedAfter,
23235
+ filterHash: page.filterHash
22916
23236
  }) : null : page.nextCursor ? encodeSessionDiscoveryCursor(page.nextCursor) : null;
22917
23237
  const result2 = {
22918
23238
  sessions: kept,
@@ -46725,6 +47045,15 @@ import {
46725
47045
  UpdateSessionToolPolicyRequest,
46726
47046
  ViewerHeartbeatRequest,
46727
47047
  WORKSPACE_CONTROL_ACTOR_MAX_BYTES,
47048
+ WORK_CLAIM_CANONICAL_KEY_MAX_BYTES as WORK_CLAIM_CANONICAL_KEY_MAX_BYTES2,
47049
+ WORK_CLAIM_DISCOVERY_LIMIT as WORK_CLAIM_DISCOVERY_LIMIT2,
47050
+ WORK_CLAIM_NAMESPACE_MAX_BYTES as WORK_CLAIM_NAMESPACE_MAX_BYTES2,
47051
+ WORK_DISCOVERY_QUERY_MAX_CHARS as WORK_DISCOVERY_QUERY_MAX_CHARS2,
47052
+ WORK_DISCOVERY_RECENT_HOURS_MAX as WORK_DISCOVERY_RECENT_HOURS_MAX2,
47053
+ WorkClaimSubjectFilter as WorkClaimSubjectFilterSchema,
47054
+ WorkClaimSubjectType as WorkClaimSubjectType2,
47055
+ normalizeWorkClaimCanonicalKey,
47056
+ normalizeWorkClaimNamespace,
46728
47057
  workspaceControlUtf8Bytes
46729
47058
  } from "@opengeni/contracts";
46730
47059
  import { streamTokenDegraded } from "@opengeni/config";
@@ -48178,60 +48507,127 @@ function registerSessionRoutes(app, deps) {
48178
48507
  throw sessionAuthorizationHttpError(error);
48179
48508
  }
48180
48509
  const query = agentTopologyQuery(c.req.query());
48181
- const page = await listSessionDiscoverySummaries2(db, workspaceId, {
48182
- limit: query.limit,
48183
- orderBy: "updatedAt",
48184
- subjectId: grant.subjectId,
48185
- ...query.cursor ? { cursor: query.cursor } : {},
48186
- ...query.search ? { search: query.search } : { parentSessionId: query.parentSessionId },
48187
- ...authorizationScope ? { authorizationScope } : {}
48188
- });
48189
- const ancestorPaths = query.search ? await listSessionDiscoveryAncestorPaths(
48190
- db,
48191
- workspaceId,
48192
- page.sessions.map((session) => session.id),
48193
- authorizationScope ?? void 0
48194
- ) : /* @__PURE__ */ new Map();
48195
- const sessions = page.sessions.map((session) => {
48196
- const blocker = session.effectiveControl.primaryBlocker;
48197
- return {
48198
- id: session.id,
48199
- title: session.title,
48200
- titleTruncated: session.titleOriginalChars !== null && session.titleOriginalChars > Array.from(session.title ?? "").length,
48201
- parentSessionId: session.parentSessionId,
48202
- rootSessionId: session.rootSessionId,
48203
- nestedAgentDepth: session.nestedAgentDepth,
48204
- ancestorPath: (ancestorPaths.get(session.id) ?? []).map((ancestor) => ({
48205
- id: ancestor.id,
48206
- title: ancestor.title,
48207
- titleTruncated: ancestor.titleOriginalChars !== null && ancestor.titleOriginalChars > Array.from(ancestor.title ?? "").length
48208
- })),
48209
- status: session.status,
48210
- pause: {
48211
- state: session.effectiveControl.state,
48212
- additionalBlockerCount: session.effectiveControl.additionalBlockerCount,
48213
- source: blocker ? {
48214
- kind: blocker.kind,
48215
- ...blocker.sessionId ? { sessionId: blocker.sessionId } : {},
48216
- displayName: blocker.displayName,
48217
- displayNameTruncated: blocker.displayNameOriginalChars > Array.from(blocker.displayName).length
48218
- } : null
48219
- },
48220
- children: session.treeStats,
48221
- createdAt: session.createdAt,
48222
- updatedAt: session.updatedAt
48510
+ const startedAtMs = performance.now();
48511
+ const mode = query.subject ? "subject" : query.query ? "query" : "browse";
48512
+ const metricAuthorizationScope = authorizationScope?.kind === "scoped" ? "scoped" : "workspace";
48513
+ if ((query.query || query.subject) && !settings.workDiscoveryEnabled) {
48514
+ observeWorkDiscovery(deps.observability, {
48515
+ surface: "agent_topology",
48516
+ mode,
48517
+ outcome: "disabled",
48518
+ authorizationScope: metricAuthorizationScope,
48519
+ durationMs: performance.now() - startedAtMs,
48520
+ responseBytes: 0,
48521
+ resultCount: 0,
48522
+ overlapCount: 0,
48523
+ matchCounts: {}
48524
+ });
48525
+ throw new HTTPException53(503, {
48526
+ message: "Agent work discovery is disabled by the operator."
48527
+ });
48528
+ }
48529
+ try {
48530
+ const orderBy = query.query || query.subject ? "relevance" : "updatedAt";
48531
+ const page = await listSessionDiscoverySummaries2(db, workspaceId, {
48532
+ limit: query.limit,
48533
+ orderBy,
48534
+ subjectId: grant.subjectId,
48535
+ ...query.cursor ? { cursor: query.cursor } : {},
48536
+ ...query.parentSessionId !== void 0 ? { parentSessionId: query.parentSessionId } : {},
48537
+ ...query.rootSessionId ? { rootSessionId: query.rootSessionId } : {},
48538
+ ...query.query ? { query: query.query } : {},
48539
+ ...query.statuses ? { statuses: query.statuses } : {},
48540
+ activeOnly: query.activeOnly,
48541
+ ...query.recentHours !== void 0 ? { recentHours: query.recentHours } : {},
48542
+ ...query.subject ? { subject: query.subject } : {},
48543
+ ...query.claimLimit !== void 0 ? { claimLimit: query.claimLimit } : {},
48544
+ includeWorkDiscovery: settings.workDiscoveryEnabled,
48545
+ ...authorizationScope ? { authorizationScope } : {}
48546
+ });
48547
+ const ancestorPaths = query.query || query.subject ? await listSessionDiscoveryAncestorPaths(
48548
+ db,
48549
+ workspaceId,
48550
+ page.sessions.map((session) => session.id),
48551
+ authorizationScope ?? void 0,
48552
+ grant.subjectId
48553
+ ) : /* @__PURE__ */ new Map();
48554
+ const sessions = page.sessions.map((session) => {
48555
+ const blocker = session.effectiveControl.primaryBlocker;
48556
+ return {
48557
+ id: session.id,
48558
+ title: session.title,
48559
+ titleTruncated: session.titleOriginalChars !== null && session.titleOriginalChars > Array.from(session.title ?? "").length,
48560
+ parentSessionId: session.parentSessionId,
48561
+ rootSessionId: session.rootSessionId,
48562
+ nestedAgentDepth: session.nestedAgentDepth,
48563
+ ancestorPath: (ancestorPaths.get(session.id) ?? []).map((ancestor) => ({
48564
+ id: ancestor.id,
48565
+ title: ancestor.title,
48566
+ titleTruncated: ancestor.titleOriginalChars !== null && ancestor.titleOriginalChars > Array.from(ancestor.title ?? "").length
48567
+ })),
48568
+ status: session.status,
48569
+ goal: session.goal ? {
48570
+ status: session.goal.status,
48571
+ summary: session.goal.text,
48572
+ summaryTruncated: session.goal.textOriginalChars > Array.from(session.goal.text).length
48573
+ } : null,
48574
+ pause: {
48575
+ state: session.effectiveControl.state,
48576
+ additionalBlockerCount: session.effectiveControl.additionalBlockerCount,
48577
+ source: blocker ? {
48578
+ kind: blocker.kind,
48579
+ ...blocker.sessionId ? { sessionId: blocker.sessionId } : {},
48580
+ displayName: blocker.displayName,
48581
+ displayNameTruncated: blocker.displayNameOriginalChars > Array.from(blocker.displayName).length
48582
+ } : null
48583
+ },
48584
+ children: session.treeStats,
48585
+ relatedWork: session.workDiscovery,
48586
+ createdAt: session.createdAt,
48587
+ updatedAt: session.updatedAt
48588
+ };
48589
+ });
48590
+ const response = {
48591
+ sessions,
48592
+ total: page.total,
48593
+ hasMore: page.hasMore,
48594
+ humanAdvisoriesEnabled: settings.workDiscoveryEnabled && settings.workDiscoveryHumanAdvisoriesEnabled,
48595
+ nextCursor: page.nextCursor ? encodeAgentTopologyCursor({
48596
+ cursor: page.nextCursor,
48597
+ parentSessionId: query.parentSessionId === void 0 ? "all" : query.parentSessionId,
48598
+ rootSessionId: query.rootSessionId ?? null,
48599
+ query: query.query ?? null,
48600
+ statuses: query.statuses ?? [],
48601
+ activeOnly: query.activeOnly,
48602
+ recentHours: query.recentHours ?? null,
48603
+ subject: query.subject ?? null,
48604
+ claimLimit: query.claimLimit ?? null
48605
+ }) : null
48223
48606
  };
48224
- });
48225
- return c.json({
48226
- sessions,
48227
- total: page.total,
48228
- hasMore: page.hasMore,
48229
- nextCursor: page.nextCursor ? encodeAgentTopologyCursor({
48230
- cursor: page.nextCursor,
48231
- parentSessionId: query.parentSessionId,
48232
- search: query.search ?? null
48233
- }) : null
48234
- });
48607
+ observeWorkDiscovery(deps.observability, {
48608
+ surface: "agent_topology",
48609
+ mode,
48610
+ outcome: sessions.length === 0 ? "empty" : "ok",
48611
+ authorizationScope: metricAuthorizationScope,
48612
+ durationMs: performance.now() - startedAtMs,
48613
+ responseBytes: Buffer.byteLength(JSON.stringify(response), "utf8"),
48614
+ ...summarizeWorkDiscoveryRows(sessions)
48615
+ });
48616
+ return c.json(response);
48617
+ } catch (error) {
48618
+ observeWorkDiscovery(deps.observability, {
48619
+ surface: "agent_topology",
48620
+ mode,
48621
+ outcome: "error",
48622
+ authorizationScope: metricAuthorizationScope,
48623
+ durationMs: performance.now() - startedAtMs,
48624
+ responseBytes: 0,
48625
+ resultCount: 0,
48626
+ overlapCount: 0,
48627
+ matchCounts: {}
48628
+ });
48629
+ throw error;
48630
+ }
48235
48631
  });
48236
48632
  app.get("/v1/workspaces/:workspaceId/sessions/:sessionId", async (c) => {
48237
48633
  const workspaceId = c.req.param("workspaceId");
@@ -51498,7 +51894,7 @@ function sessionListQuery(query, allowCursor = true) {
51498
51894
  };
51499
51895
  }
51500
51896
  function encodeAgentTopologyCursor(value) {
51501
- return Buffer.from(JSON.stringify({ v: 1, ...value }), "utf8").toString("base64url");
51897
+ return Buffer.from(JSON.stringify({ v: 2, ...value }), "utf8").toString("base64url");
51502
51898
  }
51503
51899
  function decodeAgentTopologyCursor(value) {
51504
51900
  if (value.length > 2048) {
@@ -51507,23 +51903,85 @@ function decodeAgentTopologyCursor(value) {
51507
51903
  });
51508
51904
  }
51509
51905
  try {
51510
- const parsed = z13.object({
51906
+ const decoded2 = JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
51907
+ const legacy = z13.object({
51511
51908
  v: z13.literal(1),
51512
51909
  parentSessionId: z13.string().uuid().nullable(),
51513
- search: z13.string().max(200).nullable(),
51910
+ search: z13.string().max(WORK_DISCOVERY_QUERY_MAX_CHARS2).nullable(),
51514
51911
  cursor: z13.object({
51515
- orderBy: z13.enum(["createdAt", "updatedAt"]),
51912
+ orderBy: z13.literal("updatedAt"),
51516
51913
  sortRevision: z13.string().max(64),
51517
51914
  sortAt: z13.string().max(64),
51518
51915
  id: z13.string().uuid(),
51519
51916
  snapshotAt: z13.string().max(64),
51520
51917
  snapshotRevision: z13.string().max(64),
51521
- updatedAfter: z13.string().max(64).nullable()
51918
+ updatedAfter: z13.null()
51522
51919
  })
51523
- }).parse(JSON.parse(Buffer.from(value, "base64url").toString("utf8")));
51524
- if (parsed.cursor.orderBy !== "updatedAt" || parsed.cursor.updatedAfter !== null || !/^(?:0|[1-9]\d*)$/.test(parsed.cursor.sortRevision) || !/^(?:0|[1-9]\d*)$/.test(parsed.cursor.snapshotRevision) || BigInt(parsed.cursor.sortRevision) > 9223372036854775807n || BigInt(parsed.cursor.snapshotRevision) > 9223372036854775807n || Number.isNaN(Date.parse(parsed.cursor.sortAt)) || Number.isNaN(Date.parse(parsed.cursor.snapshotAt))) {
51920
+ }).safeParse(decoded2);
51921
+ if (legacy.success) {
51922
+ if (legacy.data.search !== null) {
51923
+ throw new Error("legacy search cursor is not relevance-fenced");
51924
+ }
51925
+ const cursor = {
51926
+ ...legacy.data.cursor,
51927
+ sortRank: null,
51928
+ filterHash: null
51929
+ };
51930
+ return {
51931
+ cursor,
51932
+ parentSessionId: legacy.data.parentSessionId,
51933
+ rootSessionId: null,
51934
+ query: null,
51935
+ statuses: [],
51936
+ activeOnly: false,
51937
+ recentHours: null,
51938
+ subject: null,
51939
+ claimLimit: null
51940
+ };
51941
+ }
51942
+ const parsed = z13.object({
51943
+ v: z13.literal(2),
51944
+ parentSessionId: z13.union([z13.string().uuid(), z13.literal("all"), z13.null()]),
51945
+ rootSessionId: z13.string().uuid().nullable(),
51946
+ query: z13.string().max(WORK_DISCOVERY_QUERY_MAX_CHARS2).nullable(),
51947
+ statuses: z13.array(
51948
+ z13.enum([
51949
+ "queued",
51950
+ "running",
51951
+ "idle",
51952
+ "requires_action",
51953
+ "recovering",
51954
+ "waiting_capacity",
51955
+ "failed",
51956
+ "cancelled"
51957
+ ])
51958
+ ).max(8),
51959
+ activeOnly: z13.boolean(),
51960
+ recentHours: z13.number().int().positive().max(WORK_DISCOVERY_RECENT_HOURS_MAX2).nullable(),
51961
+ subject: z13.object({
51962
+ namespace: z13.string().min(1).max(WORK_CLAIM_NAMESPACE_MAX_BYTES2),
51963
+ type: WorkClaimSubjectType2,
51964
+ canonicalKey: z13.string().min(1).max(WORK_CLAIM_CANONICAL_KEY_MAX_BYTES2)
51965
+ }).strict().nullable(),
51966
+ claimLimit: z13.number().int().positive().max(WORK_CLAIM_DISCOVERY_LIMIT2).nullable(),
51967
+ cursor: z13.object({
51968
+ orderBy: z13.enum(["updatedAt", "relevance"]),
51969
+ sortRank: z13.number().int().nonnegative().nullable(),
51970
+ sortRevision: z13.string().max(64),
51971
+ sortAt: z13.string().max(64),
51972
+ id: z13.string().uuid(),
51973
+ snapshotAt: z13.string().max(64),
51974
+ snapshotRevision: z13.string().max(64),
51975
+ updatedAfter: z13.null(),
51976
+ filterHash: z13.string().regex(/^[0-9a-f]{64}$/).nullable()
51977
+ })
51978
+ }).parse(decoded2);
51979
+ if (!/^(?:0|[1-9]\d*)$/.test(parsed.cursor.sortRevision) || !/^(?:0|[1-9]\d*)$/.test(parsed.cursor.snapshotRevision) || BigInt(parsed.cursor.sortRevision) > 9223372036854775807n || BigInt(parsed.cursor.snapshotRevision) > 9223372036854775807n || Number.isNaN(Date.parse(parsed.cursor.sortAt)) || Number.isNaN(Date.parse(parsed.cursor.snapshotAt))) {
51525
51980
  throw new Error("invalid topology cursor fields");
51526
51981
  }
51982
+ if (parsed.cursor.orderBy === "relevance" && (parsed.cursor.sortRank === null || parsed.cursor.filterHash === null) || parsed.cursor.orderBy === "updatedAt" && (parsed.cursor.sortRank !== null || parsed.cursor.filterHash !== null)) {
51983
+ throw new Error("invalid topology cursor relevance fields");
51984
+ }
51527
51985
  return parsed;
51528
51986
  } catch {
51529
51987
  throw new HTTPException53(400, {
@@ -51545,20 +52003,108 @@ function agentTopologyQuery(query) {
51545
52003
  message: 'parentSessionId must be a session id or the literal "null"'
51546
52004
  });
51547
52005
  }
51548
- const parentSessionId = rawParent === void 0 || rawParent === "null" ? null : rawParent;
51549
- const search = query.search?.trim();
51550
- if (search && search.length > 200) {
52006
+ const rootSessionId = query.rootSessionId?.trim();
52007
+ if (rootSessionId && !z13.string().uuid().safeParse(rootSessionId).success) {
52008
+ throw new HTTPException53(400, { message: "rootSessionId must be a session id" });
52009
+ }
52010
+ const normalizeSearchQuery = (value) => {
52011
+ if (value === void 0) return void 0;
52012
+ const canonical = value.normalize("NFKC");
52013
+ if (/[\u0000-\u001f\u007f-\u009f]/u.test(canonical)) {
52014
+ throw new HTTPException53(400, { message: "query must not contain control characters" });
52015
+ }
52016
+ const normalized = canonical.trim().replace(/\s+/gu, " ").toLowerCase();
52017
+ if (!normalized) return void 0;
52018
+ if (Array.from(normalized).length > WORK_DISCOVERY_QUERY_MAX_CHARS2) {
52019
+ throw new HTTPException53(400, {
52020
+ message: `query must be at most ${WORK_DISCOVERY_QUERY_MAX_CHARS2} characters`
52021
+ });
52022
+ }
52023
+ return normalized;
52024
+ };
52025
+ const requestedQuery = normalizeSearchQuery(query.query);
52026
+ const legacySearch = normalizeSearchQuery(query.search);
52027
+ const searchQuery = requestedQuery ?? legacySearch;
52028
+ if (requestedQuery && legacySearch && requestedQuery !== legacySearch) {
52029
+ throw new HTTPException53(400, { message: "query and legacy search must match" });
52030
+ }
52031
+ const statuses = query.statuses ? [
52032
+ ...new Set(
52033
+ query.statuses.split(",").map((status) => status.trim()).filter(Boolean)
52034
+ )
52035
+ ].sort() : [];
52036
+ const parsedStatuses = z13.array(
52037
+ z13.enum([
52038
+ "queued",
52039
+ "running",
52040
+ "idle",
52041
+ "requires_action",
52042
+ "recovering",
52043
+ "waiting_capacity",
52044
+ "failed",
52045
+ "cancelled"
52046
+ ])
52047
+ ).max(8).safeParse(statuses);
52048
+ if (!parsedStatuses.success) {
52049
+ throw new HTTPException53(400, { message: "statuses contains an unsupported lifecycle state" });
52050
+ }
52051
+ const activeOnly = query.activeOnly === "true";
52052
+ if (query.activeOnly !== void 0 && query.activeOnly !== "true" && query.activeOnly !== "false") {
52053
+ throw new HTTPException53(400, { message: "activeOnly must be true or false" });
52054
+ }
52055
+ const recentHours = query.recentHours === void 0 ? void 0 : Number(query.recentHours);
52056
+ if (recentHours !== void 0 && (!Number.isSafeInteger(recentHours) || recentHours < 1 || recentHours > WORK_DISCOVERY_RECENT_HOURS_MAX2)) {
51551
52057
  throw new HTTPException53(400, {
51552
- message: "search must be at most 200 characters"
52058
+ message: `recentHours must be an integer between 1 and ${WORK_DISCOVERY_RECENT_HOURS_MAX2}`
52059
+ });
52060
+ }
52061
+ const subjectFields = [query.subjectNamespace, query.subjectType, query.subjectKey];
52062
+ if (subjectFields.some((value) => value !== void 0) && subjectFields.some((value) => !value)) {
52063
+ throw new HTTPException53(400, {
52064
+ message: "subjectNamespace, subjectType, and subjectKey must be supplied together"
51553
52065
  });
51554
52066
  }
51555
- if (search && rawParent !== void 0) {
52067
+ const parsedSubject = subjectFields.every((value) => value !== void 0) ? WorkClaimSubjectFilterSchema.safeParse({
52068
+ namespace: normalizeWorkClaimNamespace(query.subjectNamespace),
52069
+ type: query.subjectType,
52070
+ canonicalKey: normalizeWorkClaimCanonicalKey(query.subjectKey)
52071
+ }) : null;
52072
+ if (parsedSubject && !parsedSubject.success) {
52073
+ throw new HTTPException53(400, { message: "exact subject filter is invalid" });
52074
+ }
52075
+ const subject = parsedSubject?.success ? parsedSubject.data : void 0;
52076
+ if (searchQuery && subject) {
52077
+ throw new HTTPException53(400, { message: "query cannot be combined with an exact subject" });
52078
+ }
52079
+ const relevanceRequested = Boolean(searchQuery || subject);
52080
+ const parentSessionId = rawParent === void 0 ? relevanceRequested ? void 0 : null : rawParent === "null" ? null : rawParent;
52081
+ const claimLimit = query.claimLimit === void 0 ? void 0 : Number(query.claimLimit);
52082
+ if (claimLimit !== void 0 && (!Number.isSafeInteger(claimLimit) || claimLimit < 1 || claimLimit > WORK_CLAIM_DISCOVERY_LIMIT2)) {
51556
52083
  throw new HTTPException53(400, {
51557
- message: "search cannot be combined with parentSessionId"
52084
+ message: `claimLimit must be an integer between 1 and ${WORK_CLAIM_DISCOVERY_LIMIT2}`
51558
52085
  });
51559
52086
  }
51560
52087
  const envelope = query.cursor ? decodeAgentTopologyCursor(query.cursor) : void 0;
51561
- if (envelope && (envelope.parentSessionId !== parentSessionId || envelope.search !== (search || null))) {
52088
+ const expectedEnvelope = {
52089
+ parentSessionId: parentSessionId === void 0 ? "all" : parentSessionId,
52090
+ rootSessionId: rootSessionId ?? null,
52091
+ query: searchQuery || null,
52092
+ statuses: parsedStatuses.data,
52093
+ activeOnly,
52094
+ recentHours: recentHours ?? null,
52095
+ subject: subject ?? null,
52096
+ claimLimit: claimLimit ?? null
52097
+ };
52098
+ if (envelope && JSON.stringify({
52099
+ parentSessionId: envelope.parentSessionId,
52100
+ rootSessionId: envelope.rootSessionId,
52101
+ query: envelope.query,
52102
+ statuses: envelope.statuses,
52103
+ activeOnly: envelope.activeOnly,
52104
+ recentHours: envelope.recentHours,
52105
+ subject: envelope.subject,
52106
+ claimLimit: envelope.claimLimit
52107
+ }) !== JSON.stringify(expectedEnvelope)) {
51562
52108
  throw new HTTPException53(400, {
51563
52109
  message: "agent topology cursor does not match its filters"
51564
52110
  });
@@ -51566,7 +52112,13 @@ function agentTopologyQuery(query) {
51566
52112
  return {
51567
52113
  limit,
51568
52114
  parentSessionId,
51569
- search: search || void 0,
52115
+ rootSessionId: rootSessionId || void 0,
52116
+ query: searchQuery || void 0,
52117
+ statuses: parsedStatuses.data.length > 0 ? parsedStatuses.data : void 0,
52118
+ activeOnly,
52119
+ recentHours,
52120
+ subject,
52121
+ claimLimit,
51570
52122
  cursor: envelope?.cursor
51571
52123
  };
51572
52124
  }
@@ -65379,4 +65931,4 @@ export {
65379
65931
  withDefaultEnabledCapabilityMcpTools,
65380
65932
  workflowIdForSession3 as workflowIdForSession
65381
65933
  };
65382
- //# sourceMappingURL=chunk-L7GVVSSQ.js.map
65934
+ //# sourceMappingURL=chunk-QESX7HDK.js.map