@opengeni/api-router 2.10.0-canary.0 → 2.10.0-canary.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/mcp/server.ts CHANGED
@@ -1,4 +1,9 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
+ import {
3
+ prepareWorkspaceArtifactUpload,
4
+ prepareWorkspaceArtifactPublication,
5
+ workspaceArtifactDownloads,
6
+ } from "../site-uploads";
2
7
  import {
3
8
  CreateScheduledTaskRequest,
4
9
  boundSessionMcpText as capSessionDiscoveryText,
@@ -169,7 +174,7 @@ import {
169
174
  searchCapabilityCatalogItems,
170
175
  type ResolvedSessionAuthorization,
171
176
  } from "@opengeni/core";
172
- import { recordWorkspaceUsage, requireLimit } from "@opengeni/core";
177
+ import { recordWorkspaceUsage, requireLimit, workflowIdForSession } from "@opengeni/core";
173
178
  import type { ApiRouteDeps } from "@opengeni/core";
174
179
  import {
175
180
  githubBindingStatus,
@@ -245,7 +250,15 @@ import {
245
250
  type FleetServices,
246
251
  type RunOnOp,
247
252
  } from "@opengeni/core";
248
- import { getSessionBackgroundCommand } from "@opengeni/db/session-background-commands";
253
+ import {
254
+ readSessionBackgroundCommandOutput,
255
+ COMMAND_OUTPUT_MAX_BYTES,
256
+ } from "@opengeni/db/session-background-commands";
257
+ import {
258
+ readCommandWithWait,
259
+ COMMAND_READ_MAX_WAIT_SECONDS,
260
+ COMMAND_WAIT_DEFAULT_SECONDS,
261
+ } from "./command-read";
249
262
  import {
250
263
  boundSessionEventCompactResult,
251
264
  boundSessionEventMcpPage,
@@ -293,10 +306,6 @@ import { registerRememberTools } from "./remember";
293
306
  import { mintSandboxCodemodeToken } from "@opengeni/runtime/sandbox";
294
307
  import { deleteScheduledTaskWithDurableCleanup } from "../scheduled-task-deletion";
295
308
  import { observeWorkDiscovery, summarizeWorkDiscoveryRows } from "../work-discovery-observability";
296
- import {
297
- prepareWorkspaceArtifactContent,
298
- readWorkspaceArtifactContent,
299
- } from "../workspace-artifact-content";
300
309
 
301
310
  export type McpServerOptions = {
302
311
  // Origin of the HTTP request that reached the MCP route. Browser-oriented
@@ -447,6 +456,7 @@ const FIRST_PARTY_TOOL_AUTHORIZATION = {
447
456
  wait_for_input: { sessionRequired: true, allOf: ["sessions:control"] },
448
457
  goal_complete: { sessionRequired: true, allOf: ["goals:manage"] },
449
458
  goal_pause: { sessionRequired: true, allOf: ["goals:manage"] },
459
+ goal_resume: { sessionRequired: true, allOf: ["goals:manage"] },
450
460
  memory_search: { sessionRequired: true, allOf: ["documents:search"] },
451
461
  memory_save: { sessionRequired: true, allOf: ["documents:search"] },
452
462
  memory_correct: { sessionRequired: true, allOf: ["documents:search"] },
@@ -519,6 +529,7 @@ const FIRST_PARTY_TOOL_AUTHORIZATION = {
519
529
  // self target, so the tool exists only for session-scoped grants.
520
530
  session_wait: { sessionRequired: true, allOf: ["sessions:read"] },
521
531
  command_wait: { sessionRequired: true, allOf: ["sessions:read"] },
532
+ command_read: { sessionRequired: true, allOf: ["sessions:read"] },
522
533
  session_create: { allOf: ["sessions:create"] },
523
534
  session_send_message: { allOf: ["sessions:control"] },
524
535
  session_pause: { allOf: ["sessions:control"] },
@@ -633,8 +644,9 @@ const FIRST_PARTY_TOOL_AUTHORIZATION = {
633
644
  atlassian_sources_list: { allOf: ["connections:read"] },
634
645
  atlassian_search: { allOf: ["connections:read"] },
635
646
  atlassian_get: { allOf: ["connections:read"] },
636
- artifacts_list: { sessionRequired: true, allOf: ["artifacts:read"] },
647
+ artifacts_list: { allOf: ["artifacts:read"] },
637
648
  artifacts_get_source: { sessionRequired: true, allOf: ["artifacts:read"] },
649
+ artifacts_prepare_upload: { sessionRequired: true, allOf: ["artifacts:publish"] },
638
650
  artifacts_create: { sessionRequired: true, allOf: ["artifacts:publish"] },
639
651
  artifacts_publish: { sessionRequired: true, allOf: ["artifacts:publish"] },
640
652
  artifacts_rollback: { sessionRequired: true, allOf: ["artifacts:publish"] },
@@ -845,6 +857,24 @@ export function buildOpenGeniMcpServer(
845
857
  options.workspaceMemoryPromptMode ?? "retrieval_only",
846
858
  );
847
859
  }
860
+ server.registerTool(
861
+ "artifacts_list",
862
+ {
863
+ description:
864
+ "List the generic published artifacts in this workspace and their current versions.",
865
+ inputSchema: {},
866
+ },
867
+ async () => {
868
+ if (sessionId !== null && exactAgentAttemptClaims(grant) !== null) {
869
+ await authorizeFirstPartySession(deps, grant, sessionId, "session.first_party_mcp.call");
870
+ }
871
+ return json(
872
+ redactWorkspaceArtifactListProvenance(
873
+ await listWorkspaceArtifacts(deps.db, grant.workspaceId),
874
+ ),
875
+ );
876
+ },
877
+ );
848
878
  if (sessionId !== null && exactAgentAttemptClaims(grant) !== null) {
849
879
  registerPreferenceRegistryTools(server, deps, grant, json);
850
880
  registerTaskNoteTools(server, deps, grant, sessionId, json);
@@ -2764,7 +2794,7 @@ function registerGoalTools(
2764
2794
  "wait_for_input",
2765
2795
  {
2766
2796
  description:
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.",
2797
+ "End the current turn and wait out of turn for relevant session input. This is self-only and does not require a goal. After success, the production runtime ends the turn at the tool-batch boundary without another model step or final message. 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 the first absolute deadline for the turn, and repeated calls do not extend it. 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. Use goal_pause instead when the active goal itself should stop pending a human decision.",
2768
2798
  inputSchema: {
2769
2799
  reason: inputWaitReasonSchema,
2770
2800
  timeoutSeconds: z4
@@ -2811,7 +2841,7 @@ function registerGoalTools(
2811
2841
  operationId,
2812
2842
  replay,
2813
2843
  nextAction:
2814
- "End your turn now. Relevant input or the timeout deadline will start a new turn; the timeout does not cancel commands.",
2844
+ "The runtime yields this turn after the tool batch settles. Relevant input or the timeout deadline will start a new turn; the timeout does not cancel commands.",
2815
2845
  });
2816
2846
  },
2817
2847
  );
@@ -2917,6 +2947,61 @@ function registerGoalTools(
2917
2947
  );
2918
2948
  },
2919
2949
  );
2950
+ server.registerTool(
2951
+ "goal_resume",
2952
+ {
2953
+ description:
2954
+ "Resume this session's paused goal regardless of who paused it or why. Already active is a successful no-op. Preserves the objective and resets continuation counters.",
2955
+ inputSchema: {},
2956
+ },
2957
+ async () => {
2958
+ await authorizeFirstPartySession(deps, grant, sessionId, "session.goal.write");
2959
+ await requireSession(deps.db, grant.workspaceId, sessionId);
2960
+ const existing = await getSessionGoal(deps.db, grant.workspaceId, sessionId);
2961
+ if (!existing) {
2962
+ throw new Error("this session has no goal; use goal_set first");
2963
+ }
2964
+ const { goal, events, workflowWakeRevision } = await setSessionGoalStatusWithEvent(
2965
+ deps.db,
2966
+ grant.workspaceId,
2967
+ sessionId,
2968
+ {
2969
+ status: "active",
2970
+ event: { type: "goal.resumed", actor: "agent" },
2971
+ },
2972
+ );
2973
+ const changed = events.length > 0;
2974
+ if (events.length > 0) {
2975
+ await deps.bus.publish(grant.workspaceId, sessionId, events);
2976
+ }
2977
+ if (workflowWakeRevision !== null) {
2978
+ await deps.workflowClient.wakeSessionWorkflow({
2979
+ accountId: grant.accountId,
2980
+ workspaceId: grant.workspaceId,
2981
+ sessionId,
2982
+ workflowId: workflowIdForSession(sessionId),
2983
+ wakeRevision: workflowWakeRevision,
2984
+ });
2985
+ }
2986
+ return json(
2987
+ mcpMutationReceipt({
2988
+ operation: "goal_resume",
2989
+ committed: true,
2990
+ outcome: changed ? "updated" : "unchanged",
2991
+ changed,
2992
+ resource: {
2993
+ type: "session_goal",
2994
+ id: goal.id,
2995
+ version: goal.version,
2996
+ state: goal.status,
2997
+ },
2998
+ timestamp: goal.updatedAt,
2999
+ idempotency: { status: "not_supported" },
3000
+ nextAction: { tool: "session_get", arguments: { sessionId } },
3001
+ }),
3002
+ );
3003
+ },
3004
+ );
2920
3005
  }
2921
3006
 
2922
3007
  type JsonResult = (value: unknown) => {
@@ -2961,13 +3046,15 @@ function registerWorkspaceArtifactTools(
2961
3046
  const mutation = async (response: WorkspaceArtifactMutationResponse) =>
2962
3047
  await projectWorkspaceArtifactMutationProvenance(response, canReadProvenanceSession);
2963
3048
  const prepare = (
2964
- html: string,
3049
+ html: string | undefined,
2965
3050
  source?: { entrypoint: string; files: Array<{ path: string; content: string }> },
2966
3051
  requestedTools?: Array<{ serverId: string; toolName: string }>,
3052
+ uploadId?: string,
2967
3053
  ) => {
2968
3054
  if (!deps.objectStorage) throw new Error("Object storage is not configured");
2969
- return prepareWorkspaceArtifactContent(deps.objectStorage, grant.workspaceId, {
2970
- html,
3055
+ return prepareWorkspaceArtifactPublication(deps, grant, {
3056
+ ...(html === undefined ? {} : { html }),
3057
+ ...(uploadId ? { uploadId } : {}),
2971
3058
  ...(source ? { source } : {}),
2972
3059
  ...(requestedTools ? { requestedTools } : {}),
2973
3060
  });
@@ -2998,19 +3085,15 @@ function registerWorkspaceArtifactTools(
2998
3085
  };
2999
3086
 
3000
3087
  server.registerTool(
3001
- "artifacts_list",
3088
+ "artifacts_prepare_upload",
3002
3089
  {
3003
3090
  description:
3004
- "List the generic published artifacts in this workspace and their current versions.",
3091
+ "Get signed upload URLs for a Site's HTML and optional source JSON. Upload with curl or fetch, then pass uploadId to artifacts_create/artifacts_publish. No hashes or sizes needed. Source upload may be omitted for HTML-only Sites.",
3005
3092
  inputSchema: {},
3006
3093
  },
3007
3094
  async () => {
3008
3095
  await authorize();
3009
- return json(
3010
- redactWorkspaceArtifactListProvenance(
3011
- await listWorkspaceArtifacts(deps.db, grant.workspaceId),
3012
- ),
3013
- );
3096
+ return json(await prepareWorkspaceArtifactUpload(deps, grant));
3014
3097
  },
3015
3098
  );
3016
3099
 
@@ -3018,7 +3101,7 @@ function registerWorkspaceArtifactTools(
3018
3101
  "artifacts_get_source",
3019
3102
  {
3020
3103
  description:
3021
- "Read an artifact's metadata and exact HTML source. Omit versionId for the current version.",
3104
+ "Get artifact metadata, version id, and signed download URLs for HTML and optional source JSON. Download with curl or Bun. If source is null, edit the HTML instead. Omit versionId for the current version.",
3022
3105
  inputSchema: {
3023
3106
  artifactId: z4.string().uuid(),
3024
3107
  versionId: z4.string().uuid().optional(),
@@ -3043,11 +3126,11 @@ function registerWorkspaceArtifactTools(
3043
3126
  projectWorkspaceArtifactDetailProvenance(rawDetail, canReadSourceSession),
3044
3127
  projectWorkspaceArtifactVersionProvenance(ref.version, canReadSourceSession),
3045
3128
  ]);
3046
- const content = await readWorkspaceArtifactContent(deps.objectStorage, ref);
3129
+ const downloads = await workspaceArtifactDownloads(deps.objectStorage, ref, "sandbox");
3047
3130
  return json({
3048
3131
  detail,
3049
3132
  version: projectedVersion,
3050
- ...content,
3133
+ downloads,
3051
3134
  });
3052
3135
  },
3053
3136
  );
@@ -3065,7 +3148,8 @@ function registerWorkspaceArtifactTools(
3065
3148
  .regex(/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/)
3066
3149
  .max(96)
3067
3150
  .optional(),
3068
- html: z4.string().min(1).max(WORKSPACE_ARTIFACT_HTML_MAX_UTF8_BYTES),
3151
+ html: z4.string().min(1).max(WORKSPACE_ARTIFACT_HTML_MAX_UTF8_BYTES).optional(),
3152
+ uploadId: z4.string().uuid().optional(),
3069
3153
  source: z4
3070
3154
  .object({
3071
3155
  entrypoint: z4.string().min(1).max(256),
@@ -3092,7 +3176,16 @@ function registerWorkspaceArtifactTools(
3092
3176
  idempotencyKey: z4.string().min(1).max(200),
3093
3177
  },
3094
3178
  },
3095
- async ({ title, description, slug, html, source, requestedTools, idempotencyKey }) => {
3179
+ async ({
3180
+ title,
3181
+ description,
3182
+ slug,
3183
+ html,
3184
+ source,
3185
+ requestedTools,
3186
+ idempotencyKey,
3187
+ uploadId,
3188
+ }) => {
3096
3189
  await authorize();
3097
3190
  const artifactId = crypto.randomUUID();
3098
3191
  const slugBase = slug ?? (normalizeWorkspaceArtifactSlug(title) || "artifact");
@@ -3105,7 +3198,7 @@ function registerWorkspaceArtifactTools(
3105
3198
  requestedSlug: slug ?? null,
3106
3199
  title,
3107
3200
  description: description ?? null,
3108
- ...prepare(html, source, requestedTools),
3201
+ ...(await prepare(html, source, requestedTools, uploadId)),
3109
3202
  ...provenance(idempotencyKey, "artifacts_create"),
3110
3203
  }),
3111
3204
  ),
@@ -3123,7 +3216,8 @@ function registerWorkspaceArtifactTools(
3123
3216
  expectedCurrentVersionId: z4.string().uuid(),
3124
3217
  title: z4.string().min(1).max(120).optional(),
3125
3218
  description: z4.string().max(2000).nullable().optional(),
3126
- html: z4.string().min(1).max(WORKSPACE_ARTIFACT_HTML_MAX_UTF8_BYTES),
3219
+ html: z4.string().min(1).max(WORKSPACE_ARTIFACT_HTML_MAX_UTF8_BYTES).optional(),
3220
+ uploadId: z4.string().uuid().optional(),
3127
3221
  source: z4
3128
3222
  .object({
3129
3223
  entrypoint: z4.string().min(1).max(256),
@@ -3156,6 +3250,7 @@ function registerWorkspaceArtifactTools(
3156
3250
  title,
3157
3251
  description,
3158
3252
  html,
3253
+ uploadId,
3159
3254
  source,
3160
3255
  requestedTools,
3161
3256
  idempotencyKey,
@@ -3168,7 +3263,7 @@ function registerWorkspaceArtifactTools(
3168
3263
  expectedCurrentVersionId,
3169
3264
  ...(title !== undefined ? { title } : {}),
3170
3265
  ...(description !== undefined ? { description } : {}),
3171
- ...prepare(html, source, requestedTools),
3266
+ ...(await prepare(html, source, requestedTools, uploadId)),
3172
3267
  ...provenance(idempotencyKey, "artifacts_publish"),
3173
3268
  }),
3174
3269
  ),
@@ -4747,13 +4842,25 @@ function registerWorkspaceOrchestrationTools(
4747
4842
  "session_get",
4748
4843
  {
4749
4844
  description:
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.",
4845
+ "Get an authorized session. Omit sessionId to read only the authenticated current agent session (a child reads itself, never its parent or root); sessionless/operator callers must provide an explicit ID. Both forms retain live-attempt and target authorization checks. 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 snapshot lastSequence. Goal completion is not a terminal child result: join with session_wait waitFor=completion from the last consumed event cursor (0 if none), not this snapshot lastSequence, which may already include an unread completion. For an already-settled child, retrieve its result-bearing completion with session_events or join from the last consumed cursor. 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. Self reads inspect session state, not conversation history, which is supplied directly. Text loss is explicit; REST/UI defaults are unchanged.",
4751
4846
  inputSchema: {
4752
- sessionId: z4.string().uuid(),
4847
+ sessionId: z4
4848
+ .string()
4849
+ .uuid()
4850
+ .optional()
4851
+ .describe(
4852
+ "Omit only to read the authenticated current agent session, never its parent or root. Sessionless callers must provide an explicit ID.",
4853
+ ),
4753
4854
  detail: z4.enum(["compact", "full"]).optional(),
4754
4855
  },
4755
4856
  },
4756
- async ({ sessionId, detail }) => {
4857
+ async ({ sessionId: requestedSessionId, detail }) => {
4858
+ const sessionId = requestedSessionId ?? exactAgentAttemptClaims(grant)?.sessionId;
4859
+ if (sessionId === undefined) {
4860
+ throw new Error(
4861
+ "session_get requires an explicit sessionId without authenticated current agent session context",
4862
+ );
4863
+ }
4757
4864
  const authorization = await authorizeFirstPartySession(
4758
4865
  deps,
4759
4866
  grant,
@@ -4793,9 +4900,14 @@ function registerWorkspaceOrchestrationTools(
4793
4900
  "session_events",
4794
4901
  {
4795
4902
  description:
4796
- "Read a compact semantic tail only when session_get status is insufficient. With no cursor, this returns the newest matching events and excludes raw message/reasoning/command/PTY deltas. Monitoring mode also omits a human/API user.message whose turn was never claimed (still queued, or deleted/edited/cancelled before any claim; waiting work is represented by sessions_list queuedPromptCount); the row appears at its own sequence once the turn is claimed. Use `latest` as an exclusive lookup for the authoritative newest durable sequence in exactly one semantic class; `receipt` is the concise alias for `tool_receipt`, and latest cannot be combined with type or class filters. Add `resultMode=compact` to latest for one bounded result-bearing completion/checkpoint/receipt without another inference. Use nextBefore to page older or explicit after/nextAfter to page forward. Type/class filters run in the RLS-scoped database query. payloadMode none|summary|full controls retained audit payload projection, but every model result is independently byte-capped with explicit truncation and exact covered sequence bounds. Exact retained forensic payloads, including unclaimed prompt rows, require mode=forensic or the access-controlled REST/SDK events API with mode=forensic&payloadMode=full; generic source bytes never retained by the audit boundary remain unavailable.",
4903
+ "Read session history. Default view=conversation returns roughly ten complete user/assistant messages, including completed commentary, in a 16 KiB envelope; no token deltas or execution records. Prefer fewer complete messages; a single oversized message has fragment offsets and a lossless nextCursor continuation over retained source text, including large legacy rows. Fragment unit is codepoint for plain text or utf16 for codec text; pass the opaque v2 cursor unchanged (v1 cursors must restart). Pass cursor=nextCursor with sessionId, omitting other selectors; it binds the view, detail and direction. after/nextAfter and before/nextBefore only change position, never view or detail; use nextCursor when present to avoid skipping a message fragment. view=results returns final turn answers and actionable outcomes without duplicate message-completion text. view=tools returns compact call/result identities; includeArguments/includeOutput opt into one text or JSON-encoded value, and callId selects an exact call (sparse scans can return an empty advancing page). sourceExact=false and sourceOmitted identify oversized structured values that were omitted, never partial JSON presented as complete; scalar text remains resumable. Conversation/results/tools omit never-claimed human/API prompts and stale duplicate events. view=debug exposes the existing authorized audit query with explicit type/class filters, mode=monitoring|forensic and payloadMode=none|summary|full; raw deltas and never-claimed prompts require mode=forensic. Explicit legacy audit selectors remain supported without view. latest is an exclusive semantic-class lookup; resultMode=compact requires latest. No read observes commands or changes append-only history. REST behavior is unchanged.",
4797
4904
  inputSchema: {
4798
4905
  sessionId: z4.string().uuid(),
4906
+ view: z4.enum(["conversation", "results", "tools", "debug"]).optional(),
4907
+ cursor: z4.string().max(4096).optional(),
4908
+ callId: z4.string().max(512).optional(),
4909
+ includeArguments: z4.boolean().optional(),
4910
+ includeOutput: z4.boolean().optional(),
4799
4911
  after: z4.number().int().nonnegative().optional(),
4800
4912
  before: z4.number().int().positive().optional(),
4801
4913
  limit: z4.number().int().positive().optional(),
@@ -4803,8 +4915,34 @@ function registerWorkspaceOrchestrationTools(
4803
4915
  mode: z4.enum(SessionEventReadMode.options).optional(),
4804
4916
  payloadMode: z4.enum(SessionEventPayloadMode.options).optional(),
4805
4917
  resultMode: z4.enum(SessionEventResultMode.options).optional(),
4806
- includeTypes: z4.array(z4.enum(SessionEventType.options)).max(100).optional(),
4807
- excludeTypes: z4.array(z4.enum(SessionEventType.options)).max(100).optional(),
4918
+ includeTypes: z4
4919
+ .array(
4920
+ z4
4921
+ .string()
4922
+ .refine(
4923
+ (value) => SessionEventType.safeParse(value).success,
4924
+ "Unknown session event type",
4925
+ ),
4926
+ )
4927
+ .max(100)
4928
+ .describe(
4929
+ "Debug audit event types, e.g. turn.completed, user.message, agent.message.completed, agent.toolCall.output. Validated against the canonical event-type registry.",
4930
+ )
4931
+ .optional(),
4932
+ excludeTypes: z4
4933
+ .array(
4934
+ z4
4935
+ .string()
4936
+ .refine(
4937
+ (value) => SessionEventType.safeParse(value).success,
4938
+ "Unknown session event type",
4939
+ ),
4940
+ )
4941
+ .max(100)
4942
+ .describe(
4943
+ "Debug audit event types to exclude; validated against the canonical event-type registry.",
4944
+ )
4945
+ .optional(),
4808
4946
  includeClasses: z4
4809
4947
  .array(z4.enum(SessionEventSemanticClass.options))
4810
4948
  .max(SessionEventSemanticClass.options.length)
@@ -4818,6 +4956,11 @@ function registerWorkspaceOrchestrationTools(
4818
4956
  },
4819
4957
  async ({
4820
4958
  sessionId,
4959
+ view,
4960
+ cursor,
4961
+ callId,
4962
+ includeArguments,
4963
+ includeOutput,
4821
4964
  after,
4822
4965
  before,
4823
4966
  limit,
@@ -4825,19 +4968,75 @@ function registerWorkspaceOrchestrationTools(
4825
4968
  mode: requestedMode,
4826
4969
  payloadMode: requestedPayloadMode,
4827
4970
  resultMode: requestedResultMode,
4828
- includeTypes,
4829
- excludeTypes,
4971
+ includeTypes: requestedIncludeTypes,
4972
+ excludeTypes: requestedExcludeTypes,
4830
4973
  includeClasses,
4831
4974
  excludeClasses,
4832
4975
  latest,
4833
4976
  }) => {
4834
4977
  await authorizeFirstPartySession(deps, grant, sessionId, "session.events.read");
4978
+ // Keep the model schema compact without weakening either MCP validation
4979
+ // or direct adapter calls: the canonical registry owns accepted types.
4980
+ const includeTypes = requestedIncludeTypes?.map((type) => SessionEventType.parse(type));
4981
+ const excludeTypes = requestedExcludeTypes?.map((type) => SessionEventType.parse(type));
4835
4982
  const latestClass =
4836
4983
  latest === undefined ? undefined : sessionEventLatestClassToSemanticClass(latest);
4837
4984
  if (requestedResultMode === "compact" && latestClass === undefined) {
4838
4985
  throw new Error("resultMode=compact requires latest");
4839
4986
  }
4840
4987
  await requireSession(deps.db, grant.workspaceId, sessionId);
4988
+ const auditRequested = [
4989
+ requestedMode,
4990
+ requestedPayloadMode,
4991
+ requestedResultMode,
4992
+ includeTypes,
4993
+ excludeTypes,
4994
+ includeClasses,
4995
+ excludeClasses,
4996
+ latest,
4997
+ ].some((value) => value !== undefined);
4998
+ if (((view !== undefined && view !== "debug") || cursor !== undefined) && auditRequested) {
4999
+ throw new Error(
5000
+ "Audit selectors require view=debug and cannot change a conversation cursor",
5001
+ );
5002
+ }
5003
+ if (view !== "debug" && !auditRequested) {
5004
+ const { readSessionEventView } = await import("./session-event-view");
5005
+ const { listSessionEventSlices } = await import("@opengeni/db/session-event-slices");
5006
+ return json(
5007
+ await readSessionEventView(
5008
+ {
5009
+ sessionId,
5010
+ view,
5011
+ cursor,
5012
+ callId,
5013
+ includeArguments,
5014
+ includeOutput,
5015
+ after,
5016
+ before,
5017
+ direction: requestedDirection,
5018
+ limit,
5019
+ },
5020
+ (options) =>
5021
+ listSessionEventSlices(
5022
+ deps.db,
5023
+ grant.workspaceId,
5024
+ sessionId,
5025
+ options,
5026
+ (legacyOptions) =>
5027
+ listSessionEventPage(deps.db, grant.workspaceId, sessionId, legacyOptions),
5028
+ ),
5029
+ ),
5030
+ );
5031
+ }
5032
+ if (
5033
+ cursor !== undefined ||
5034
+ callId !== undefined ||
5035
+ includeArguments !== undefined ||
5036
+ includeOutput !== undefined
5037
+ ) {
5038
+ throw new Error("cursor/callId/includeArguments/includeOutput require a non-debug view");
5039
+ }
4841
5040
  if (
4842
5041
  latest &&
4843
5042
  [includeTypes, excludeTypes, includeClasses, excludeClasses].some(
@@ -4846,7 +5045,7 @@ function registerWorkspaceOrchestrationTools(
4846
5045
  ) {
4847
5046
  throw new Error("latest cannot be combined with event filters");
4848
5047
  }
4849
- const mode = requestedMode ?? (after !== undefined ? "forensic" : "monitoring");
5048
+ const mode = requestedMode ?? "monitoring";
4850
5049
  const direction = latestClass
4851
5050
  ? "before"
4852
5051
  : (requestedDirection ??
@@ -4905,7 +5104,7 @@ function registerWorkspaceOrchestrationTools(
4905
5104
  server.registerTool(
4906
5105
  "session_wait",
4907
5106
  {
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.`,
5107
+ description: `Wait once for durable session changes, your pending machine input, or maxWaitSeconds (default ${SESSION_WAIT_DEFAULT_SECONDS}, max ${SESSION_WAIT_MAX_SECONDS}). Pass targets with sessionId and afterSequence: the last consumed cursor, or 0. Never substitute session_get.lastSequence, which may include an unread completion. waitFor=change returns on turn lifecycle, completed messages, terminal commands, blockers, goal facts, or session control. waitFor=completion joins a child result: only a result-bearing final turn or blocker qualifies, not commentary, goal.completed, background commands, maintenance turns, or continuation segments. Neither mode wakes on raw deltas or tool receipts. Each target contains up to ${SESSION_WAIT_EVENTS_PER_TARGET} bounded summaries, latestSequence (the next afterSequence), and hasMore. For omitted rows use session_events view=results after=latestSequence for final outcomes, or view=debug with explicit filters for diagnostics; the default conversation view does not contain execution records. Byte limits can leave events=[] with hasMore=true. ownPendingUpdates > 0 means input will arrive when your next turn is claimed: finish this turn, or use includeOwnPendingUpdates=false to keep waiting. timedOut=true means no matching change; liveFanout=false means the deadline re-check supplied durable truth without the live bus. Do not immediately repeat a timeout without new evidence. For long or uncertain waits call wait_for_input once and end the turn.`,
4909
5108
  inputSchema: {
4910
5109
  targets: z4
4911
5110
  .array(
@@ -5007,124 +5206,53 @@ function registerWorkspaceOrchestrationTools(
5007
5206
  );
5008
5207
 
5009
5208
  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(),
5209
+ for (const toolName of ["command_read", "command_wait"] as const) {
5210
+ server.registerTool(
5211
+ toolName,
5212
+ {
5213
+ description: `${toolName === "command_read" ? "Read retained output immediately, or briefly wait" : "Briefly wait using the same read operation as command_read"} for one command owned by the current session, across sandbox and Connected Machine providers. Returns bounded stdout/stderr chunks, durable state and exitCode, nextCursor and hasMore, plus explicit retention limitations. Pass nextCursor to continue; output remains readable after the process exits. A terminal read observes completion and suppresses only its still-pending inbox notification; running reads and already claimed history are unchanged. waitSeconds is at most 50; command_wait defaults to 45, command_read to 0. For long waits use wait_for_input. Neither tool sends stdin or cancels the command.`,
5214
+ inputSchema: {
5215
+ commandId: z4.string().uuid(),
5216
+ cursor: z4.string().max(128).optional(),
5217
+ waitSeconds: z4.number().int().min(0).max(COMMAND_READ_MAX_WAIT_SECONDS).optional(),
5218
+ maxOutputBytes: z4.number().int().min(4).max(COMMAND_OUTPUT_MAX_BYTES).optional(),
5219
+ },
5017
5220
  },
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.
5221
+ async ({ commandId, cursor, waitSeconds, maxOutputBytes }, extra) => {
5095
5222
  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",
5223
+ let initialRead = true;
5224
+ return json(
5225
+ await readCommandWithWait({
5226
+ commandId,
5227
+ waitSeconds:
5228
+ waitSeconds ?? (toolName === "command_wait" ? COMMAND_WAIT_DEFAULT_SECONDS : 0),
5229
+ ...(maxOutputBytes === undefined ? {} : { maxOutputBytes }),
5230
+ ...(extra?.signal === undefined ? {} : { signal: extra.signal }),
5231
+ read: async () => {
5232
+ if (!initialRead)
5233
+ await authorizeFirstPartySession(
5234
+ deps,
5235
+ grant,
5236
+ callerSessionId,
5237
+ "session.events.read",
5238
+ );
5239
+ initialRead = false;
5240
+ return await readSessionBackgroundCommandOutput(deps.db, {
5241
+ accountId: grant.accountId,
5242
+ workspaceId: grant.workspaceId,
5243
+ sessionId: callerSessionId,
5112
5244
  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
- );
5245
+ ...(cursor === undefined ? {} : { cursor }),
5246
+ ...(maxOutputBytes === undefined ? {} : { maxOutputBytes }),
5247
+ });
5248
+ },
5249
+ subscribe: (onEvents) =>
5250
+ deps.bus.subscribe(grant.workspaceId, callerSessionId, onEvents),
5251
+ }),
5252
+ );
5253
+ },
5254
+ );
5255
+ }
5128
5256
  }
5129
5257
  }
5130
5258
 
@@ -5142,7 +5270,12 @@ function registerWorkspaceOrchestrationTools(
5142
5270
  ),
5143
5271
  instructions: z4.string().min(1).max(SESSION_INSTRUCTIONS_MAX_CHARACTERS).optional(),
5144
5272
  goal: z4.unknown().optional(),
5145
- resources: z4.array(z4.unknown()).optional(),
5273
+ resources: z4
5274
+ .array(z4.unknown())
5275
+ .optional()
5276
+ .describe(
5277
+ "Omit to inherit parent repositories only. Files are never inherited automatically; explicitly include file resources to attach them. An empty array inherits no resources.",
5278
+ ),
5146
5279
  tools: z4.array(z4.unknown()).optional(),
5147
5280
  mcpServers: z4.array(z4.unknown()).optional(),
5148
5281
  variableSetId: z4.string().uuid().optional(),
@@ -6303,6 +6436,7 @@ const SESSION_DISCOVERY_PREVIEW_OMISSION_REASON = "aggregatePreviewBudget" as co
6303
6436
  const SESSION_DISCOVERY_PAGE_MAX_BYTES = 128_000;
6304
6437
  const SESSION_DISCOVERY_PREVIEW_DRILL_DOWN_TOOL = "session_events" as const;
6305
6438
  const SESSION_DISCOVERY_PREVIEW_DRILL_DOWN_BASE_INPUT = {
6439
+ view: "debug",
6306
6440
  direction: "before",
6307
6441
  limit: 1,
6308
6442
  mode: "monitoring",