@opengeni/api-router 2.6.4 → 2.10.0-canary.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.
Files changed (75) hide show
  1. package/dist/access-grant-rls.d.ts +3 -0
  2. package/dist/app.d.ts +2 -1
  3. package/dist/app.js +3 -1
  4. package/dist/auth/organization-user-setup.d.ts +23 -0
  5. package/dist/{chunk-XTLI3CBH.js → chunk-XXH7DW34.js} +6866 -3554
  6. package/dist/chunk-XXH7DW34.js.map +1 -0
  7. package/dist/codemode.d.ts +6 -0
  8. package/dist/github-access.d.ts +25 -2
  9. package/dist/http/request-source.d.ts +14 -0
  10. package/dist/index.js +19 -3
  11. package/dist/index.js.map +1 -1
  12. package/dist/integrations/oauth-client.d.ts +2 -11
  13. package/dist/integrations/personal-github-repositories.d.ts +41 -2
  14. package/dist/integrations/slack-interactions.d.ts +1 -1
  15. package/dist/mcp/server.d.ts +158 -1
  16. package/dist/mcp/session-view.d.ts +56 -2
  17. package/dist/mcp/session-wait.d.ts +1 -1
  18. package/dist/mcp-oauth.d.ts +19 -0
  19. package/dist/model-catalog.d.ts +5 -31
  20. package/dist/routes/codex.d.ts +27 -2
  21. package/dist/routes/github.d.ts +2 -0
  22. package/dist/routes/organization-model-providers.d.ts +3 -0
  23. package/dist/routes/workspace-artifacts.d.ts +2 -1
  24. package/dist/work-discovery-observability.d.ts +1 -1
  25. package/dist/workspace-artifact-content.d.ts +28 -0
  26. package/dist/workspace-artifact-provenance.d.ts +7 -0
  27. package/dist/workspace-deletion.d.ts +6 -0
  28. package/dist/workspace-tool-gateway-observability.d.ts +17 -0
  29. package/dist/workspace-tool-gateway.d.ts +118 -0
  30. package/package.json +19 -18
  31. package/src/access-grant-rls.ts +40 -0
  32. package/src/app.ts +296 -53
  33. package/src/auth/organization-user-setup.ts +95 -5
  34. package/src/codemode.ts +33 -4
  35. package/src/github-access.ts +117 -1
  36. package/src/http/auth.ts +11 -0
  37. package/src/http/request-source.ts +37 -0
  38. package/src/http/sse.ts +15 -7
  39. package/src/index.ts +18 -2
  40. package/src/integrations/oauth-client.ts +168 -203
  41. package/src/integrations/personal-github-repositories.ts +238 -9
  42. package/src/integrations/slack-app-home.ts +2 -2
  43. package/src/integrations/slack-interactions.ts +77 -37
  44. package/src/mcp/company-brain-governed-writes.ts +2 -2
  45. package/src/mcp/company-profile-agent-admin.ts +1 -1
  46. package/src/mcp/remember.ts +1 -1
  47. package/src/mcp/server.ts +504 -133
  48. package/src/mcp/session-view.ts +20 -1
  49. package/src/mcp/session-wait.ts +3 -0
  50. package/src/mcp-oauth.ts +539 -0
  51. package/src/model-catalog.ts +45 -337
  52. package/src/routes/automations.ts +178 -19
  53. package/src/routes/codex.ts +242 -73
  54. package/src/routes/connections.ts +271 -16
  55. package/src/routes/documents.ts +67 -19
  56. package/src/routes/files.ts +54 -6
  57. package/src/routes/github.ts +116 -15
  58. package/src/routes/organization-memberships.ts +59 -16
  59. package/src/routes/organization-model-providers.ts +231 -0
  60. package/src/routes/packs.ts +1 -0
  61. package/src/routes/personal-github.ts +39 -0
  62. package/src/routes/pr-review.ts +72 -4
  63. package/src/routes/scheduled-tasks.ts +40 -13
  64. package/src/routes/sessions.ts +153 -65
  65. package/src/routes/supergrok.ts +3 -2
  66. package/src/routes/workspace-artifacts.ts +176 -67
  67. package/src/routes/workspaces.ts +405 -62
  68. package/src/sandbox/channel-a.ts +17 -4
  69. package/src/work-discovery-observability.ts +3 -3
  70. package/src/workspace-artifact-content.ts +163 -0
  71. package/src/workspace-artifact-provenance.ts +104 -0
  72. package/src/workspace-deletion.ts +64 -0
  73. package/src/workspace-tool-gateway-observability.ts +100 -0
  74. package/src/workspace-tool-gateway.ts +691 -0
  75. package/dist/chunk-XTLI3CBH.js.map +0 -1
package/src/mcp/server.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import {
3
3
  CreateScheduledTaskRequest,
4
+ boundSessionMcpText as capSessionDiscoveryText,
5
+ compactSessionMcpListRow,
6
+ sessionMcpIncludesRelatedWork,
4
7
  FIRST_PARTY_MCP_TOOL_NAMES,
5
8
  defaultRepositoryMountPath,
6
9
  SESSION_EVENT_RAW_DELTA_TYPES,
@@ -30,10 +33,13 @@ import {
30
33
  type SessionAuthorizationSurface,
31
34
  type Session,
32
35
  type WorkspaceMemoryPromptMode,
36
+ type WorkspaceArtifactMutationResponse,
33
37
  type ScheduledTask,
34
38
  UpdateScheduledTaskRequest,
35
39
  normalizeWorkspaceArtifactSlug,
36
40
  WORKSPACE_ARTIFACT_HTML_MAX_UTF8_BYTES,
41
+ WORKSPACE_ARTIFACT_REQUESTED_TOOLS_MAX,
42
+ WORKSPACE_ARTIFACT_SOURCE_MAX_FILES,
37
43
  SESSION_GOAL_PROGRESS_MAX_BYTES,
38
44
  SESSION_GOAL_RATIONALE_MAX_BYTES,
39
45
  SESSION_GOAL_SUCCESS_CRITERIA_MAX_BYTES,
@@ -67,6 +73,7 @@ import {
67
73
  encryptVariableSetValue,
68
74
  getSession,
69
75
  getSessionGoal,
76
+ getSessionMcpMonitoringSummary,
70
77
  getSessionQueueSnapshot,
71
78
  getSessionTurn,
72
79
  getOrCreatePreferenceRegistrySnapshot,
@@ -101,9 +108,9 @@ import {
101
108
  serializeEffectiveSessionControl,
102
109
  setSessionGoalStatusWithEvent,
103
110
  recordSessionGoalProgressWithEvent,
104
- holdSessionGoalContinuationWithEvent,
105
- SESSION_GOAL_HOLD_MAX_SECONDS,
106
- SESSION_GOAL_HOLD_MIN_SECONDS,
111
+ waitForSessionInputWithEvent,
112
+ SESSION_INPUT_WAIT_MAX_SECONDS,
113
+ SESSION_INPUT_WAIT_MIN_SECONDS,
107
114
  setVariableSetVariable,
108
115
  updateSessionGoalWithEvent,
109
116
  upsertSessionGoalWithEvent,
@@ -114,6 +121,7 @@ import {
114
121
  listWorkspaceArtifacts,
115
122
  publishWorkspaceArtifactVersion,
116
123
  rollbackWorkspaceArtifact,
124
+ setWorkspaceArtifactStatus,
117
125
  archiveTaskNote,
118
126
  createTaskNote,
119
127
  listTaskNotes,
@@ -154,6 +162,7 @@ import {
154
162
  requireLiveAgentAttemptAuthorization,
155
163
  requireSessionAuthorization,
156
164
  requireSessionAuthorizationListScope,
165
+ resolveWorkspaceCatalogSettings,
157
166
  SessionAuthorizationDeniedError,
158
167
  SessionAuthorizationUnavailableError,
159
168
  saveWorkspaceMemoryWithSlackPublication,
@@ -162,7 +171,6 @@ import {
162
171
  } from "@opengeni/core";
163
172
  import { recordWorkspaceUsage, requireLimit } from "@opengeni/core";
164
173
  import type { ApiRouteDeps } from "@opengeni/core";
165
- import { retryWhileMissing } from "@opengeni/storage";
166
174
  import {
167
175
  githubBindingStatus,
168
176
  listWorkspaceGitHubInstallationBindings,
@@ -170,6 +178,12 @@ import {
170
178
  } from "../github-access";
171
179
  import { githubBrowserBaseUrl, githubBrowserGrantClaims } from "../github-browser-flow";
172
180
  import { publishSandboxFileArtifact } from "../sandbox-file-artifacts";
181
+ import {
182
+ projectWorkspaceArtifactDetailProvenance,
183
+ projectWorkspaceArtifactMutationProvenance,
184
+ projectWorkspaceArtifactVersionProvenance,
185
+ redactWorkspaceArtifactListProvenance,
186
+ } from "../workspace-artifact-provenance";
173
187
  import {
174
188
  assertSocialConnectionProvider,
175
189
  socialMentionsLive,
@@ -231,10 +245,12 @@ import {
231
245
  type FleetServices,
232
246
  type RunOnOp,
233
247
  } from "@opengeni/core";
248
+ import { getSessionBackgroundCommand } from "@opengeni/db/session-background-commands";
234
249
  import {
235
250
  boundSessionEventCompactResult,
236
251
  boundSessionEventMcpPage,
237
252
  boundSessionDetailMcp,
253
+ boundSessionCompactDetailMcp,
238
254
  boundRigDetailMcp,
239
255
  SESSION_EVENT_MCP_MAX_BYTES,
240
256
  } from "./session-view";
@@ -277,6 +293,10 @@ import { registerRememberTools } from "./remember";
277
293
  import { mintSandboxCodemodeToken } from "@opengeni/runtime/sandbox";
278
294
  import { deleteScheduledTaskWithDurableCleanup } from "../scheduled-task-deletion";
279
295
  import { observeWorkDiscovery, summarizeWorkDiscoveryRows } from "../work-discovery-observability";
296
+ import {
297
+ prepareWorkspaceArtifactContent,
298
+ readWorkspaceArtifactContent,
299
+ } from "../workspace-artifact-content";
280
300
 
281
301
  export type McpServerOptions = {
282
302
  // Origin of the HTTP request that reached the MCP route. Browser-oriented
@@ -424,7 +444,7 @@ const FIRST_PARTY_TOOL_AUTHORIZATION = {
424
444
  goal_set: { sessionRequired: true, allOf: ["goals:manage"] },
425
445
  goal_update: { sessionRequired: true, allOf: ["goals:manage"] },
426
446
  goal_progress: { sessionRequired: true, allOf: ["goals:manage"] },
427
- goal_wait: { sessionRequired: true, allOf: ["goals:manage"] },
447
+ wait_for_input: { sessionRequired: true, allOf: ["sessions:control"] },
428
448
  goal_complete: { sessionRequired: true, allOf: ["goals:manage"] },
429
449
  goal_pause: { sessionRequired: true, allOf: ["goals:manage"] },
430
450
  memory_search: { sessionRequired: true, allOf: ["documents:search"] },
@@ -498,6 +518,7 @@ const FIRST_PARTY_TOOL_AUTHORIZATION = {
498
518
  // Blocking wait inside a running turn: the live attempt's own session is the
499
519
  // self target, so the tool exists only for session-scoped grants.
500
520
  session_wait: { sessionRequired: true, allOf: ["sessions:read"] },
521
+ command_wait: { sessionRequired: true, allOf: ["sessions:read"] },
501
522
  session_create: { allOf: ["sessions:create"] },
502
523
  session_send_message: { allOf: ["sessions:control"] },
503
524
  session_pause: { allOf: ["sessions:control"] },
@@ -617,6 +638,8 @@ const FIRST_PARTY_TOOL_AUTHORIZATION = {
617
638
  artifacts_create: { sessionRequired: true, allOf: ["artifacts:publish"] },
618
639
  artifacts_publish: { sessionRequired: true, allOf: ["artifacts:publish"] },
619
640
  artifacts_rollback: { sessionRequired: true, allOf: ["artifacts:publish"] },
641
+ artifacts_archive: { sessionRequired: true, allOf: ["artifacts:publish"] },
642
+ artifacts_restore: { sessionRequired: true, allOf: ["artifacts:publish"] },
620
643
  sandbox_file_publish: {
621
644
  sessionRequired: true,
622
645
  allOf: ["files:read", "files:upload"],
@@ -805,8 +828,11 @@ export function buildOpenGeniMcpServer(
805
828
  },
806
829
  );
807
830
  }
808
- // Goal tools require goals:manage (in the default first-party permission set).
809
- if (sessionId !== null && can("goals:manage")) {
831
+ // PolicyMcpServer applies each tool's own permission contract. Register this
832
+ // mixed group for every session so session-level wait_for_input remains
833
+ // available with sessions:control even when goals:manage is intentionally
834
+ // absent.
835
+ if (sessionId !== null) {
810
836
  registerGoalTools(server, deps, grant, sessionId, json);
811
837
  }
812
838
  if (sessionId !== null && options.workspaceMemoryEnabled === true) {
@@ -1718,7 +1744,7 @@ export function buildOpenGeniMcpServer(
1718
1744
  async ({ id, triggerId }) => {
1719
1745
  const task = await requireScheduledTask(deps.db, grant.workspaceId, id);
1720
1746
  if (task.action.kind === "agent_turn") {
1721
- await validateScheduledTaskTarget({
1747
+ const targetSession = await validateScheduledTaskTarget({
1722
1748
  db: deps.db,
1723
1749
  sessionAuthorization: deps.sessionAuthorization,
1724
1750
  authorizationSurface: "first_party_mcp",
@@ -1730,8 +1756,16 @@ export function buildOpenGeniMcpServer(
1730
1756
  agentConfig: task.agentConfig,
1731
1757
  missingTargetStatus: 404,
1732
1758
  });
1759
+ const catalogSourceSettings = deps.catalogSourceSettings ?? deps.settings;
1760
+ const catalogSettings = (
1761
+ await resolveWorkspaceCatalogSettings(deps.db, catalogSourceSettings, {
1762
+ accountId: grant.accountId,
1763
+ workspaceId: grant.workspaceId,
1764
+ ...(targetSession ? { retainedProductModelId: targetSession.model } : {}),
1765
+ })
1766
+ ).settings;
1733
1767
  await validateScheduledTaskMachineTarget({
1734
- settings: deps.settings,
1768
+ settings: catalogSettings,
1735
1769
  db: deps.db,
1736
1770
  grant,
1737
1771
  runMode: task.runMode,
@@ -2563,6 +2597,12 @@ function registerGoalTools(
2563
2597
  SESSION_GOAL_PROGRESS_MAX_BYTES,
2564
2598
  "goal progress note",
2565
2599
  );
2600
+ const inputWaitReasonSchema = boundedGoalToolString(2 * 1024, "session input wait reason").refine(
2601
+ (value) => value.trim().length > 0,
2602
+ {
2603
+ message: "session input wait reason must not be blank",
2604
+ },
2605
+ );
2566
2606
  server.registerTool(
2567
2607
  "goal_set",
2568
2608
  {
@@ -2721,38 +2761,36 @@ function registerGoalTools(
2721
2761
  );
2722
2762
 
2723
2763
  server.registerTool(
2724
- "goal_wait",
2764
+ "wait_for_input",
2725
2765
  {
2726
2766
  description:
2727
- "Hold the active goal's automatic continuation while progress depends on child sessions or an external event. Use it instead of sleeping or polling sessions_list/session_get/session_events, and only after re-checking the child or external state once. Always end your turn right after calling it: you will be woken by a child result, an agent message, a human prompt, or at the deadline, and the goal stays active. The deadline (untilSeconds) is mandatory and capped at 7 days. Never use goal_wait instead of goal_pause when you are blocked on a human decision.",
2767
+ "End the current turn and wait out of turn for relevant session input. This is self-only and does not require a goal. Use it for long or uncertain waits instead of sleeping or repeatedly calling session_wait/command_wait. timeoutSeconds is a relative safety-wake duration; OpenGeni persists its absolute deadline, and timeout never cancels a background command. A human/API prompt, agent message or Steer, child terminal result, scheduled input, terminal background-command result, or the deadline wakes the session. Always end your turn immediately after the tool succeeds. Use goal_pause instead when the active goal itself should stop pending a human decision.",
2728
2768
  inputSchema: {
2729
- reason: goalRationale,
2730
- untilSeconds: z4
2769
+ reason: inputWaitReasonSchema,
2770
+ timeoutSeconds: z4
2731
2771
  .number()
2732
2772
  .int()
2733
- .min(SESSION_GOAL_HOLD_MIN_SECONDS)
2734
- .max(SESSION_GOAL_HOLD_MAX_SECONDS),
2773
+ .min(SESSION_INPUT_WAIT_MIN_SECONDS)
2774
+ .max(SESSION_INPUT_WAIT_MAX_SECONDS),
2735
2775
  idempotencyKey: z4.string().uuid().optional(),
2736
2776
  },
2737
2777
  },
2738
- async ({ reason, untilSeconds, idempotencyKey }) => {
2739
- await authorizeFirstPartySession(deps, grant, sessionId, "session.goal.write");
2778
+ async ({ reason, timeoutSeconds, idempotencyKey }) => {
2779
+ await authorizeFirstPartySession(deps, grant, sessionId, "session.control");
2740
2780
  const context = exactAgentCommandContext(grant, sessionId);
2741
- // Without a caller-supplied key the operation is idempotent per (goal
2742
- // target, turn, exact arguments): a replacement attempt replaying the
2743
- // same call receives the stored result instead of re-stamping the hold,
2744
- // while a genuinely new same-turn wait (different reason/deadline) is a
2745
- // new operation whose later hold wins.
2746
2781
  const operationKey =
2747
2782
  idempotencyKey ??
2748
- `goal-wait:${context.callerTurnId}:${createHash("sha256")
2749
- .update(JSON.stringify({ reason, untilSeconds }))
2783
+ `wait-for-input:${context.callerTurnId}:${createHash("sha256")
2784
+ .update(JSON.stringify({ reason, timeoutSeconds }))
2750
2785
  .digest("hex")
2751
2786
  .slice(0, 32)}`;
2752
- const { goal, events, operationId, replay, untilAt } =
2753
- await holdSessionGoalContinuationWithEvent(deps.db, grant.workspaceId, sessionId, {
2787
+ const { events, operationId, replay, deadlineAt } = await waitForSessionInputWithEvent(
2788
+ deps.db,
2789
+ grant.workspaceId,
2790
+ sessionId,
2791
+ {
2754
2792
  reason,
2755
- untilSeconds,
2793
+ timeoutSeconds,
2756
2794
  command: {
2757
2795
  accountId: grant.accountId,
2758
2796
  actor: {
@@ -2764,16 +2802,16 @@ function registerGoalTools(
2764
2802
  },
2765
2803
  operationKey,
2766
2804
  },
2767
- });
2805
+ },
2806
+ );
2768
2807
  await publishDurableSessionEvents(deps.bus, grant.workspaceId, sessionId, events);
2769
2808
  return json({
2770
- status: "held",
2771
- goalId: goal.id,
2772
- untilAt,
2809
+ status: "waiting_for_input",
2810
+ deadlineAt,
2773
2811
  operationId,
2774
2812
  replay,
2775
2813
  nextAction:
2776
- "End your turn now; you will be woken by a child result, a message, a human prompt, or at untilAt.",
2814
+ "End your turn now. Relevant input or the timeout deadline will start a new turn; the timeout does not cancel commands.",
2777
2815
  });
2778
2816
  },
2779
2817
  );
@@ -2913,42 +2951,42 @@ function registerWorkspaceArtifactTools(
2913
2951
  const authorize = async () => {
2914
2952
  await authorizeFirstPartySession(deps, grant, sessionId, "session.first_party_mcp.call");
2915
2953
  };
2916
- const prepare = (html: string) => {
2954
+ const canReadProvenanceSession = (sourceSessionId: string): Promise<boolean> =>
2955
+ authorizeFirstPartySession(deps, grant, sourceSessionId, "session.read")
2956
+ .then(() => true)
2957
+ .catch((error) => {
2958
+ if (error instanceof SessionAuthorizationDeniedError) return false;
2959
+ throw error;
2960
+ });
2961
+ const mutation = async (response: WorkspaceArtifactMutationResponse) =>
2962
+ await projectWorkspaceArtifactMutationProvenance(response, canReadProvenanceSession);
2963
+ const prepare = (
2964
+ html: string,
2965
+ source?: { entrypoint: string; files: Array<{ path: string; content: string }> },
2966
+ requestedTools?: Array<{ serverId: string; toolName: string }>,
2967
+ ) => {
2917
2968
  if (!deps.objectStorage) throw new Error("Object storage is not configured");
2918
- const bytes = new TextEncoder().encode(html);
2919
- if (bytes.byteLength < 1 || bytes.byteLength > WORKSPACE_ARTIFACT_HTML_MAX_UTF8_BYTES) {
2920
- throw new Error(
2921
- `Artifact HTML must be 1-${WORKSPACE_ARTIFACT_HTML_MAX_UTF8_BYTES} UTF-8 bytes`,
2922
- );
2923
- }
2924
- const contentSha256 = createHash("sha256").update(bytes).digest("hex");
2925
- const contentKey = `workspaces/${grant.workspaceId}/workspace-artifacts/blobs/${contentSha256}.html`;
2926
- return {
2927
- contentKey,
2928
- contentSha256,
2929
- sizeBytes: bytes.byteLength,
2930
- persistContent: async () => {
2931
- await deps.objectStorage!.putObject({
2932
- key: contentKey,
2933
- contentType: "text/html; charset=utf-8",
2934
- body: bytes,
2935
- sha256: contentSha256,
2936
- });
2937
- },
2938
- };
2969
+ return prepareWorkspaceArtifactContent(deps.objectStorage, grant.workspaceId, {
2970
+ html,
2971
+ ...(source ? { source } : {}),
2972
+ ...(requestedTools ? { requestedTools } : {}),
2973
+ });
2939
2974
  };
2940
2975
  const provenance = (
2941
2976
  idempotencyKey: string,
2942
- sourceToolName: "artifacts_create" | "artifacts_publish" | "artifacts_rollback",
2977
+ sourceToolName:
2978
+ | "artifacts_create"
2979
+ | "artifacts_publish"
2980
+ | "artifacts_rollback"
2981
+ | "artifacts_archive"
2982
+ | "artifacts_restore",
2943
2983
  ) => {
2944
2984
  const claims = attempt();
2945
2985
  return {
2946
2986
  accountId: grant.accountId,
2947
2987
  workspaceId: grant.workspaceId,
2948
2988
  operationKey: `attempt:${createHash("sha256")
2949
- .update(
2950
- `${claims.sessionId}:${claims.turnId}:${claims.attemptId}:${claims.executionGeneration}:${idempotencyKey}`,
2951
- )
2989
+ .update(`${claims.sessionId}:${claims.turnId}:${idempotencyKey}`)
2952
2990
  .digest("hex")}`,
2953
2991
  actorSubjectId: grant.subjectId,
2954
2992
  sourceSessionId: claims.sessionId,
@@ -2968,7 +3006,11 @@ function registerWorkspaceArtifactTools(
2968
3006
  },
2969
3007
  async () => {
2970
3008
  await authorize();
2971
- return json(await listWorkspaceArtifacts(deps.db, grant.workspaceId));
3009
+ return json(
3010
+ redactWorkspaceArtifactListProvenance(
3011
+ await listWorkspaceArtifacts(deps.db, grant.workspaceId),
3012
+ ),
3013
+ );
2972
3014
  },
2973
3015
  );
2974
3016
 
@@ -2985,21 +3027,27 @@ function registerWorkspaceArtifactTools(
2985
3027
  async ({ artifactId, versionId }) => {
2986
3028
  await authorize();
2987
3029
  if (!deps.objectStorage) throw new Error("Object storage is not configured");
2988
- const [detail, ref] = await Promise.all([
3030
+ const [rawDetail, ref] = await Promise.all([
2989
3031
  getWorkspaceArtifact(deps.db, grant.workspaceId, artifactId),
2990
3032
  getWorkspaceArtifactContentRef(deps.db, grant.workspaceId, artifactId, versionId),
2991
3033
  ]);
2992
- const object = await retryWhileMissing(async () =>
2993
- deps.objectStorage!.getObjectBytes(ref.contentKey),
2994
- );
2995
- if (!object) throw new Error("Artifact content is unavailable");
2996
- const actualHash = createHash("sha256").update(object.bytes).digest("hex");
2997
- if (actualHash !== ref.version.contentSha256)
2998
- throw new Error("Artifact content failed integrity verification");
3034
+ const sourceAuthorizations = new Map<string, Promise<boolean>>();
3035
+ const canReadSourceSession = (sourceSessionId: string): Promise<boolean> => {
3036
+ const existing = sourceAuthorizations.get(sourceSessionId);
3037
+ if (existing) return existing;
3038
+ const decision = canReadProvenanceSession(sourceSessionId);
3039
+ sourceAuthorizations.set(sourceSessionId, decision);
3040
+ return decision;
3041
+ };
3042
+ const [detail, projectedVersion] = await Promise.all([
3043
+ projectWorkspaceArtifactDetailProvenance(rawDetail, canReadSourceSession),
3044
+ projectWorkspaceArtifactVersionProvenance(ref.version, canReadSourceSession),
3045
+ ]);
3046
+ const content = await readWorkspaceArtifactContent(deps.objectStorage, ref);
2999
3047
  return json({
3000
3048
  detail,
3001
- version: ref.version,
3002
- html: new TextDecoder().decode(object.bytes),
3049
+ version: projectedVersion,
3050
+ ...content,
3003
3051
  });
3004
3052
  },
3005
3053
  );
@@ -3018,23 +3066,49 @@ function registerWorkspaceArtifactTools(
3018
3066
  .max(96)
3019
3067
  .optional(),
3020
3068
  html: z4.string().min(1).max(WORKSPACE_ARTIFACT_HTML_MAX_UTF8_BYTES),
3069
+ source: z4
3070
+ .object({
3071
+ entrypoint: z4.string().min(1).max(256),
3072
+ files: z4
3073
+ .array(
3074
+ z4.object({
3075
+ path: z4.string().min(1).max(256),
3076
+ content: z4.string(),
3077
+ }),
3078
+ )
3079
+ .min(1)
3080
+ .max(WORKSPACE_ARTIFACT_SOURCE_MAX_FILES),
3081
+ })
3082
+ .optional(),
3083
+ requestedTools: z4
3084
+ .array(
3085
+ z4.object({
3086
+ serverId: z4.string().min(1).max(256),
3087
+ toolName: z4.string().min(1).max(512),
3088
+ }),
3089
+ )
3090
+ .max(WORKSPACE_ARTIFACT_REQUESTED_TOOLS_MAX)
3091
+ .optional(),
3021
3092
  idempotencyKey: z4.string().min(1).max(200),
3022
3093
  },
3023
3094
  },
3024
- async ({ title, description, slug, html, idempotencyKey }) => {
3095
+ async ({ title, description, slug, html, source, requestedTools, idempotencyKey }) => {
3025
3096
  await authorize();
3026
3097
  const artifactId = crypto.randomUUID();
3027
3098
  const slugBase = slug ?? (normalizeWorkspaceArtifactSlug(title) || "artifact");
3028
3099
  const resolvedSlug = slug ?? `${slugBase.slice(0, 87)}-${artifactId.slice(0, 8)}`;
3029
3100
  return json(
3030
- await createWorkspaceArtifact(deps.db, {
3031
- artifactId,
3032
- slug: resolvedSlug,
3033
- title,
3034
- description: description ?? null,
3035
- ...prepare(html),
3036
- ...provenance(idempotencyKey, "artifacts_create"),
3037
- }),
3101
+ await mutation(
3102
+ await createWorkspaceArtifact(deps.db, {
3103
+ artifactId,
3104
+ slug: resolvedSlug,
3105
+ requestedSlug: slug ?? null,
3106
+ title,
3107
+ description: description ?? null,
3108
+ ...prepare(html, source, requestedTools),
3109
+ ...provenance(idempotencyKey, "artifacts_create"),
3110
+ }),
3111
+ ),
3038
3112
  );
3039
3113
  },
3040
3114
  );
@@ -3050,20 +3124,54 @@ function registerWorkspaceArtifactTools(
3050
3124
  title: z4.string().min(1).max(120).optional(),
3051
3125
  description: z4.string().max(2000).nullable().optional(),
3052
3126
  html: z4.string().min(1).max(WORKSPACE_ARTIFACT_HTML_MAX_UTF8_BYTES),
3127
+ source: z4
3128
+ .object({
3129
+ entrypoint: z4.string().min(1).max(256),
3130
+ files: z4
3131
+ .array(
3132
+ z4.object({
3133
+ path: z4.string().min(1).max(256),
3134
+ content: z4.string(),
3135
+ }),
3136
+ )
3137
+ .min(1)
3138
+ .max(WORKSPACE_ARTIFACT_SOURCE_MAX_FILES),
3139
+ })
3140
+ .optional(),
3141
+ requestedTools: z4
3142
+ .array(
3143
+ z4.object({
3144
+ serverId: z4.string().min(1).max(256),
3145
+ toolName: z4.string().min(1).max(512),
3146
+ }),
3147
+ )
3148
+ .max(WORKSPACE_ARTIFACT_REQUESTED_TOOLS_MAX)
3149
+ .optional(),
3053
3150
  idempotencyKey: z4.string().min(1).max(200),
3054
3151
  },
3055
3152
  },
3056
- async ({ artifactId, expectedCurrentVersionId, title, description, html, idempotencyKey }) => {
3153
+ async ({
3154
+ artifactId,
3155
+ expectedCurrentVersionId,
3156
+ title,
3157
+ description,
3158
+ html,
3159
+ source,
3160
+ requestedTools,
3161
+ idempotencyKey,
3162
+ }) => {
3057
3163
  await authorize();
3058
3164
  return json(
3059
- await publishWorkspaceArtifactVersion(deps.db, {
3060
- artifactId,
3061
- expectedCurrentVersionId,
3062
- ...(title !== undefined ? { title } : {}),
3063
- ...(description !== undefined ? { description } : {}),
3064
- ...prepare(html),
3065
- ...provenance(idempotencyKey, "artifacts_publish"),
3066
- }),
3165
+ await mutation(
3166
+ await publishWorkspaceArtifactVersion(deps.db, {
3167
+ artifactId,
3168
+ expectedCurrentVersionId,
3169
+ ...(title !== undefined ? { title } : {}),
3170
+ ...(description !== undefined ? { description } : {}),
3171
+ ...prepare(html, source, requestedTools),
3172
+ ...provenance(idempotencyKey, "artifacts_publish"),
3173
+ }),
3174
+ ),
3067
3175
  );
3068
3176
  },
3069
3177
  );
@@ -3084,13 +3192,71 @@ function registerWorkspaceArtifactTools(
3084
3192
  async ({ artifactId, versionId, expectedCurrentVersionId, reason, idempotencyKey }) => {
3085
3193
  await authorize();
3086
3194
  return json(
3087
- await rollbackWorkspaceArtifact(deps.db, {
3088
- artifactId,
3089
- versionId,
3090
- expectedCurrentVersionId,
3091
- reason,
3092
- ...provenance(idempotencyKey, "artifacts_rollback"),
3093
- }),
3195
+ await mutation(
3196
+ await rollbackWorkspaceArtifact(deps.db, {
3197
+ artifactId,
3198
+ versionId,
3199
+ expectedCurrentVersionId,
3200
+ reason,
3201
+ ...provenance(idempotencyKey, "artifacts_rollback"),
3202
+ }),
3203
+ ),
3204
+ );
3205
+ },
3206
+ );
3207
+
3208
+ server.registerTool(
3209
+ "artifacts_archive",
3210
+ {
3211
+ description:
3212
+ "Unpublish an active workspace artifact without deleting its immutable versions or retained source.",
3213
+ inputSchema: {
3214
+ artifactId: z4.string().uuid(),
3215
+ expectedCurrentVersionId: z4.string().uuid(),
3216
+ reason: z4.string().min(1).max(4096),
3217
+ idempotencyKey: z4.string().min(1).max(200),
3218
+ },
3219
+ },
3220
+ async ({ artifactId, expectedCurrentVersionId, reason, idempotencyKey }) => {
3221
+ await authorize();
3222
+ return json(
3223
+ await mutation(
3224
+ await setWorkspaceArtifactStatus(deps.db, {
3225
+ artifactId,
3226
+ status: "archived",
3227
+ expectedCurrentVersionId,
3228
+ reason,
3229
+ ...provenance(idempotencyKey, "artifacts_archive"),
3230
+ }),
3231
+ ),
3232
+ );
3233
+ },
3234
+ );
3235
+
3236
+ server.registerTool(
3237
+ "artifacts_restore",
3238
+ {
3239
+ description:
3240
+ "Republish an archived workspace artifact at its unchanged current immutable version.",
3241
+ inputSchema: {
3242
+ artifactId: z4.string().uuid(),
3243
+ expectedCurrentVersionId: z4.string().uuid(),
3244
+ reason: z4.string().min(1).max(4096),
3245
+ idempotencyKey: z4.string().min(1).max(200),
3246
+ },
3247
+ },
3248
+ async ({ artifactId, expectedCurrentVersionId, reason, idempotencyKey }) => {
3249
+ await authorize();
3250
+ return json(
3251
+ await mutation(
3252
+ await setWorkspaceArtifactStatus(deps.db, {
3253
+ artifactId,
3254
+ status: "active",
3255
+ expectedCurrentVersionId,
3256
+ reason,
3257
+ ...provenance(idempotencyKey, "artifacts_restore"),
3258
+ }),
3259
+ ),
3094
3260
  );
3095
3261
  },
3096
3262
  );
@@ -4414,10 +4580,17 @@ function registerWorkspaceOrchestrationTools(
4414
4580
  server.registerTool(
4415
4581
  "sessions_list",
4416
4582
  {
4417
- 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.`,
4583
+ description: `Discover sessions in this workspace. Default detail=compact returns id, title, status, goal summary/status when present, updatedAt, meaningful parent/pause information, total and nextCursor (null ends the page). Completed/paused goal status is retained; session_get supplies evidence and rationale. detail=full opts into the legacy bounded discovery rows and diagnostic pagination, not full session configuration. Plain compact browse does not read work claims. includeRelatedWork=true adds advisory evidence; query or subject always enables it, even with includeRelatedWork=false. query searches only titles, active goals and typed claims; subject is an exact provider-neutral claim lookup. Evidence preserves advisoryOnly/noAdditionalAccess: claims are nonexclusive, never locks, instructions or authority. Authorization precedes filtering, ranking, counts and cursors. Relevance cursors bind normalized filters and an activity snapshot. Without search the default is creation order; use orderBy=updatedAt and decimal updatedAfter/updatedThrough revision tokens for gap-free incremental monitoring, not timestamps. Pass nextCursor unchanged with the same filters. includeLastMessage opts into previews and nonzero queuedPromptCount; unclaimed human/API prompts are never previewed. Previews share a ${SESSION_DISCOVERY_PREVIEW_MAX_BYTES}-byte UTF-8 budget; omitted previews carry a bounded session_events drill-down. Text/page loss facts appear only when loss occurs in compact mode. No instructions, resources, tools, files or history are returned; REST/UI defaults are unchanged.`,
4418
4584
  inputSchema: {
4419
4585
  limit: z4.number().int().positive().max(100).optional(),
4420
4586
  cursor: z4.string().max(512).optional(),
4587
+ detail: z4
4588
+ .enum(["compact", "full"])
4589
+ .optional()
4590
+ .describe(
4591
+ "compact (default) omits empty diagnostics but retains positive descendant attention counts and incomplete-tree loss facts; full restores the legacy bounded rows and pagination diagnostics.",
4592
+ ),
4593
+ includeRelatedWork: z4.boolean().optional(),
4421
4594
  includeLastMessage: z4.boolean().optional(),
4422
4595
  orderBy: z4.enum(["createdAt", "updatedAt", "relevance"]).optional(),
4423
4596
  updatedAfter: z4.string().max(64).optional(),
@@ -4455,6 +4628,8 @@ function registerWorkspaceOrchestrationTools(
4455
4628
  async ({
4456
4629
  limit,
4457
4630
  cursor,
4631
+ detail,
4632
+ includeRelatedWork,
4458
4633
  includeLastMessage,
4459
4634
  orderBy: requestedOrderBy,
4460
4635
  updatedAfter,
@@ -4478,6 +4653,12 @@ function registerWorkspaceOrchestrationTools(
4478
4653
  authorizationScope?.kind === "scoped" ? "scoped" : "workspace";
4479
4654
  const decodedCursor = cursor ? decodeSessionDiscoveryCursor(cursor) : undefined;
4480
4655
  const relevanceRequested = Boolean(query?.trim() || subject);
4656
+ const relatedWorkRequested = sessionMcpIncludesRelatedWork({
4657
+ detail,
4658
+ includeRelatedWork,
4659
+ query,
4660
+ subject,
4661
+ });
4481
4662
  if (relevanceRequested && !deps.settings.workDiscoveryEnabled) {
4482
4663
  observeWorkDiscovery(deps.observability, {
4483
4664
  surface: "first_party_mcp",
@@ -4524,18 +4705,24 @@ function registerWorkspaceOrchestrationTools(
4524
4705
  ...(parentSessionId !== undefined ? { parentSessionId } : {}),
4525
4706
  ...(subject ? { subject: subject as WorkClaimSubjectFilter } : {}),
4526
4707
  ...(claimLimit !== undefined ? { claimLimit } : {}),
4527
- includeWorkDiscovery: deps.settings.workDiscoveryEnabled,
4708
+ includeWorkDiscovery: deps.settings.workDiscoveryEnabled && relatedWorkRequested,
4528
4709
  subjectId: grant.subjectId,
4529
4710
  ...(authorizationScope ? { authorizationScope } : {}),
4530
4711
  });
4531
- const result = capSessionDiscoveryPage(page, includeLastMessage === true);
4712
+ const result =
4713
+ detail === "full"
4714
+ ? capSessionDiscoveryPage(page, includeLastMessage === true)
4715
+ : capSessionDiscoveryCompactPage(page, {
4716
+ includeLastMessage: includeLastMessage === true,
4717
+ includeRelatedWork: relatedWorkRequested,
4718
+ });
4532
4719
  observeWorkDiscovery(deps.observability, {
4533
4720
  surface: "first_party_mcp",
4534
4721
  mode,
4535
4722
  outcome: result.sessions.length === 0 ? "empty" : "ok",
4536
4723
  authorizationScope: metricAuthorizationScope,
4537
4724
  durationMs: performance.now() - startedAtMs,
4538
- responseBytes: result.bytes,
4725
+ responseBytes: Buffer.byteLength(JSON.stringify(result, null, 2), "utf8"),
4539
4726
  ...summarizeWorkDiscoveryRows(result.sessions),
4540
4727
  });
4541
4728
  return json(result);
@@ -4560,10 +4747,13 @@ function registerWorkspaceOrchestrationTools(
4560
4747
  "session_get",
4561
4748
  {
4562
4749
  description:
4563
- "Get another session you are managing: status, goal-bearing metadata, resources, persisted tool refs, exact bounded effectiveToolPolicy, and variableSet attachment (names/ids only, never variable values). effectiveToolPolicy distinguishes selected refs from workspace defaults, mandatory carriers, configured/deferred refs, and dropped refs. Do not call this with your own current session id to reconstruct context; your model-facing conversation history and persistent setting state are already supplied directly. Unbounded agent-set fields are clamped so monitoring another session cannot flood this context.",
4564
- inputSchema: { sessionId: z4.string().uuid() },
4750
+ "Get another session you are managing. Default detail=compact returns status, goal (including completion evidence or pause rationale), latest recorded goal progress, meaningful pause/wait state, queue counts, active turn and lastSequence for session_wait. Goal completion is not a terminal child result: join with session_wait waitFor=completion. Use detail=full for the legacy bounded configuration including resources, persisted tool refs, effectiveToolPolicy and variableSet ids (never variable values). Full mode is configuration, not a substitute for compact goal/progress facts. Do not use your own session id to reconstruct context; conversation history and persistent settings are supplied directly. Text loss is explicit; REST/UI defaults are unchanged.",
4751
+ inputSchema: {
4752
+ sessionId: z4.string().uuid(),
4753
+ detail: z4.enum(["compact", "full"]).optional(),
4754
+ },
4565
4755
  },
4566
- async ({ sessionId }) => {
4756
+ async ({ sessionId, detail }) => {
4567
4757
  const authorization = await authorizeFirstPartySession(
4568
4758
  deps,
4569
4759
  grant,
@@ -4582,12 +4772,18 @@ function registerWorkspaceOrchestrationTools(
4582
4772
  },
4583
4773
  authorization?.relatedSessionAccess ?? "root",
4584
4774
  );
4775
+ if (detail !== "full") {
4776
+ return json(
4777
+ boundSessionCompactDetailMcp(
4778
+ projected,
4779
+ await getSessionMcpMonitoringSummary(deps.db, grant.workspaceId, sessionId),
4780
+ queue,
4781
+ ),
4782
+ );
4783
+ }
4585
4784
  return json(
4586
4785
  boundSessionDetailMcp(
4587
- await withMcpEffectivePolicy(deps, grant.workspaceId, grant.subjectId, {
4588
- ...projected,
4589
- effectiveControl: queue?.effectiveControl ?? projected.effectiveControl,
4590
- }),
4786
+ await withMcpEffectivePolicy(deps, grant.workspaceId, grant.subjectId, projected),
4591
4787
  ),
4592
4788
  );
4593
4789
  },
@@ -4709,7 +4905,7 @@ function registerWorkspaceOrchestrationTools(
4709
4905
  server.registerTool(
4710
4906
  "session_wait",
4711
4907
  {
4712
- description: `Block until a watched session has new durable events after your cursor, until your own session has pending machine input (a child result, an agent message, a steer), or until maxWaitSeconds (default ${SESSION_WAIT_DEFAULT_SECONDS}, max ${SESSION_WAIT_MAX_SECONDS}) elapses. Use this for short waits inside the current turn instead of sleeping and polling session_events/session_get/sessions_list while a child or peer session works; for long waits end this turn with goal_wait rather than looping session_wait for hours while holding the turn and sandbox. Pass each target's sessionId and afterSequence (its last seen sequence, 0 for a new session). waitFor=change is the backward-compatible default and returns on turn lifecycle, agent.message.completed, blocking failures, goal facts, or session status/control changes. waitFor=completion is the child-result join: it ignores progress, completed commentary messages, goal facts, maintenance turns, and continuation segment settlements and returns only for a result-bearing final turn or a blocking state. A goal.completed event records goal state but is not a terminal child result. Raw deltas, tool receipts, sandbox diagnostics, and unrelated progress never wake either mode. Each changed target returns a bounded compact summary of up to ${SESSION_WAIT_EVENTS_PER_TARGET} exact durable events plus latestSequence (pass it back as the next afterSequence) and hasMore (drill down with session_events after=latestSequence). ownPendingUpdates > 0 means your own session has machine input that is delivered only when your next turn is claimed: finish this turn to receive it, or pass includeOwnPendingUpdates=false to keep waiting on the targets. timedOut=true means nothing changed; liveFanout=false means the live bus was unavailable and the wait relied on the deadline re-check. The whole result is byte-bounded: summaries are shortened first, then newest rows dropped, so a changed target may come back with events=[] and hasMore=true; read those rows with session_events after=latestSequence. The wait cannot exceed ${SESSION_WAIT_MAX_SECONDS} seconds because the MCP client request timeout is 60 seconds.`,
4908
+ description: `Block until a watched session has new durable events after your cursor, until your own session has pending machine input (a child result, an agent message, a steer, or a background-command result), or until maxWaitSeconds (default ${SESSION_WAIT_DEFAULT_SECONDS}, max ${SESSION_WAIT_MAX_SECONDS}) elapses. Use this for one short wait inside the current turn instead of sleeping and polling session_events/session_get/sessions_list while a child or peer session works. Do not immediately repeat it after timedOut=true unless new evidence suggests completion within one more window; for long or uncertain waits call wait_for_input once and end the turn. Pass each target's sessionId and afterSequence (its last seen sequence, 0 for a new session). waitFor=change is the default and returns on turn lifecycle, agent.message.completed, terminal background commands, blocking failures, goal facts, or session status/control changes. waitFor=completion is the child-result join: it ignores progress, completed commentary messages, goal facts, background commands, maintenance turns, and continuation segment settlements and returns only for a result-bearing final turn or a blocking state. A goal.completed event records goal state but is not a terminal child result. Raw deltas, tool receipts, sandbox diagnostics, and unrelated progress never wake either mode. Each changed target returns a bounded compact summary of up to ${SESSION_WAIT_EVENTS_PER_TARGET} exact durable events plus latestSequence (pass it back as the next afterSequence) and hasMore (drill down with session_events after=latestSequence). ownPendingUpdates > 0 means your own session has machine input that is delivered only when your next turn is claimed: finish this turn to receive it, or pass includeOwnPendingUpdates=false to keep waiting on the targets. timedOut=true means nothing changed; liveFanout=false means the live bus was unavailable and the wait relied on the deadline re-check. The whole result is byte-bounded: summaries are shortened first, then newest rows dropped, so a changed target may come back with events=[] and hasMore=true; read those rows with session_events after=latestSequence. The wait cannot exceed ${SESSION_WAIT_MAX_SECONDS} seconds because the MCP client request timeout is 60 seconds.`,
4713
4909
  inputSchema: {
4714
4910
  targets: z4
4715
4911
  .array(
@@ -4809,6 +5005,127 @@ function registerWorkspaceOrchestrationTools(
4809
5005
  );
4810
5006
  },
4811
5007
  );
5008
+
5009
+ if (callerSessionId !== null) {
5010
+ server.registerTool(
5011
+ "command_wait",
5012
+ {
5013
+ description: `Wait briefly for one background command owned by this session to settle, without provider-specific polling or write_stdin loops. The durable command row is authoritative. This returns immediately when the command is already terminal or otherwise waits up to maxWaitSeconds (default ${SESSION_WAIT_DEFAULT_SECONDS}, max ${SESSION_WAIT_MAX_SECONDS}) for its session.command.finished event or other pending input for this session. timedOut=true never cancels the command. Do not immediately repeat command_wait after a timeout unless new evidence suggests completion within one more window; for long or uncertain work call wait_for_input once and end the turn.`,
5014
+ inputSchema: {
5015
+ commandId: z4.string().uuid(),
5016
+ maxWaitSeconds: z4.number().int().min(1).max(SESSION_WAIT_MAX_SECONDS).optional(),
5017
+ },
5018
+ },
5019
+ async ({ commandId, maxWaitSeconds }, extra) => {
5020
+ await authorizeFirstPartySession(deps, grant, callerSessionId, "session.events.read");
5021
+ const initialSession = await requireSession(deps.db, grant.workspaceId, callerSessionId);
5022
+ let command = await getSessionBackgroundCommand(deps.db, {
5023
+ accountId: grant.accountId,
5024
+ workspaceId: grant.workspaceId,
5025
+ sessionId: callerSessionId,
5026
+ commandId,
5027
+ });
5028
+ if (!command) throw new Error("Background command not found in this session");
5029
+
5030
+ const terminal = () => command?.state === "exited" || command?.state === "lost";
5031
+ let waitResult: Awaited<ReturnType<typeof waitForSessionChanges>> | null = null;
5032
+ if (!terminal()) {
5033
+ waitResult = await waitForSessionChanges({
5034
+ targets: [{ sessionId: callerSessionId, afterSequence: initialSession.lastSequence }],
5035
+ ownSessionId: callerSessionId,
5036
+ maxWaitMs: (maxWaitSeconds ?? SESSION_WAIT_DEFAULT_SECONDS) * 1_000,
5037
+ targetEventTypes: ["session.command.finished"],
5038
+ targetEventMatches: (event) => {
5039
+ const payload = event.payload;
5040
+ return (
5041
+ payload !== null &&
5042
+ typeof payload === "object" &&
5043
+ !Array.isArray(payload) &&
5044
+ (payload as Record<string, unknown>).commandId === commandId
5045
+ );
5046
+ },
5047
+ signal: extra?.signal,
5048
+ source: {
5049
+ readTargetEvents: async (target) => {
5050
+ const page = await listSessionEventPage(
5051
+ deps.db,
5052
+ grant.workspaceId,
5053
+ target.sessionId,
5054
+ {
5055
+ after: target.afterSequence,
5056
+ direction: "after",
5057
+ limit: SESSION_WAIT_EVENTS_PER_TARGET,
5058
+ payloadMode: "full",
5059
+ includeTypes: ["session.command.finished"],
5060
+ maxBytes: SESSION_EVENT_MCP_MAX_BYTES * 4,
5061
+ },
5062
+ );
5063
+ return { events: page.events, hasMore: page.hasMore };
5064
+ },
5065
+ readOwnPendingUpdateKinds: async () =>
5066
+ (
5067
+ await listOutstandingSessionSystemUpdates(
5068
+ deps.db,
5069
+ grant.workspaceId,
5070
+ callerSessionId,
5071
+ )
5072
+ ).map((update) => update.kind),
5073
+ subscribe: (targetSessionId, onEvents) =>
5074
+ deps.bus.subscribe(grant.workspaceId, targetSessionId, onEvents),
5075
+ },
5076
+ });
5077
+ }
5078
+
5079
+ command =
5080
+ (await getSessionBackgroundCommand(deps.db, {
5081
+ accountId: grant.accountId,
5082
+ workspaceId: grant.workspaceId,
5083
+ sessionId: callerSessionId,
5084
+ commandId,
5085
+ })) ?? command;
5086
+ const isTerminal = terminal();
5087
+ const ownPendingUpdateKinds = (
5088
+ await listOutstandingSessionSystemUpdates(deps.db, grant.workspaceId, callerSessionId)
5089
+ ).map((update) => update.kind);
5090
+ const ownPendingUpdates = ownPendingUpdateKinds.length;
5091
+ if (waitResult) {
5092
+ // A bounded wait may cross a host/session revocation boundary. The
5093
+ // durable command and pending-input reads above are not returned
5094
+ // until the target is authorized again at the response boundary.
5095
+ await authorizeFirstPartySession(deps, grant, callerSessionId, "session.events.read");
5096
+ }
5097
+ const timedOut =
5098
+ (waitResult?.timedOut ?? false) && !isTerminal && ownPendingUpdates === 0;
5099
+ return json({
5100
+ command,
5101
+ terminal: isTerminal,
5102
+ waitedMs: waitResult?.waitedMs ?? 0,
5103
+ timedOut,
5104
+ aborted: waitResult?.aborted ?? false,
5105
+ liveFanout: waitResult?.liveFanout ?? true,
5106
+ ownPendingUpdates,
5107
+ ownPendingUpdateKinds,
5108
+ ...(isTerminal
5109
+ ? {
5110
+ outputLocator: {
5111
+ eventType: "sandbox.command.output.delta",
5112
+ commandId,
5113
+ },
5114
+ }
5115
+ : {}),
5116
+ nextAction: isTerminal
5117
+ ? ownPendingUpdates > 0
5118
+ ? "End this turn to receive the queued command result or other machine input."
5119
+ : "The command is terminal; inspect output events only if more detail is needed."
5120
+ : ownPendingUpdates > 0
5121
+ ? "End this turn to receive pending machine input."
5122
+ : waitResult?.timedOut
5123
+ ? "Do not immediately repeat this short wait without new completion evidence; use wait_for_input for a long or uncertain wait."
5124
+ : "The wait was aborted; follow the newer session control or input.",
5125
+ });
5126
+ },
5127
+ );
5128
+ }
4812
5129
  }
4813
5130
 
4814
5131
  if (can("sessions:create") && sessionCreateVisible) {
@@ -6177,36 +6494,90 @@ export function decodeSessionDiscoveryCursor(value: string): SessionDiscoveryCur
6177
6494
  }
6178
6495
  }
6179
6496
 
6180
- function capSessionDiscoveryText(
6181
- value: string | null,
6182
- maxChars = SESSION_DISCOVERY_TEXT_CHARS,
6183
- originalChars?: number | null,
6497
+ export function capSessionDiscoveryCompactPage(
6498
+ page: Awaited<ReturnType<typeof listSessionDiscoverySummaries>>,
6499
+ options: { includeLastMessage?: boolean; includeRelatedWork?: boolean } = {},
6500
+ maxBytes = SESSION_DISCOVERY_PAGE_MAX_BYTES,
6184
6501
  ) {
6185
- if (value === null) {
6186
- return { text: value, truncated: false };
6187
- }
6188
- const projectedChars = Array.from(value);
6189
- const sourceChars = Math.max(projectedChars.length, originalChars ?? projectedChars.length);
6190
- if (sourceChars <= maxChars) {
6191
- return { text: value, truncated: false };
6502
+ let previewBytes = 0;
6503
+ const projected = page.sessions.map((session) => {
6504
+ const row = {
6505
+ ...compactSessionMcpListRow(session, options.includeRelatedWork),
6506
+ ...(options.includeLastMessage && session.queuedPromptCount > 0
6507
+ ? { queuedPromptCount: session.queuedPromptCount }
6508
+ : {}),
6509
+ };
6510
+ if (!options.includeLastMessage) return row;
6511
+ const preview = capSessionDiscoveryText(
6512
+ session.latestMessage?.preview ?? null,
6513
+ 600,
6514
+ session.latestMessage?.previewOriginalChars,
6515
+ );
6516
+ if (!session.latestMessage) return row;
6517
+ const bytes = Buffer.byteLength(preview.text ?? "", "utf8");
6518
+ const omitted = previewBytes + bytes > SESSION_DISCOVERY_PREVIEW_MAX_BYTES;
6519
+ if (!omitted) previewBytes += bytes;
6520
+ return {
6521
+ ...row,
6522
+ latestMessage: {
6523
+ type: session.latestMessage.type,
6524
+ ...(omitted
6525
+ ? {
6526
+ previewOmitted: true,
6527
+ previewOmissionReason: SESSION_DISCOVERY_PREVIEW_OMISSION_REASON,
6528
+ previewDrillDownTool: SESSION_DISCOVERY_PREVIEW_DRILL_DOWN_TOOL,
6529
+ previewDrillDownInput: sessionDiscoveryPreviewDrillDownInput(
6530
+ session.id,
6531
+ session.latestMessage.type,
6532
+ ),
6533
+ }
6534
+ : {
6535
+ preview: preview.text,
6536
+ ...(preview.truncated ? { previewTruncated: true } : {}),
6537
+ }),
6538
+ },
6539
+ };
6540
+ });
6541
+ let kept = projected;
6542
+ const build = () => {
6543
+ const dropped = kept.length < projected.length;
6544
+ const last = page.sessions[kept.length - 1];
6545
+ const cursor =
6546
+ dropped && last
6547
+ ? {
6548
+ orderBy: page.orderBy,
6549
+ sortRank: last.sortRank,
6550
+ sortRevision: last.sortRevision,
6551
+ sortAt: last.sortAt,
6552
+ id: last.id,
6553
+ snapshotAt: page.snapshotAt,
6554
+ snapshotRevision: page.snapshotRevision,
6555
+ updatedAfter: page.updatedAfter,
6556
+ filterHash: page.filterHash,
6557
+ }
6558
+ : page.nextCursor;
6559
+ return {
6560
+ sessions: kept,
6561
+ total: page.total,
6562
+ nextCursor: cursor ? encodeSessionDiscoveryCursor(cursor) : null,
6563
+ ...(page.orderBy === "updatedAt" ? { updatedThrough: page.updatedThrough } : {}),
6564
+ ...(dropped
6565
+ ? {
6566
+ responseTruncated: true,
6567
+ truncationReason: `response exceeded ${maxBytes} bytes; continue with nextCursor`,
6568
+ }
6569
+ : {}),
6570
+ };
6571
+ };
6572
+ let result = build();
6573
+ while (Buffer.byteLength(JSON.stringify(result, null, 2), "utf8") > maxBytes && kept.length > 1) {
6574
+ kept = kept.slice(0, -1);
6575
+ result = build();
6192
6576
  }
6193
-
6194
- // The database supplies only a bounded prefix plus the original character
6195
- // count. Iterate to a stable marker width so the reported omission includes
6196
- // the characters replaced by the marker itself.
6197
- let bodyChars = maxChars;
6198
- let marker = "";
6199
- for (let attempt = 0; attempt < 4; attempt += 1) {
6200
- const omittedChars = Math.max(0, sourceChars - bodyChars);
6201
- marker = `…[${omittedChars} chars truncated]…`;
6202
- const nextBodyChars = Math.max(0, maxChars - Array.from(marker).length);
6203
- if (nextBodyChars === bodyChars) break;
6204
- bodyChars = nextBodyChars;
6577
+ if (Buffer.byteLength(JSON.stringify(result, null, 2), "utf8") > maxBytes) {
6578
+ throw new RangeError(`sessions_list compact metadata exceeds its ${maxBytes}-byte envelope`);
6205
6579
  }
6206
- return {
6207
- text: `${projectedChars.slice(0, bodyChars).join("")}${marker}`,
6208
- truncated: true,
6209
- };
6580
+ return result;
6210
6581
  }
6211
6582
 
6212
6583
  export function capSessionDiscoveryPage(