@opengeni/api-router 0.30.3 → 2.2.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 (165) hide show
  1. package/dist/api-websocket.d.ts +18 -0
  2. package/dist/app.js +1 -1
  3. package/dist/auth/managed-auth.d.ts +6 -0
  4. package/dist/{chunk-WCXHI3FF.js → chunk-3TP54PPX.js} +26692 -15389
  5. package/dist/chunk-3TP54PPX.js.map +1 -0
  6. package/dist/codemode.d.ts +8 -1
  7. package/dist/company-brain-okf.d.ts +24 -0
  8. package/dist/connected-machine-computer-access.d.ts +11 -0
  9. package/dist/connection-authority-owner.d.ts +59 -0
  10. package/dist/connection-ownership.d.ts +47 -0
  11. package/dist/controller-data-plane.d.ts +16 -0
  12. package/dist/editable-artifact-websocket.d.ts +5 -15
  13. package/dist/editable-artifact-workspace-files.d.ts +1 -1
  14. package/dist/http/api-error.d.ts +10 -0
  15. package/dist/http/cors.d.ts +5 -0
  16. package/dist/http/interaction-control-error.d.ts +13 -0
  17. package/dist/http/sse.d.ts +2 -3
  18. package/dist/index.d.ts +5 -2
  19. package/dist/index.js +326 -82
  20. package/dist/index.js.map +1 -1
  21. package/dist/integrations/atlassian.d.ts +17 -0
  22. package/dist/integrations/fiken.d.ts +7 -2
  23. package/dist/integrations/google-drive.d.ts +18 -0
  24. package/dist/integrations/oauth-client.d.ts +22 -3
  25. package/dist/integrations/oauth-profiles.d.ts +195 -0
  26. package/dist/integrations/personal-github-repositories.d.ts +45 -0
  27. package/dist/integrations/personal-github.d.ts +40 -0
  28. package/dist/integrations/pr-review-provider.d.ts +22 -0
  29. package/dist/integrations/provider-oauth.d.ts +8 -0
  30. package/dist/integrations/slack-app-home.d.ts +24 -0
  31. package/dist/integrations/slack-bot.d.ts +157 -9
  32. package/dist/integrations/slack-interactions.d.ts +39 -5
  33. package/dist/integrations/slack-routing.d.ts +120 -0
  34. package/dist/integrations/social-oauth.d.ts +5 -0
  35. package/dist/interaction-frame-proxy.d.ts +68 -0
  36. package/dist/mcp/company-brain-governed-writes.d.ts +26 -0
  37. package/dist/mcp/company-profile-agent-admin.d.ts +44 -0
  38. package/dist/mcp/documents.d.ts +1 -0
  39. package/dist/mcp/remember.d.ts +28 -0
  40. package/dist/mcp/request-abort.d.ts +19 -0
  41. package/dist/mcp/scheduled-task-view.d.ts +4 -4
  42. package/dist/mcp/server.d.ts +15 -2
  43. package/dist/mcp/session-view.d.ts +4 -0
  44. package/dist/mcp/session-wait.d.ts +137 -0
  45. package/dist/routes/automations.d.ts +4 -0
  46. package/dist/routes/billing.d.ts +5 -0
  47. package/dist/routes/browser-sessions.d.ts +4 -1
  48. package/dist/routes/company-brain.d.ts +3 -0
  49. package/dist/routes/company-profile.d.ts +2 -1
  50. package/dist/routes/connection-authorities.d.ts +3 -0
  51. package/dist/routes/personal-github-git-broker.d.ts +29 -0
  52. package/dist/routes/personal-github.d.ts +3 -0
  53. package/dist/routes/pr-review.d.ts +3 -0
  54. package/dist/routes/sessions.d.ts +1 -0
  55. package/dist/routes/user-resource-authorities.d.ts +3 -0
  56. package/dist/routes/workspace-capture.d.ts +10 -2
  57. package/dist/routes/workspace-learning.d.ts +3 -0
  58. package/dist/routes/workspaces.d.ts +19 -0
  59. package/dist/sandbox/channel-a.d.ts +11 -1
  60. package/dist/sandbox/connection-authority.d.ts +3 -0
  61. package/dist/sandbox/enrollment.d.ts +2 -0
  62. package/dist/sandbox/machines.d.ts +2 -2
  63. package/dist/sandbox/metrics-ingestion.d.ts +10 -20
  64. package/dist/sandbox/viewer.d.ts +24 -1
  65. package/dist/sandbox-file-artifacts.d.ts +11 -0
  66. package/dist/scheduled-task-deletion.d.ts +14 -0
  67. package/dist/slack-reaction-files.d.ts +1 -1
  68. package/dist/temporal-schedule-cleanup.d.ts +1 -0
  69. package/package.json +20 -18
  70. package/src/api-websocket.ts +23 -0
  71. package/src/app.ts +161 -23
  72. package/src/auth/managed-auth.ts +25 -3
  73. package/src/codemode.ts +53 -23
  74. package/src/codex-realtime.ts +8 -2
  75. package/src/company-brain-okf.ts +340 -0
  76. package/src/connected-machine-computer-access.ts +33 -0
  77. package/src/connection-authority-owner.ts +61 -0
  78. package/src/connection-ownership.ts +180 -0
  79. package/src/controller-data-plane.ts +47 -0
  80. package/src/editable-artifact-native-kernel.ts +26 -15
  81. package/src/editable-artifact-office-import.ts +28 -3
  82. package/src/editable-artifact-production.ts +13 -7
  83. package/src/editable-artifact-websocket.ts +11 -18
  84. package/src/editable-artifact-workspace-files.ts +31 -6
  85. package/src/http/api-error.ts +28 -0
  86. package/src/http/auth.ts +4 -0
  87. package/src/http/cors.ts +35 -0
  88. package/src/http/interaction-control-error.ts +164 -0
  89. package/src/http/sse.ts +112 -66
  90. package/src/index.ts +46 -6
  91. package/src/integrations/atlassian.ts +146 -35
  92. package/src/integrations/fiken.ts +102 -22
  93. package/src/integrations/google-drive.ts +301 -92
  94. package/src/integrations/oauth-client.ts +272 -143
  95. package/src/integrations/oauth-profiles.ts +477 -0
  96. package/src/integrations/personal-github-repositories.ts +445 -0
  97. package/src/integrations/personal-github.ts +705 -0
  98. package/src/integrations/pr-review-provider.ts +246 -0
  99. package/src/integrations/provider-oauth.ts +121 -5
  100. package/src/integrations/slack-app-home.ts +300 -0
  101. package/src/integrations/slack-bot.ts +342 -57
  102. package/src/integrations/slack-interactions.ts +1898 -259
  103. package/src/integrations/slack-routing.ts +337 -0
  104. package/src/integrations/social-oauth.ts +25 -0
  105. package/src/interaction-frame-proxy.ts +409 -0
  106. package/src/mcp/company-brain-governed-writes.ts +262 -0
  107. package/src/mcp/company-profile-agent-admin.ts +205 -0
  108. package/src/mcp/documents.ts +37 -6
  109. package/src/mcp/files.ts +16 -1
  110. package/src/mcp/remember.ts +181 -0
  111. package/src/mcp/request-abort.ts +44 -0
  112. package/src/mcp/scheduled-task-view.ts +1 -0
  113. package/src/mcp/server.ts +719 -369
  114. package/src/mcp/session-view.ts +54 -0
  115. package/src/mcp/session-wait.ts +554 -0
  116. package/src/model-catalog.ts +20 -12
  117. package/src/routes/api-integrations.ts +4 -0
  118. package/src/routes/api-keys.ts +1 -0
  119. package/src/routes/automations.ts +534 -0
  120. package/src/routes/billing.ts +57 -1
  121. package/src/routes/browser-sessions.ts +329 -79
  122. package/src/routes/channels.ts +17 -1
  123. package/src/routes/codex.ts +50 -0
  124. package/src/routes/company-brain.ts +367 -0
  125. package/src/routes/company-profile.ts +1 -1
  126. package/src/routes/computer-sessions.ts +320 -93
  127. package/src/routes/connection-authorities.ts +139 -0
  128. package/src/routes/connections.ts +144 -34
  129. package/src/routes/documents.ts +353 -4
  130. package/src/routes/editable-artifacts.ts +11 -3
  131. package/src/routes/enrollments.ts +116 -26
  132. package/src/routes/environments.ts +127 -55
  133. package/src/routes/files.ts +72 -11
  134. package/src/routes/install.ts +82 -28
  135. package/src/routes/integration-facets.ts +29 -0
  136. package/src/routes/interaction-resources.ts +20 -2
  137. package/src/routes/machines.ts +275 -13
  138. package/src/routes/organization-memberships.ts +717 -28
  139. package/src/routes/packs.ts +4 -3
  140. package/src/routes/personal-github-git-broker.ts +785 -0
  141. package/src/routes/personal-github.ts +319 -0
  142. package/src/routes/pr-review.ts +531 -0
  143. package/src/routes/rigs.ts +104 -39
  144. package/src/routes/scheduled-tasks.ts +19 -21
  145. package/src/routes/sessions.ts +775 -38
  146. package/src/routes/social.ts +14 -2
  147. package/src/routes/supergrok.ts +23 -4
  148. package/src/routes/transcription-recordings.ts +8 -2
  149. package/src/routes/user-resource-authorities.ts +138 -0
  150. package/src/routes/workspace-artifacts.ts +4 -1
  151. package/src/routes/workspace-capture.ts +21 -0
  152. package/src/routes/workspace-learning.ts +228 -0
  153. package/src/routes/workspaces.ts +59 -6
  154. package/src/sandbox/auth-callout.ts +38 -12
  155. package/src/sandbox/channel-a.ts +242 -19
  156. package/src/sandbox/connection-authority.ts +3 -0
  157. package/src/sandbox/enrollment.ts +15 -3
  158. package/src/sandbox/machines.ts +186 -64
  159. package/src/sandbox/metrics-ingestion.ts +220 -58
  160. package/src/sandbox/viewer.ts +243 -19
  161. package/src/sandbox-file-artifacts.ts +329 -0
  162. package/src/scheduled-task-deletion.ts +127 -0
  163. package/src/slack-reaction-files.ts +16 -5
  164. package/src/temporal-schedule-cleanup.ts +13 -0
  165. package/dist/chunk-WCXHI3FF.js.map +0 -1
@@ -1,6 +1,9 @@
1
1
  import {
2
2
  AcknowledgeStreamRequest,
3
3
  ApplySessionGoalRevisionRequest,
4
+ ListSessionGoalRevisionsQuery,
5
+ RejectSessionGoalRevisionRequest,
6
+ RollbackSessionGoalRevisionRequest,
4
7
  ActivateCodexRealtimeConnectionRequest,
5
8
  AttachViewerRequest,
6
9
  BeginSessionRealtimeRequest,
@@ -19,6 +22,7 @@ import {
19
22
  FsMoveRequest,
20
23
  FsReadRequest,
21
24
  FsWriteRequest,
25
+ ForkSessionRequest,
22
26
  HumanInputRequestStatus,
23
27
  GitDiffRequest,
24
28
  GitReadBatchRequest,
@@ -30,6 +34,7 @@ import {
30
34
  PtyOpenRequest,
31
35
  PtyResizeRequest,
32
36
  PtyWriteRequest,
37
+ PublishSandboxFileArtifactRequest,
33
38
  RenewSessionRealtimeRequest,
34
39
  SyncSessionRealtimeLedgerRequest,
35
40
  SessionControlRequest,
@@ -45,14 +50,18 @@ import {
45
50
  compactSessionEventResult,
46
51
  sessionEventLatestClassToSemanticClass,
47
52
  SaveComposerDraftRequest,
53
+ SubmitComposerDraftRequest,
48
54
  SteerSessionQueueItemRequest,
49
55
  SteerSessionMessageRequest,
50
56
  TerminalExecRequest,
51
57
  UpdateSessionChannelRequest,
58
+ UpdateSessionAttentionRequest,
59
+ UpdateSessionArchiveRequest,
52
60
  UpdateSessionPinRequest,
53
61
  UpdateSessionGoalRequest,
54
62
  UpdateSessionMcpApprovalPolicyRequest,
55
63
  UpdateSessionRequest,
64
+ UpdateSessionVisibilityRequest,
56
65
  UpdateSessionToolPolicyRequest,
57
66
  ViewerHeartbeatRequest,
58
67
  WORKSPACE_CONTROL_ACTOR_MAX_BYTES,
@@ -73,15 +82,18 @@ import {
73
82
  } from "@opengeni/contracts";
74
83
  import { streamTokenDegraded } from "@opengeni/config";
75
84
  import {
85
+ recordAuditEvent,
76
86
  acceptSessionApprovalDecision,
77
87
  acceptSessionHumanInputResponse,
78
88
  clearSessionGoal,
79
89
  clearSessionContext,
80
90
  getOpenPtySession,
81
91
  getEnrollment,
92
+ getLiveEnrollmentConnection,
82
93
  getRetainedProcess,
83
94
  getSandbox,
84
95
  getSession,
96
+ deleteSessionTreeIfQuiescent,
85
97
  getSessionEvent,
86
98
  getSessionForSubject,
87
99
  getSessionGoal,
@@ -93,7 +105,9 @@ import {
93
105
  insertPtySession,
94
106
  listSessionEventPage,
95
107
  listSessionHumanInputRequests,
108
+ listSessionGoalRevisionPage,
96
109
  listSessionGoalRevisions,
110
+ rejectSessionGoalRevisionWithEvent,
97
111
  listSessionIdsInGroup,
98
112
  listSessionDiscoverySummaries,
99
113
  listSessionDiscoveryAncestorPaths,
@@ -108,13 +122,16 @@ import {
108
122
  withSessionCodexCapacityMutation,
109
123
  setSessionChannel,
110
124
  ChannelNotFoundError,
125
+ setSessionAttention,
126
+ setSessionArchive,
111
127
  setSessionPin,
128
+ SessionAttentionVersionConflictError,
129
+ SessionArchiveVersionConflictError,
112
130
  SessionPinVersionConflictError,
113
131
  SessionPinAccessError,
114
132
  SessionListAccessError,
115
133
  SessionListCursorError,
116
134
  SessionListCursorExpiredError,
117
- SessionListSnapshotLimitError,
118
135
  decodeSessionListCursor,
119
136
  revokeViewer,
120
137
  setSessionGoalStatusWithEvent,
@@ -133,6 +150,11 @@ import {
133
150
  SessionRealtimeConflictError,
134
151
  SessionToolPolicyVersionConflictError,
135
152
  SessionContextBusyError,
153
+ workspaceControlRequestLockTimeoutMs,
154
+ SessionTenancyAccessError,
155
+ SessionTenancyConflictError,
156
+ SessionTenancyInvalidRequestError,
157
+ SessionTenancyNotActivatedError,
136
158
  HumanInputResponseValidationError,
137
159
  latestWorkspaceCapture,
138
160
  sessionLatestWorkspaceCapture,
@@ -149,6 +171,11 @@ import {
149
171
  type SessionDiscoveryCursor,
150
172
  type SessionDiscoveryAncestor,
151
173
  } from "@opengeni/db";
174
+ import {
175
+ backgroundCommandActivityForSessions,
176
+ listSessionBackgroundCommands,
177
+ requestSessionBackgroundCommandCancellation,
178
+ } from "@opengeni/db/session-background-commands";
152
179
  import {
153
180
  appendAndPublishEvents,
154
181
  boundSessionEventHttpPage,
@@ -173,6 +200,7 @@ import {
173
200
  NatsControlRpc,
174
201
  negotiateCapabilities,
175
202
  negotiateSelfhostedCapabilities,
203
+ selectBackend,
176
204
  SelfhostedSession,
177
205
  } from "@opengeni/runtime/sandbox";
178
206
  import type { Context, Hono, MiddlewareHandler } from "hono";
@@ -181,6 +209,8 @@ import type { ContentfulStatusCode } from "hono/utils/http-status";
181
209
  import {
182
210
  hasPermission,
183
211
  requireAccessGrant,
212
+ requireAccessGrantAuthorization,
213
+ requireFreshAccessGrant,
184
214
  requirePermission,
185
215
  requireSessionAuthorization,
186
216
  requireSessionAuthorizationListScope,
@@ -188,6 +218,8 @@ import {
188
218
  SESSION_AUTHORIZATION_DEFAULT_REAUTHORIZE_MS,
189
219
  SessionAuthorizationDeniedError,
190
220
  SessionAuthorizationUnavailableError,
221
+ SessionTenancyManagedHumanRequiredError,
222
+ SessionTenancyPersistenceOutcomeUnknownError,
191
223
  type ResolvedSessionAuthorization,
192
224
  } from "@opengeni/core";
193
225
  import type { ApiRouteDeps } from "@opengeni/core";
@@ -207,12 +239,15 @@ import {
207
239
  import { buildSessionCodexRealtimeBroker, CodexRealtimeBrokerError } from "../codex-realtime";
208
240
  import {
209
241
  acceptSessionUserMessage,
242
+ acceptSessionUserMessageWithOutcome,
210
243
  controlHumanSessionWorkstream,
211
244
  createSessionForRequest,
212
245
  deleteHumanQueuePrompt,
213
246
  editHumanQueuePrompt,
214
247
  getActorNewSessionDraft,
248
+ getManagedHumanSessionCreateCapabilities,
215
249
  getHumanComposerDraft,
250
+ forkManagedHumanSession,
216
251
  moveHumanQueuePrompt,
217
252
  readSessionLineage,
218
253
  saveHumanComposerDraft,
@@ -221,6 +256,7 @@ import {
221
256
  sessionSpawnDenialEnvelope,
222
257
  steerHumanQueuePrompt,
223
258
  updateSessionMcpApprovalPolicy,
259
+ updateManagedHumanSessionVisibility,
224
260
  updateSessionToolPolicy,
225
261
  updateSessionTitle,
226
262
  workflowIdForSession,
@@ -236,11 +272,27 @@ import {
236
272
  serveWorkspaceCaptureFile,
237
273
  WorkspaceCaptureManifestCache,
238
274
  } from "./workspace-capture";
275
+ import { publishSandboxFileArtifact } from "../sandbox-file-artifacts";
276
+ import { ApiHttpError } from "../http/api-error";
239
277
 
240
278
  type SessionRouteDeps = ApiRouteDeps & Pick<ViewerServices, "establishSandboxSession">;
241
279
 
242
280
  const VIEWER_LIFECYCLE_PERMISSIONS = ["stream:view", "terminal:attach", "files:write"] as const;
243
281
 
282
+ /**
283
+ * Bounded responder kind recorded on a child's requires-action resolution
284
+ * notice for its parent: a key principal is `api`, a signed agent attempt is
285
+ * `agent_attempt`, and every other HTTP caller is a human. Never a subject id.
286
+ */
287
+ function childRequiresActionRespondedByKindForGrant(
288
+ grant: Pick<AccessGrant, "principalKind">,
289
+ ): "human" | "api" | "agent_attempt" | "system" {
290
+ if (grant.principalKind === "api_key" || grant.principalKind === "configured_key") return "api";
291
+ if (grant.principalKind === "agent_attempt") return "agent_attempt";
292
+ if (grant.principalKind === "service") return "system";
293
+ return "human";
294
+ }
295
+
244
296
  function requireViewerLifecyclePermission(grant: AccessGrant): void {
245
297
  if (
246
298
  VIEWER_LIFECYCLE_PERMISSIONS.some((permission) => hasPermission(grant.permissions, permission))
@@ -406,6 +458,16 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
406
458
  if (!operation) {
407
459
  throw sessionAuthorizationHttpError(new SessionAuthorizationUnavailableError());
408
460
  }
461
+ if (operation === "session.visibility.write" || operation === "session.fork.create") {
462
+ // These product mutations perform their target-free managed-cookie,
463
+ // permission, and activation gates inside core before resolving the
464
+ // target exactly once. Visibility changes remain owner-only; a shared
465
+ // source may be forked by any currently authorized workspace member.
466
+ // Running the generic middleware here would both create a pre-gate
467
+ // existence oracle and double-call the host.
468
+ await next();
469
+ return;
470
+ }
409
471
  const grant = await requireAccessGrant(c, deps, workspaceId);
410
472
  try {
411
473
  const authorization = await requireSessionAuthorization(deps, grant, {
@@ -434,9 +496,26 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
434
496
  : {}),
435
497
  };
436
498
 
499
+ app.get("/v1/workspaces/:workspaceId/session-tenancy/capabilities", async (c) => {
500
+ const workspaceId = c.req.param("workspaceId");
501
+ const authorization = await requireAccessGrantAuthorization(
502
+ c,
503
+ deps,
504
+ workspaceId,
505
+ "sessions:create",
506
+ );
507
+ return c.json(await getManagedHumanSessionCreateCapabilities(deps, authorization, workspaceId));
508
+ });
509
+
437
510
  app.post("/v1/workspaces/:workspaceId/sessions", async (c) => {
438
511
  const workspaceId = c.req.param("workspaceId");
439
- const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:create");
512
+ const authorization = await requireAccessGrantAuthorization(
513
+ c,
514
+ deps,
515
+ workspaceId,
516
+ "sessions:create",
517
+ );
518
+ const grant = authorization.grant;
440
519
  let payload: unknown;
441
520
  try {
442
521
  payload = await c.req.json();
@@ -451,7 +530,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
451
530
  }
452
531
  let session: Session;
453
532
  try {
454
- session = await createSessionForRequest(deps, grant, workspaceId, payload);
533
+ session = await createSessionForRequest(deps, grant, workspaceId, payload, authorization);
455
534
  } catch (error) {
456
535
  return sessionCreateErrorResponse(c, error);
457
536
  }
@@ -469,7 +548,13 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
469
548
 
470
549
  app.put("/v1/workspaces/:workspaceId/new-session-draft", async (c) => {
471
550
  const workspaceId = c.req.param("workspaceId");
472
- const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:create");
551
+ const authorization = await requireAccessGrantAuthorization(
552
+ c,
553
+ deps,
554
+ workspaceId,
555
+ "sessions:create",
556
+ );
557
+ const grant = authorization.grant;
473
558
  let payload: unknown;
474
559
  try {
475
560
  payload = await c.req.json();
@@ -489,6 +574,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
489
574
  grant,
490
575
  workspaceId,
491
576
  payload,
577
+ authorization.canonicalManagedHumanSession,
492
578
  ),
493
579
  );
494
580
  } catch (error) {
@@ -517,7 +603,13 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
517
603
 
518
604
  app.get("/v1/workspaces/:workspaceId/sessions", async (c) => {
519
605
  const workspaceId = c.req.param("workspaceId");
520
- const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:read");
606
+ const authorization = await requireAccessGrantAuthorization(
607
+ c,
608
+ deps,
609
+ workspaceId,
610
+ "sessions:read",
611
+ );
612
+ const grant = authorization.grant;
521
613
  let authorizationScope;
522
614
  try {
523
615
  authorizationScope = await requireSessionAuthorizationListScope(deps, grant, "http");
@@ -535,8 +627,13 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
535
627
  ...(query.cursor ? { cursor: query.cursor } : {}),
536
628
  ...(query.search ? { search: query.search } : {}),
537
629
  ...(query.pinsOnly ? { pinsOnly: true } : {}),
630
+ ...(query.archivedOnly ? { archivedOnly: true } : {}),
538
631
  ...(query.parentSessionId !== undefined ? { parentSessionId: query.parentSessionId } : {}),
539
632
  ...(authorizationScope ? { authorizationScope } : {}),
633
+ // A managed human's own personal workspace has no membership row, so
634
+ // the list's removal fence must fall back to the organization-membership
635
+ // pointer — for the canonical managed-cookie session that owns it only.
636
+ personalWorkspaceOwnerException: authorization.canonicalManagedHumanSession,
540
637
  });
541
638
  } catch (error) {
542
639
  if (error instanceof SessionListAccessError) {
@@ -552,10 +649,6 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
552
649
  if (error instanceof SessionListCursorError) {
553
650
  throw new HTTPException(400, { message: error.message });
554
651
  }
555
- if (error instanceof SessionListSnapshotLimitError) {
556
- c.header("Retry-After", "5");
557
- throw new HTTPException(429, { message: error.message });
558
- }
559
652
  throw error;
560
653
  }
561
654
  // The page body carries this fact directly. Preserve the historical array
@@ -563,12 +656,19 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
563
656
  // to raw HTTP consumers without changing that response shape.
564
657
  c.header("x-opengeni-pinned-truncated", page.pinnedTruncated === true ? "true" : "false");
565
658
  const policy = await loadEffectivePolicyContext(deps, workspaceId, grant.subjectId);
566
- const decorate = (session: Session): Session =>
567
- sessionWithEffectiveToolPolicy(
568
- session,
659
+ const commandActivity = await backgroundCommandActivityForSessions(db, {
660
+ accountId: grant.accountId,
661
+ workspaceId,
662
+ sessionIds: [...page.pinned, ...page.sessions].map((session) => session.id),
663
+ });
664
+ const decorate = (session: Session): Session => {
665
+ const activity = commandActivity.get(session.id);
666
+ return sessionWithEffectiveToolPolicy(
667
+ { ...session, ...(activity ? { backgroundCommandActivity: activity } : {}) },
569
668
  policy.workspaceServerIds,
570
669
  policy.workspaceDefaultServerIds,
571
670
  );
671
+ };
572
672
  if (pageView) {
573
673
  return c.json({
574
674
  ...page,
@@ -681,6 +781,99 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
681
781
  return c.json(await withEffectivePolicy(deps, workspaceId, grant.subjectId, session));
682
782
  });
683
783
 
784
+ app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/background-commands", async (c) => {
785
+ const workspaceId = c.req.param("workspaceId");
786
+ const sessionId = c.req.param("sessionId");
787
+ const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:read");
788
+ const commands = await listSessionBackgroundCommands(db, {
789
+ accountId: grant.accountId,
790
+ workspaceId,
791
+ sessionId,
792
+ });
793
+ return c.json({ commands });
794
+ });
795
+
796
+ app.delete(
797
+ "/v1/workspaces/:workspaceId/sessions/:sessionId/background-commands/:commandId",
798
+ async (c) => {
799
+ const workspaceId = c.req.param("workspaceId");
800
+ const sessionId = c.req.param("sessionId");
801
+ const commandId = c.req.param("commandId");
802
+ if (!z.string().uuid().safeParse(commandId).success) {
803
+ throw new HTTPException(404, { message: "background command not found" });
804
+ }
805
+ const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
806
+ const result = await requestSessionBackgroundCommandCancellation(db, {
807
+ accountId: grant.accountId,
808
+ workspaceId,
809
+ sessionId,
810
+ commandId,
811
+ subjectId: grant.subjectId,
812
+ });
813
+ if (!result.command) {
814
+ throw new HTTPException(404, { message: "background command not found" });
815
+ }
816
+ return c.json(result);
817
+ },
818
+ );
819
+
820
+ app.put("/v1/workspaces/:workspaceId/sessions/:sessionId/visibility", async (c) => {
821
+ const workspaceId = c.req.param("workspaceId");
822
+ const sessionId = c.req.param("sessionId");
823
+ const authorization = await requireAccessGrantAuthorization(c, deps, workspaceId);
824
+ const parsed = UpdateSessionVisibilityRequest.safeParse(await c.req.json().catch(() => null));
825
+ if (!parsed.success) {
826
+ throw new ApiHttpError(422, {
827
+ code: "validation_failed",
828
+ message: "Invalid session visibility request.",
829
+ retryable: false,
830
+ details: { fields: zodErrorFields(parsed.error) },
831
+ });
832
+ }
833
+ try {
834
+ return c.json(
835
+ await updateManagedHumanSessionVisibility(
836
+ deps,
837
+ authorization,
838
+ workspaceId,
839
+ sessionId,
840
+ parsed.data,
841
+ "http",
842
+ ),
843
+ );
844
+ } catch (error) {
845
+ throw sessionTenancyHttpError(error);
846
+ }
847
+ });
848
+
849
+ app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/forks", async (c) => {
850
+ const workspaceId = c.req.param("workspaceId");
851
+ const sessionId = c.req.param("sessionId");
852
+ const authorization = await requireAccessGrantAuthorization(c, deps, workspaceId);
853
+ const parsed = ForkSessionRequest.safeParse(await c.req.json().catch(() => null));
854
+ if (!parsed.success) {
855
+ throw new ApiHttpError(422, {
856
+ code: "validation_failed",
857
+ message: "Invalid session fork request.",
858
+ retryable: false,
859
+ details: { fields: zodErrorFields(parsed.error) },
860
+ });
861
+ }
862
+ try {
863
+ const response = await forkManagedHumanSession(
864
+ deps,
865
+ authorization,
866
+ workspaceId,
867
+ sessionId,
868
+ parsed.data,
869
+ "http",
870
+ );
871
+ return c.json(response, response.replay ? 200 : 201);
872
+ } catch (error) {
873
+ throw sessionTenancyHttpError(error);
874
+ }
875
+ });
876
+
684
877
  const publishRealtimeMutation = async (
685
878
  accountId: string,
686
879
  workspaceId: string,
@@ -1299,6 +1492,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
1299
1492
  realtimeId,
1300
1493
  ownerSubjectId: grant.subjectId,
1301
1494
  ...parsed.data,
1495
+ controlLockTimeoutMs: workspaceControlRequestLockTimeoutMs(),
1302
1496
  }),
1303
1497
  );
1304
1498
  await publishRealtimeMutation(grant.accountId, workspaceId, sessionId, result);
@@ -1315,7 +1509,13 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
1315
1509
  // (not session control) and returns 404 for a foreign/inaccessible session.
1316
1510
  app.put("/v1/workspaces/:workspaceId/sessions/:sessionId/pin", async (c) => {
1317
1511
  const workspaceId = c.req.param("workspaceId");
1318
- const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:read");
1512
+ const authorization = await requireAccessGrantAuthorization(
1513
+ c,
1514
+ deps,
1515
+ workspaceId,
1516
+ "sessions:read",
1517
+ );
1518
+ const grant = authorization.grant;
1319
1519
  const sessionId = c.req.param("sessionId");
1320
1520
  if (!z.string().uuid().safeParse(sessionId).success) {
1321
1521
  throw new HTTPException(404, { message: "session not found" });
@@ -1329,6 +1529,8 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
1329
1529
  workspaceId,
1330
1530
  subjectId: grant.subjectId,
1331
1531
  sessionId,
1532
+ // Same owner-only personal-workspace fallback as the list above.
1533
+ personalWorkspaceOwnerException: authorization.canonicalManagedHumanSession,
1332
1534
  ...parsed.data,
1333
1535
  });
1334
1536
  if (!session) {
@@ -1359,6 +1561,94 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
1359
1561
  }
1360
1562
  });
1361
1563
 
1564
+ // Personal follow-up state. Session reads stay side-effect free; clients use
1565
+ // this explicit mutation after a deliberate read/unread action. The managed
1566
+ // web console treats an exact foreground chat event frontier as that action,
1567
+ // while the actively-working label remains independent of acknowledgment.
1568
+ app.put("/v1/workspaces/:workspaceId/sessions/:sessionId/attention", async (c) => {
1569
+ const workspaceId = c.req.param("workspaceId");
1570
+ const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:read");
1571
+ const sessionId = c.req.param("sessionId");
1572
+ if (!z.string().uuid().safeParse(sessionId).success) {
1573
+ throw new HTTPException(404, { message: "session not found" });
1574
+ }
1575
+ const parsed = UpdateSessionAttentionRequest.safeParse(await c.req.json().catch(() => null));
1576
+ if (!parsed.success) {
1577
+ throw new HTTPException(400, { message: "invalid session attention request" });
1578
+ }
1579
+ try {
1580
+ const session = await setSessionAttention(db, {
1581
+ workspaceId,
1582
+ subjectId: grant.subjectId,
1583
+ sessionId,
1584
+ ...parsed.data,
1585
+ });
1586
+ if (!session) throw new HTTPException(404, { message: "session not found" });
1587
+ return c.json(
1588
+ await withEffectivePolicy(
1589
+ deps,
1590
+ workspaceId,
1591
+ grant.subjectId,
1592
+ projectSessionForRelatedAccess(session, relatedSessionAccessFor(c)),
1593
+ ),
1594
+ );
1595
+ } catch (error) {
1596
+ if (error instanceof SessionPinAccessError) {
1597
+ throw new HTTPException(403, { message: error.message });
1598
+ }
1599
+ if (error instanceof SessionAttentionVersionConflictError) {
1600
+ return c.json(
1601
+ {
1602
+ message: "session attention changed in another client",
1603
+ current: error.current,
1604
+ },
1605
+ 409,
1606
+ );
1607
+ }
1608
+ throw error;
1609
+ }
1610
+ });
1611
+
1612
+ // Personal chat organization: archived roots leave the ordinary rail only
1613
+ // for this member and remain recoverable through the archived list view.
1614
+ app.put("/v1/workspaces/:workspaceId/sessions/:sessionId/archive", async (c) => {
1615
+ const workspaceId = c.req.param("workspaceId");
1616
+ const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:read");
1617
+ const sessionId = c.req.param("sessionId");
1618
+ const parsed = UpdateSessionArchiveRequest.safeParse(await c.req.json().catch(() => null));
1619
+ if (!parsed.success) {
1620
+ throw new HTTPException(400, { message: "invalid session archive request" });
1621
+ }
1622
+ try {
1623
+ const session = await setSessionArchive(db, {
1624
+ workspaceId,
1625
+ subjectId: grant.subjectId,
1626
+ sessionId,
1627
+ ...parsed.data,
1628
+ });
1629
+ if (!session) throw new HTTPException(404, { message: "session not found" });
1630
+ return c.json(
1631
+ await withEffectivePolicy(
1632
+ deps,
1633
+ workspaceId,
1634
+ grant.subjectId,
1635
+ projectSessionForRelatedAccess(session, relatedSessionAccessFor(c)),
1636
+ ),
1637
+ );
1638
+ } catch (error) {
1639
+ if (error instanceof SessionPinAccessError) {
1640
+ throw new HTTPException(403, { message: error.message });
1641
+ }
1642
+ if (error instanceof SessionArchiveVersionConflictError) {
1643
+ return c.json(
1644
+ { message: "session archive changed in another client", current: error.current },
1645
+ 409,
1646
+ );
1647
+ }
1648
+ throw error;
1649
+ }
1650
+ });
1651
+
1362
1652
  app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/lineage", async (c) => {
1363
1653
  const workspaceId = c.req.param("workspaceId");
1364
1654
  const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:read");
@@ -1505,6 +1795,49 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
1505
1795
  return c.json(await withEffectivePolicy(deps, workspaceId, grant.subjectId, session));
1506
1796
  });
1507
1797
 
1798
+ app.delete("/v1/workspaces/:workspaceId/sessions/:sessionId", async (c) => {
1799
+ const workspaceId = c.req.param("workspaceId");
1800
+ const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
1801
+ const deleted = await deleteSessionTreeIfQuiescent(db, {
1802
+ workspaceId,
1803
+ subjectId: grant.subjectId,
1804
+ sessionId: c.req.param("sessionId"),
1805
+ controlLockTimeoutMs: workspaceControlRequestLockTimeoutMs(),
1806
+ });
1807
+ switch (deleted.status) {
1808
+ case "deleted":
1809
+ return c.json({ deletedSessionCount: deleted.deletedSessionCount });
1810
+ case "not_found":
1811
+ throw new HTTPException(404, { message: "session not found" });
1812
+ case "not_root":
1813
+ throw new HTTPException(409, {
1814
+ message: "delete the root session to remove the complete workstream",
1815
+ });
1816
+ case "active_sessions":
1817
+ throw new HTTPException(409, {
1818
+ message: "cancel the workstream and wait for active turns to finish before deleting it",
1819
+ });
1820
+ case "active_video_generations":
1821
+ throw new HTTPException(409, {
1822
+ message: "wait for active video generations to finish before deleting this workstream",
1823
+ });
1824
+ case "active_background_commands":
1825
+ throw new HTTPException(409, {
1826
+ message: "pause or cancel this workstream's background commands before deleting it",
1827
+ });
1828
+ case "live_sandboxes":
1829
+ throw new HTTPException(409, {
1830
+ message:
1831
+ "wait for the workstream's sandbox activity to finish draining before deleting it",
1832
+ });
1833
+ case "externally_referenced":
1834
+ throw new HTTPException(409, {
1835
+ message:
1836
+ "this workstream has durable workspace outputs or independent forks; archive it instead",
1837
+ });
1838
+ }
1839
+ });
1840
+
1508
1841
  app.patch(
1509
1842
  "/v1/workspaces/:workspaceId/sessions/:sessionId/mcp-servers/:serverId/approval-policy",
1510
1843
  async (c) => {
@@ -1576,6 +1909,34 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
1576
1909
  return c.json(await listSessionGoalRevisions(db, workspaceId, sessionId));
1577
1910
  });
1578
1911
 
1912
+ app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/goal/revisions/page", async (c) => {
1913
+ const workspaceId = c.req.param("workspaceId");
1914
+ await requireAccessGrant(c, deps, workspaceId, "sessions:read");
1915
+ const sessionId = c.req.param("sessionId");
1916
+ await assertSessionExists(db, workspaceId, sessionId);
1917
+ const parsedQuery = ListSessionGoalRevisionsQuery.safeParse({
1918
+ limit: c.req.query("limit"),
1919
+ before: c.req.query("before"),
1920
+ });
1921
+ if (!parsedQuery.success) {
1922
+ throw new HTTPException(400, { message: "invalid goal revision page query" });
1923
+ }
1924
+ const query = parsedQuery.data;
1925
+ try {
1926
+ return c.json(
1927
+ await listSessionGoalRevisionPage(db, workspaceId, sessionId, {
1928
+ limit: query.limit,
1929
+ ...(query.before ? { before: query.before } : {}),
1930
+ }),
1931
+ );
1932
+ } catch (error) {
1933
+ if (error instanceof SessionControlConflictError) {
1934
+ throw new HTTPException(409, { message: error.message, cause: error });
1935
+ }
1936
+ throw error;
1937
+ }
1938
+ });
1939
+
1579
1940
  app.post(
1580
1941
  "/v1/workspaces/:workspaceId/sessions/:sessionId/goal/revisions/:revisionId/apply",
1581
1942
  async (c) => {
@@ -1607,6 +1968,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
1607
1968
  sessionId,
1608
1969
  text: revision.text,
1609
1970
  successCriteria: revision.successCriteria,
1971
+ rootConstraints: revision.rootConstraints,
1610
1972
  mutationPolicy: revision.mutationPolicy,
1611
1973
  expectedObjectiveRevision: payload.expectedObjectiveRevision,
1612
1974
  expectedGoalId: revision.goalId,
@@ -1639,6 +2001,88 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
1639
2001
  },
1640
2002
  );
1641
2003
 
2004
+ app.post(
2005
+ "/v1/workspaces/:workspaceId/sessions/:sessionId/goal/revisions/:revisionId/reject",
2006
+ async (c) => {
2007
+ const workspaceId = c.req.param("workspaceId");
2008
+ const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
2009
+ const sessionId = c.req.param("sessionId");
2010
+ await assertSessionExists(db, workspaceId, sessionId);
2011
+ const payload = RejectSessionGoalRevisionRequest.parse(await c.req.json());
2012
+ try {
2013
+ const result = await rejectSessionGoalRevisionWithEvent(db, {
2014
+ accountId: grant.accountId,
2015
+ workspaceId,
2016
+ sessionId,
2017
+ revisionId: c.req.param("revisionId"),
2018
+ expectedObjectiveRevision: payload.expectedObjectiveRevision,
2019
+ rationale: payload.rationale,
2020
+ });
2021
+ await publishDurableSessionEvents(bus, workspaceId, sessionId, result.events);
2022
+ return c.json({ revision: result.revision, replay: result.replay });
2023
+ } catch (error) {
2024
+ if (error instanceof SessionControlConflictError) {
2025
+ throw new HTTPException(409, { message: error.message, cause: error });
2026
+ }
2027
+ throw error;
2028
+ }
2029
+ },
2030
+ );
2031
+
2032
+ app.post(
2033
+ "/v1/workspaces/:workspaceId/sessions/:sessionId/goal/revisions/:revisionId/rollback",
2034
+ async (c) => {
2035
+ const workspaceId = c.req.param("workspaceId");
2036
+ const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
2037
+ const sessionId = c.req.param("sessionId");
2038
+ await assertSessionExists(db, workspaceId, sessionId);
2039
+ const payload = RollbackSessionGoalRevisionRequest.parse(await c.req.json());
2040
+ const revision = await getSessionGoalRevision(
2041
+ db,
2042
+ workspaceId,
2043
+ sessionId,
2044
+ c.req.param("revisionId"),
2045
+ );
2046
+ if (!revision || revision.disposition !== "applied") {
2047
+ throw new HTTPException(404, { message: "applied goal revision not found" });
2048
+ }
2049
+ try {
2050
+ const { goal, workflowWakeRevision, events } = await upsertSessionGoalWithEvent(db, {
2051
+ accountId: grant.accountId,
2052
+ workspaceId,
2053
+ sessionId,
2054
+ text: revision.text,
2055
+ successCriteria: revision.successCriteria,
2056
+ rootConstraints: revision.rootConstraints,
2057
+ mutationPolicy: revision.mutationPolicy,
2058
+ expectedObjectiveRevision: payload.expectedObjectiveRevision,
2059
+ expectedGoalId: revision.goalId,
2060
+ changeKind: "replacement",
2061
+ changeRationale: payload.rationale,
2062
+ rollbackOfRevisionId: revision.id,
2063
+ createdBy: "api",
2064
+ actor: "api",
2065
+ });
2066
+ await publishDurableSessionEvents(bus, workspaceId, sessionId, events);
2067
+ if (workflowWakeRevision !== null) {
2068
+ await workflowClient.wakeSessionWorkflow({
2069
+ accountId: grant.accountId,
2070
+ workspaceId,
2071
+ sessionId,
2072
+ workflowId: workflowIdForSession(sessionId),
2073
+ wakeRevision: workflowWakeRevision,
2074
+ });
2075
+ }
2076
+ return c.json((await getSessionGoalWithContinuation(db, workspaceId, sessionId)) ?? goal);
2077
+ } catch (error) {
2078
+ if (error instanceof SessionControlConflictError) {
2079
+ throw new HTTPException(409, { message: error.message, cause: error });
2080
+ }
2081
+ throw error;
2082
+ }
2083
+ },
2084
+ );
2085
+
1642
2086
  app.patch("/v1/workspaces/:workspaceId/sessions/:sessionId/goal", async (c) => {
1643
2087
  const workspaceId = c.req.param("workspaceId");
1644
2088
  const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
@@ -1660,6 +2104,10 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
1660
2104
  payload.successCriteria !== undefined
1661
2105
  ? payload.successCriteria
1662
2106
  : existing.successCriteria,
2107
+ rootConstraints:
2108
+ payload.rootConstraints !== undefined
2109
+ ? payload.rootConstraints
2110
+ : existing.rootConstraints,
1663
2111
  maxAutoContinuations: existing.maxAutoContinuations,
1664
2112
  mutationPolicy: payload.mutationPolicy ?? existing.mutationPolicy,
1665
2113
  expectedObjectiveRevision: payload.expectedObjectiveRevision,
@@ -2021,19 +2469,16 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
2021
2469
  c.req.raw.signal,
2022
2470
  {
2023
2471
  observability: deps.observability,
2024
- ...(authorization
2025
- ? {
2026
- reauthorizeAfterMs:
2027
- authorization.reauthorizeAfterMs ?? SESSION_AUTHORIZATION_DEFAULT_REAUTHORIZE_MS,
2028
- reauthorize: async () => {
2029
- await requireSessionAuthorization(deps, grant, {
2030
- sessionId,
2031
- operation: "session.stream.read",
2032
- surface: "stream",
2033
- });
2034
- },
2035
- }
2036
- : {}),
2472
+ reauthorizeAfterMs:
2473
+ authorization?.reauthorizeAfterMs ?? SESSION_AUTHORIZATION_DEFAULT_REAUTHORIZE_MS,
2474
+ reauthorize: async () => {
2475
+ const freshGrant = await requireFreshAccessGrant(c, deps, workspaceId, "sessions:read");
2476
+ await requireSessionAuthorization(deps, freshGrant, {
2477
+ sessionId,
2478
+ operation: "session.stream.read",
2479
+ surface: "stream",
2480
+ });
2481
+ },
2037
2482
  },
2038
2483
  );
2039
2484
  });
@@ -2250,7 +2695,13 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
2250
2695
 
2251
2696
  app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/steer", async (c) => {
2252
2697
  const workspaceId = c.req.param("workspaceId");
2253
- const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
2698
+ const authorization = await requireAccessGrantAuthorization(
2699
+ c,
2700
+ deps,
2701
+ workspaceId,
2702
+ "sessions:control",
2703
+ );
2704
+ const grant = authorization.grant;
2254
2705
  const sessionId = c.req.param("sessionId");
2255
2706
  await assertSessionExists(db, workspaceId, sessionId);
2256
2707
  const payload = parseSteerSessionAdmission(await c.req.json().catch(() => null));
@@ -2263,6 +2714,11 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
2263
2714
  reasoningEffort: payload.reasoningEffort ?? null,
2264
2715
  latencyMode: payload.latencyMode ?? null,
2265
2716
  mcpCredentialUpdates: payload.mcpCredentialUpdates ?? [],
2717
+ connectionAuthorities: payload.connectionAuthorities,
2718
+ ...(payload.personalResourceAttachment
2719
+ ? { personalResourceAttachment: payload.personalResourceAttachment }
2720
+ : {}),
2721
+ authorization,
2266
2722
  delivery: "steer",
2267
2723
  origin: "human",
2268
2724
  ...(payload.controlEtag !== undefined ? { controlEtag: payload.controlEtag } : {}),
@@ -2274,9 +2730,69 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
2274
2730
  return c.json(result, 202);
2275
2731
  });
2276
2732
 
2733
+ app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/composer-draft/submit", async (c) => {
2734
+ const workspaceId = c.req.param("workspaceId");
2735
+ const authorization = await requireAccessGrantAuthorization(
2736
+ c,
2737
+ deps,
2738
+ workspaceId,
2739
+ "sessions:control",
2740
+ );
2741
+ const grant = authorization.grant;
2742
+ const sessionId = c.req.param("sessionId");
2743
+ await assertSessionExists(db, workspaceId, sessionId);
2744
+ const payload = SubmitComposerDraftRequest.parse(await c.req.json().catch(() => null));
2745
+ let result: Awaited<ReturnType<typeof acceptSessionUserMessageWithOutcome>>;
2746
+ try {
2747
+ result = await acceptSessionUserMessageWithOutcome(deps, grant, workspaceId, sessionId, {
2748
+ text: payload.text,
2749
+ annotations: payload.annotations,
2750
+ modelContext: payload.modelContext ?? null,
2751
+ resources: payload.resources,
2752
+ model: payload.model,
2753
+ reasoningEffort: payload.reasoningEffort,
2754
+ latencyMode: payload.latencyMode,
2755
+ mcpCredentialUpdates: payload.mcpCredentialUpdates ?? [],
2756
+ connectionAuthorities: payload.connectionAuthorities,
2757
+ ...(payload.personalResourceAttachment
2758
+ ? { personalResourceAttachment: payload.personalResourceAttachment }
2759
+ : {}),
2760
+ authorization,
2761
+ delivery: payload.delivery,
2762
+ origin: "human",
2763
+ expectedDraftRevision: payload.expectedDraftRevision,
2764
+ clientEventId: payload.clientEventId,
2765
+ ...(payload.controlEtag ? { controlEtag: payload.controlEtag } : {}),
2766
+ });
2767
+ } catch (error) {
2768
+ return commandConflictResponse(c, error);
2769
+ }
2770
+ if (!result.draft) {
2771
+ throw new Error("Accepted composer draft submission did not return its next draft");
2772
+ }
2773
+ return c.json(
2774
+ {
2775
+ accepted: result.accepted,
2776
+ turn: result.turn,
2777
+ draft: result.draft,
2778
+ receipt: result.receipt,
2779
+ routing: result.routing,
2780
+ interruptionCount: result.interruptionCount,
2781
+ replay: result.replay,
2782
+ },
2783
+ 202,
2784
+ );
2785
+ });
2786
+
2277
2787
  app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/events", async (c) => {
2278
2788
  const workspaceId = c.req.param("workspaceId");
2279
- const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:control");
2789
+ const authorization = await requireAccessGrantAuthorization(
2790
+ c,
2791
+ deps,
2792
+ workspaceId,
2793
+ "sessions:control",
2794
+ );
2795
+ const grant = authorization.grant;
2280
2796
  const sessionId = c.req.param("sessionId");
2281
2797
  const event = parseSessionEventAdmission(await c.req.json().catch(() => null));
2282
2798
  const refinedOperation =
@@ -2306,6 +2822,11 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
2306
2822
  reasoningEffort: event.payload.reasoningEffort ?? null,
2307
2823
  latencyMode: event.payload.latencyMode ?? null,
2308
2824
  mcpCredentialUpdates: event.payload.mcpCredentialUpdates ?? [],
2825
+ connectionAuthorities: event.payload.connectionAuthorities,
2826
+ ...(event.payload.personalResourceAttachment
2827
+ ? { personalResourceAttachment: event.payload.personalResourceAttachment }
2828
+ : {}),
2829
+ authorization,
2309
2830
  ...(event.payload.controlEtag !== undefined
2310
2831
  ? { controlEtag: event.payload.controlEtag }
2311
2832
  : {}),
@@ -2323,6 +2844,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
2323
2844
  workspaceId,
2324
2845
  sessionId,
2325
2846
  subjectId: grant.subjectId,
2847
+ respondedByKind: childRequiresActionRespondedByKindForGrant(grant),
2326
2848
  payload: event.payload,
2327
2849
  clientEventId: event.clientEventId ?? null,
2328
2850
  });
@@ -2354,6 +2876,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
2354
2876
  requestId: event.payload.requestId,
2355
2877
  response: event.payload.response,
2356
2878
  respondedBy: grant.subjectId,
2879
+ respondedByKind: childRequiresActionRespondedByKindForGrant(grant),
2357
2880
  clientEventId: event.clientEventId ?? null,
2358
2881
  });
2359
2882
  } catch (error) {
@@ -2481,7 +3004,11 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
2481
3004
  // would mint its relay terminal/desktop cells. Resolve the active target once
2482
3005
  // and use the selfhosted liveness probe when it is a connected machine.
2483
3006
  const activeSandbox = session.activeSandboxId
2484
- ? await getSandbox(db, workspaceId, session.activeSandboxId)
3007
+ ? await getSandbox(
3008
+ db,
3009
+ { accountId: grant.accountId, workspaceId, subjectId: grant.subjectId },
3010
+ session.activeSandboxId,
3011
+ )
2485
3012
  : null;
2486
3013
  const selfhostedActive = activeSandbox?.kind === "selfhosted";
2487
3014
  const lease = await readGroupLease(
@@ -2529,12 +3056,19 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
2529
3056
 
2530
3057
  let capabilities;
2531
3058
  if (selfhostedActive && activeSandbox.enrollmentId) {
2532
- const enrollment = await getEnrollment(db, workspaceId, activeSandbox.enrollmentId);
3059
+ const [enrollment, liveConnection] = await Promise.all([
3060
+ getEnrollment(db, grant, activeSandbox.enrollmentId),
3061
+ getLiveEnrollmentConnection(db, grant, activeSandbox.enrollmentId),
3062
+ ]);
2533
3063
  let probeResponded = false;
2534
- if (enrollment?.status === "active") {
3064
+ if (liveConnection?.connectionInstanceId) {
2535
3065
  const machine = new SelfhostedSession({
2536
- workspaceId,
2537
- agentId: enrollment.id,
3066
+ workspaceId: activeSandbox.workspaceId,
3067
+ agentId: liveConnection.id,
3068
+ connectionInstanceId: liveConnection.connectionInstanceId,
3069
+ // Capability negotiation only pings, so a pre-root agent may still
3070
+ // report upgrade guidance without exposing a false filesystem root.
3071
+ workspaceRoot: liveConnection.workspaceRoot ?? "/",
2538
3072
  controlRpc: new NatsControlRpc(async () => bus.getRequestConnection()),
2539
3073
  relay: relayConfigFromSettings(settings),
2540
3074
  epoch: session.activeEpoch,
@@ -2597,6 +3131,20 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
2597
3131
  // place (resolveActiveDesktopTransport), covering BOTH swap directions.
2598
3132
  let responseCapabilities = {
2599
3133
  ...capabilities,
3134
+ FileSystem: {
3135
+ ...capabilities.FileSystem,
3136
+ root: selectBackend(
3137
+ (selfhostedActive
3138
+ ? "selfhosted"
3139
+ : activeSandbox?.kind === "modal"
3140
+ ? "modal"
3141
+ : session.sandboxBackend === "selfhosted" &&
3142
+ settings.sandboxBackend !== "selfhosted" &&
3143
+ settings.sandboxBackend !== "none"
3144
+ ? settings.sandboxBackend
3145
+ : session.sandboxBackend) as SandboxBackend,
3146
+ ).workspaceRoot,
3147
+ },
2600
3148
  Git: {
2601
3149
  ...capabilities.Git,
2602
3150
  repos: capabilities.Git.available ? repositoryRoots : [],
@@ -2723,7 +3271,11 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
2723
3271
  // attachViewer (it warms the Modal group box — the wrong target). Synthesize a
2724
3272
  // result shaped like ViewerAttachResult and mint relay cells directly.
2725
3273
  const activeSandbox = session.activeSandboxId
2726
- ? await getSandbox(db, workspaceId, session.activeSandboxId)
3274
+ ? await getSandbox(
3275
+ db,
3276
+ { accountId: grant.accountId, workspaceId, subjectId: grant.subjectId },
3277
+ session.activeSandboxId,
3278
+ )
2727
3279
  : null;
2728
3280
  const selfhostedActive = activeSandbox?.kind === "selfhosted";
2729
3281
 
@@ -2753,6 +3305,8 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
2753
3305
  stream = await mintDesktopStream(viewerServices, {
2754
3306
  accountId: grant.accountId,
2755
3307
  workspaceId,
3308
+ resourceSubjectId: grant.subjectId,
3309
+ resourceSubjectDelegated: grant.metadata?.delegated === true,
2756
3310
  session,
2757
3311
  viewerId,
2758
3312
  // No Modal lease for selfhosted-active; the mint routes to the relay.
@@ -2762,6 +3316,8 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
2762
3316
  terminal = await mintTerminalStream(viewerServices, {
2763
3317
  accountId: grant.accountId,
2764
3318
  workspaceId,
3319
+ resourceSubjectId: grant.subjectId,
3320
+ resourceSubjectDelegated: grant.metadata?.delegated === true,
2765
3321
  session,
2766
3322
  viewerId,
2767
3323
  // No Modal lease for selfhosted-active; the mint routes to the relay.
@@ -2773,6 +3329,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
2773
3329
  accountId: grant.accountId,
2774
3330
  workspaceId,
2775
3331
  session,
3332
+ viewerSubjectId: grant.subjectId,
2776
3333
  waitSignal: c.req.raw.signal,
2777
3334
  ...(parsed.data.viewerId ? { viewerId: parsed.data.viewerId } : {}),
2778
3335
  });
@@ -2800,6 +3357,8 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
2800
3357
  stream = await mintDesktopStream(viewerServices, {
2801
3358
  accountId: grant.accountId,
2802
3359
  workspaceId,
3360
+ resourceSubjectId: grant.subjectId,
3361
+ resourceSubjectDelegated: grant.metadata?.delegated === true,
2803
3362
  session,
2804
3363
  viewerId: result.viewerId,
2805
3364
  lease,
@@ -2809,6 +3368,8 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
2809
3368
  terminal = await mintTerminalStream(viewerServices, {
2810
3369
  accountId: grant.accountId,
2811
3370
  workspaceId,
3371
+ resourceSubjectId: grant.subjectId,
3372
+ resourceSubjectDelegated: grant.metadata?.delegated === true,
2812
3373
  session,
2813
3374
  viewerId: result.viewerId,
2814
3375
  lease,
@@ -2951,6 +3512,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
2951
3512
  // (rides files:read); Terminal exec + PTY ride terminal:attach.
2952
3513
 
2953
3514
  type ChannelARouteCtx = ChannelAContext & {
3515
+ grant: AccessGrant;
2954
3516
  waitSignal: AbortSignal;
2955
3517
  operation: ChannelAOperation;
2956
3518
  };
@@ -2971,6 +3533,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
2971
3533
  throw new HTTPException(404, { message: "session not found" });
2972
3534
  }
2973
3535
  return {
3536
+ grant,
2974
3537
  accountId: grant.accountId,
2975
3538
  workspaceId,
2976
3539
  session,
@@ -3020,6 +3583,21 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
3020
3583
  return c.json(out);
3021
3584
  });
3022
3585
 
3586
+ app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/artifacts/publish", async (c) => {
3587
+ const workspaceId = c.req.param("workspaceId");
3588
+ await requireAccessGrant(c, deps, workspaceId, "files:upload");
3589
+ const ctx = await channelAPreamble(c, "files:read", "artifact.publish");
3590
+ const request = await parseChannelABody(c, PublishSandboxFileArtifactRequest);
3591
+ return c.json(
3592
+ await publishSandboxFileArtifact(deps, {
3593
+ grant: ctx.grant,
3594
+ session: ctx.session,
3595
+ path: request.path,
3596
+ signal: ctx.waitSignal,
3597
+ }),
3598
+ );
3599
+ });
3600
+
3023
3601
  app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/fs/write", async (c) => {
3024
3602
  const ctx = await channelAPreamble(c, "files:write", "fs.write");
3025
3603
  const req = await parseChannelABody(c, FsWriteRequest);
@@ -3142,7 +3720,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
3142
3720
  // the live/wake path — the feature degrades to today's behavior, never worse.
3143
3721
  app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/workspace/capture", async (c) => {
3144
3722
  const workspaceId = c.req.param("workspaceId") ?? "";
3145
- await requireAccessGrant(c, deps, workspaceId, "files:read");
3723
+ const grant = await requireAccessGrant(c, deps, workspaceId, "files:read");
3146
3724
  const sessionId = c.req.param("sessionId") ?? "";
3147
3725
  const lookup = await sessionLatestWorkspaceCapture(db, workspaceId, sessionId);
3148
3726
  if (!lookup.sessionExists) {
@@ -3153,13 +3731,27 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
3153
3731
  return c.json({ available: false });
3154
3732
  }
3155
3733
  return c.json(
3156
- await serveWorkspaceCapture(lookup.capture, objectStorage, workspaceCaptureManifestCache),
3734
+ await serveWorkspaceCapture(
3735
+ lookup.capture,
3736
+ objectStorage,
3737
+ workspaceCaptureManifestCache,
3738
+ (fact) =>
3739
+ recordAuditEvent(db, {
3740
+ accountId: grant.accountId,
3741
+ workspaceId,
3742
+ subjectId: grant.subjectId,
3743
+ action: "file.signed_url.issued",
3744
+ targetType: "workspace_capture",
3745
+ targetId: sessionId,
3746
+ metadata: { sessionId, ...fact },
3747
+ }),
3748
+ ),
3157
3749
  );
3158
3750
  });
3159
3751
 
3160
3752
  app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/workspace/capture/file", async (c) => {
3161
3753
  const workspaceId = c.req.param("workspaceId") ?? "";
3162
- await requireAccessGrant(c, deps, workspaceId, "files:read");
3754
+ const grant = await requireAccessGrant(c, deps, workspaceId, "files:read");
3163
3755
  const sessionId = c.req.param("sessionId") ?? "";
3164
3756
  const path = c.req.query("path");
3165
3757
  if (!path) {
@@ -3188,7 +3780,21 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
3188
3780
  } else {
3189
3781
  row = await latestWorkspaceCapture(db, workspaceId, sessionId);
3190
3782
  }
3191
- return c.json(await serveWorkspaceCaptureFile(row, path, objectStorage));
3783
+ return c.json(
3784
+ await serveWorkspaceCaptureFile(row, path, objectStorage, (fact) =>
3785
+ recordAuditEvent(db, {
3786
+ accountId: grant.accountId,
3787
+ workspaceId,
3788
+ subjectId: grant.subjectId,
3789
+ action: "file.signed_url.issued",
3790
+ targetType: "workspace_capture",
3791
+ targetId: sessionId,
3792
+ // The captured PATH is workspace-file identity (like a filename),
3793
+ // not content; the signed URL and object key are never recorded.
3794
+ metadata: { sessionId, path, ...fact },
3795
+ }),
3796
+ ),
3797
+ );
3192
3798
  });
3193
3799
 
3194
3800
  // ── Terminal: synchronous exec ────────────────────────────────────────────
@@ -3510,15 +4116,24 @@ export function sessionAuthorizationOperationForHttp(
3510
4116
  if (suffix === "") {
3511
4117
  if (verb === "GET") return "session.read";
3512
4118
  if (verb === "PATCH") return "session.title.write";
4119
+ if (verb === "DELETE") return "session.delete";
3513
4120
  return null;
3514
4121
  }
3515
4122
  if (suffix === "/pin" && verb === "PUT") return "session.pin.write";
4123
+ if (suffix === "/attention" && verb === "PUT") return "session.attention.write";
4124
+ if (suffix === "/archive" && verb === "PUT") return "session.archive.write";
4125
+ if (suffix === "/visibility" && verb === "PUT") return "session.visibility.write";
4126
+ if (suffix === "/forks" && verb === "POST") return "session.fork.create";
3516
4127
  if (suffix === "/channel" && verb === "PUT") return "session.channel.write";
3517
4128
  if (suffix === "/tool-policy" && verb === "PUT") return "session.tool_policy.write";
3518
4129
  if (/^\/mcp-servers\/[^/]+\/approval-policy$/.test(suffix) && verb === "PATCH") {
3519
4130
  return "session.mcp.approval_policy.write";
3520
4131
  }
3521
4132
  if (suffix === "/lineage" && verb === "GET") return "session.lineage.read";
4133
+ if (suffix === "/background-commands" && verb === "GET") return "session.read";
4134
+ if (/^\/background-commands\/[^/]+$/.test(suffix) && verb === "DELETE") {
4135
+ return "session.control";
4136
+ }
3522
4137
  if (suffix === "/codex-account" && verb === "POST") {
3523
4138
  return "session.codex_account.write";
3524
4139
  }
@@ -3554,9 +4169,13 @@ export function sessionAuthorizationOperationForHttp(
3554
4169
  : null;
3555
4170
  }
3556
4171
  if (suffix === "/goal/revisions" && verb === "GET") return "session.goal.read";
4172
+ if (suffix === "/goal/revisions/page" && verb === "GET") return "session.goal.read";
3557
4173
  if (/^\/goal\/revisions\/[^/]+\/apply$/.test(suffix) && verb === "POST") {
3558
4174
  return "session.goal.write";
3559
4175
  }
4176
+ if (/^\/goal\/revisions\/[^/]+\/(reject|rollback)$/.test(suffix) && verb === "POST") {
4177
+ return "session.goal.write";
4178
+ }
3560
4179
  if (suffix === "/context/clear" || suffix === "/context/compact") {
3561
4180
  return verb === "POST" ? "session.context.write" : null;
3562
4181
  }
@@ -3574,6 +4193,9 @@ export function sessionAuthorizationOperationForHttp(
3574
4193
  if (verb === "PUT") return "session.composer.write";
3575
4194
  return null;
3576
4195
  }
4196
+ // Same HTTP gate as POST /events. Delivery `steer` is re-authorized inside
4197
+ // acceptSessionUserMessageWithOutcome after the body is parsed.
4198
+ if (suffix === "/composer-draft/submit" && verb === "POST") return "session.append";
3577
4199
  if (suffix === "/control" && verb === "POST") return "session.control";
3578
4200
  if (suffix === "/steer" && verb === "POST") return "session.steer";
3579
4201
  if (suffix === "/human-input-requests" && verb === "GET") {
@@ -3593,6 +4215,7 @@ export function sessionAuthorizationOperationForHttp(
3593
4215
  if (suffix === "/fs/list" || suffix === "/fs/list-batch" || suffix === "/fs/read") {
3594
4216
  return verb === "POST" ? "session.files.read" : null;
3595
4217
  }
4218
+ if (suffix === "/artifacts/publish" && verb === "POST") return "session.files.write";
3596
4219
  if (["/fs/write", "/fs/delete", "/fs/move", "/fs/mkdir"].includes(suffix)) {
3597
4220
  return verb === "POST" ? "session.files.write" : null;
3598
4221
  }
@@ -3621,6 +4244,70 @@ function sessionAuthorizationHttpError(error: unknown): HTTPException {
3621
4244
  throw error;
3622
4245
  }
3623
4246
 
4247
+ export function sessionTenancyHttpError(error: unknown): Error {
4248
+ if (error instanceof SessionTenancyManagedHumanRequiredError) {
4249
+ return new ApiHttpError(403, {
4250
+ code: "forbidden",
4251
+ message: "Session tenancy mutations require the owning managed-human session.",
4252
+ retryable: false,
4253
+ });
4254
+ }
4255
+ if (error instanceof SessionTenancyNotActivatedError) {
4256
+ return new ApiHttpError(409, {
4257
+ code: "conflict",
4258
+ message: "Session tenancy is not activated for this organization.",
4259
+ retryable: false,
4260
+ details: { reason: "not_activated" },
4261
+ });
4262
+ }
4263
+ if (error instanceof SessionTenancyConflictError) {
4264
+ return new ApiHttpError(409, {
4265
+ code: error.reason === "operation_reuse" ? "idempotency_conflict" : "conflict",
4266
+ message:
4267
+ error.reason === "not_quiescent"
4268
+ ? "The session must be fully quiescent before this change."
4269
+ : error.reason === "authority_epoch"
4270
+ ? "The session authority changed before this operation committed."
4271
+ : "The idempotency key was already used with different input.",
4272
+ retryable: false,
4273
+ details: {
4274
+ reason: error.reason,
4275
+ ...(error.blocker ? { blocker: error.blocker } : {}),
4276
+ },
4277
+ });
4278
+ }
4279
+ if (
4280
+ error instanceof SessionTenancyAccessError ||
4281
+ error instanceof SessionAuthorizationDeniedError
4282
+ ) {
4283
+ return new ApiHttpError(404, {
4284
+ code: "not_found",
4285
+ message: "Session not found.",
4286
+ retryable: false,
4287
+ });
4288
+ }
4289
+ if (error instanceof SessionTenancyInvalidRequestError) {
4290
+ return new ApiHttpError(422, {
4291
+ code: "validation_failed",
4292
+ message: "Invalid session tenancy request.",
4293
+ retryable: false,
4294
+ });
4295
+ }
4296
+ if (error instanceof SessionTenancyPersistenceOutcomeUnknownError) {
4297
+ return new ApiHttpError(503, {
4298
+ code: "upstream_unavailable",
4299
+ message:
4300
+ "The session tenancy mutation outcome is unknown. Retry with the same idempotency key.",
4301
+ retryable: true,
4302
+ outcomeUnknown: true,
4303
+ });
4304
+ }
4305
+ if (error instanceof SessionAuthorizationUnavailableError) {
4306
+ return sessionAuthorizationHttpError(error);
4307
+ }
4308
+ return error instanceof Error ? error : new Error("Unknown session tenancy error");
4309
+ }
4310
+
3624
4311
  function eventEnumValue<T extends string>(
3625
4312
  raw: string | undefined,
3626
4313
  schema: { safeParse(value: unknown): { success: boolean; data?: T } },
@@ -3682,6 +4369,7 @@ function sessionListQuery(
3682
4369
  cursor: ReturnType<typeof decodeSessionListCursor> | undefined;
3683
4370
  search: string | undefined;
3684
4371
  pinsOnly: boolean;
4372
+ archivedOnly: boolean;
3685
4373
  } {
3686
4374
  const parentSessionId = query.parentSessionId;
3687
4375
  // "null" = roots only; a uuid = children of that session; anything else is
@@ -3713,6 +4401,10 @@ function sessionListQuery(
3713
4401
  });
3714
4402
  }
3715
4403
  const pinsOnly = query.pinsOnly === "true";
4404
+ if (query.archivedOnly !== undefined && query.archivedOnly !== "true") {
4405
+ throw new HTTPException(400, { message: 'archivedOnly must be the literal "true"' });
4406
+ }
4407
+ const archivedOnly = query.archivedOnly === "true";
3716
4408
  if (pinsOnly && !allowCursor) {
3717
4409
  throw new HTTPException(400, { message: 'pinsOnly requires view="page"' });
3718
4410
  }
@@ -3721,6 +4413,9 @@ function sessionListQuery(
3721
4413
  message: "pinsOnly cannot be combined with cursor, parentSessionId, or search",
3722
4414
  });
3723
4415
  }
4416
+ if (pinsOnly && archivedOnly) {
4417
+ throw new HTTPException(400, { message: "pinsOnly cannot be combined with archivedOnly" });
4418
+ }
3724
4419
  return {
3725
4420
  limit: query.limit,
3726
4421
  parentSessionId:
@@ -3732,6 +4427,7 @@ function sessionListQuery(
3732
4427
  cursor,
3733
4428
  search: search || undefined,
3734
4429
  pinsOnly,
4430
+ archivedOnly,
3735
4431
  };
3736
4432
  }
3737
4433
 
@@ -3865,6 +4561,24 @@ function optionalEventSequence(raw: string | undefined): number | undefined {
3865
4561
 
3866
4562
  /** Stable, value-free JSON errors for only the create-session boundary. */
3867
4563
  export function sessionCreateErrorResponse(c: Context, error: unknown): Response {
4564
+ if (error instanceof SessionTenancyManagedHumanRequiredError) {
4565
+ return c.json(
4566
+ {
4567
+ code: "SESSION_CREATE_FORBIDDEN",
4568
+ message: "Only managed-account users can create an Only-me session.",
4569
+ },
4570
+ 403,
4571
+ );
4572
+ }
4573
+ if (error instanceof SessionTenancyNotActivatedError) {
4574
+ return c.json(
4575
+ {
4576
+ code: "SESSION_TENANCY_NOT_ACTIVATED",
4577
+ message: "Private sessions are not enabled for this organization.",
4578
+ },
4579
+ 409,
4580
+ );
4581
+ }
3868
4582
  if (error instanceof ChannelNotFoundError) {
3869
4583
  // Covers the create-vs-channel-delete race the pre-validation cannot: the
3870
4584
  // insert's FK rejection surfaces as the same 422 an unknown id gets.
@@ -3885,6 +4599,20 @@ export function sessionCreateErrorResponse(c: Context, error: unknown): Response
3885
4599
  422,
3886
4600
  );
3887
4601
  }
4602
+ if (
4603
+ error instanceof HTTPException &&
4604
+ error.status === 409 &&
4605
+ error.cause instanceof NewSessionDraftConflictError
4606
+ ) {
4607
+ return c.json(
4608
+ {
4609
+ code: "NEW_SESSION_DRAFT_CONFLICT",
4610
+ message: error.cause.message,
4611
+ currentRevision: error.cause.currentRevision,
4612
+ },
4613
+ 409,
4614
+ );
4615
+ }
3888
4616
  if (error instanceof HTTPException && error.status === 422) {
3889
4617
  return c.json(
3890
4618
  {
@@ -3894,6 +4622,15 @@ export function sessionCreateErrorResponse(c: Context, error: unknown): Response
3894
4622
  422,
3895
4623
  );
3896
4624
  }
4625
+ if (error instanceof HTTPException && error.status === 409) {
4626
+ return c.json(
4627
+ {
4628
+ code: "SESSION_CREATE_CONFLICT",
4629
+ message: error.message,
4630
+ },
4631
+ 409,
4632
+ );
4633
+ }
3897
4634
  throw error;
3898
4635
  }
3899
4636