@opengeni/contracts 2.9.2 → 2.13.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 (51) hide show
  1. package/dist/artifacts.d.ts +121 -0
  2. package/dist/atlassian.js +11 -11
  3. package/dist/{chunk-4IVHBXRI.js → chunk-7ABONTXE.js} +42 -2
  4. package/dist/chunk-7ABONTXE.js.map +1 -0
  5. package/dist/{chunk-7RMTKFJW.js → chunk-7Q6HPDY2.js} +8 -8
  6. package/dist/{chunk-AWNGBY5I.js → chunk-DPCCW6U7.js} +7395 -6619
  7. package/dist/chunk-DPCCW6U7.js.map +1 -0
  8. package/dist/{chunk-N6GRVXAJ.js → chunk-NPBM4QSK.js} +2 -2
  9. package/dist/connection-authority.js +11 -11
  10. package/dist/editable-artifact-codec-registry.js +2 -2
  11. package/dist/editable-artifact-live.js +2 -2
  12. package/dist/editable-artifact-serialized-commit.js +3 -3
  13. package/dist/editable-artifacts.js +13 -13
  14. package/dist/github-repository-contracts.d.ts +36 -0
  15. package/dist/github-repository-contracts.js +59 -0
  16. package/dist/github-repository-contracts.js.map +1 -0
  17. package/dist/github-repository.d.ts +14 -0
  18. package/dist/github-repository.js +36 -0
  19. package/dist/github-repository.js.map +1 -0
  20. package/dist/google-drive.js +12 -12
  21. package/dist/index.d.ts +809 -70
  22. package/dist/index.js +299 -135
  23. package/dist/mcp-oauth.d.ts +71 -0
  24. package/dist/model-context-inspector.d.ts +205 -0
  25. package/dist/model-picker-order.d.ts +23 -0
  26. package/dist/model-picker-order.js +42 -0
  27. package/dist/model-picker-order.js.map +1 -0
  28. package/dist/organization-membership-lifecycle.d.ts +17 -0
  29. package/dist/personal-github.js +11 -11
  30. package/dist/session-mcp-projections.d.ts +210 -0
  31. package/dist/session-titles.d.ts +35 -0
  32. package/dist/session-titles.js +9 -3
  33. package/dist/tool-catalog.d.ts +271 -0
  34. package/dist/workspace-learning-policy.d.ts +1 -1
  35. package/package.json +13 -1
  36. package/src/artifacts.ts +111 -1
  37. package/src/github-repository-contracts.ts +85 -0
  38. package/src/github-repository.ts +59 -0
  39. package/src/index.ts +486 -52
  40. package/src/mcp-oauth.ts +68 -0
  41. package/src/model-context-inspector.ts +104 -0
  42. package/src/model-picker-order.ts +87 -0
  43. package/src/organization-membership-lifecycle.ts +20 -0
  44. package/src/session-mcp-projections.ts +268 -0
  45. package/src/session-titles.ts +100 -0
  46. package/src/tool-catalog.ts +136 -0
  47. package/src/workspace-learning-policy.ts +4 -3
  48. package/dist/chunk-4IVHBXRI.js.map +0 -1
  49. package/dist/chunk-AWNGBY5I.js.map +0 -1
  50. /package/dist/{chunk-7RMTKFJW.js.map → chunk-7Q6HPDY2.js.map} +0 -0
  51. /package/dist/{chunk-N6GRVXAJ.js.map → chunk-NPBM4QSK.js.map} +0 -0
@@ -0,0 +1,68 @@
1
+ import { z } from "zod";
2
+
3
+ export const MCP_OAUTH_SCOPE = "mcp:access" as const;
4
+ export const MCP_OAUTH_ACCESS_TOKEN_TTL_SECONDS = 15 * 60;
5
+ export const MCP_OAUTH_REFRESH_TOKEN_TTL_SECONDS = 30 * 24 * 60 * 60;
6
+ export const MCP_OAUTH_AUTHORIZATION_CODE_TTL_SECONDS = 5 * 60;
7
+ export const MCP_OAUTH_CONSENT_TTL_SECONDS = 10 * 60;
8
+
9
+ const RedirectUri = z.string().url().max(2_048);
10
+
11
+ export const McpOAuthClientRegistrationRequest = z
12
+ .object({
13
+ redirect_uris: z.array(RedirectUri).min(1).max(16),
14
+ client_name: z.string().trim().min(1).max(200).optional(),
15
+ application_type: z.enum(["native", "web"]).optional(),
16
+ token_endpoint_auth_method: z.literal("none").default("none"),
17
+ scope: z.literal(MCP_OAUTH_SCOPE).optional(),
18
+ grant_types: z
19
+ .array(z.enum(["authorization_code", "refresh_token"]))
20
+ .min(1)
21
+ .max(2)
22
+ .refine((grantTypes) => grantTypes.includes("authorization_code"), {
23
+ message: "authorization_code grant is required",
24
+ })
25
+ .default(["authorization_code", "refresh_token"]),
26
+ response_types: z.array(z.literal("code")).min(1).max(1).default(["code"]),
27
+ })
28
+ .strict();
29
+ export type McpOAuthClientRegistrationRequest = z.infer<typeof McpOAuthClientRegistrationRequest>;
30
+
31
+ export const McpOAuthClientRegistrationResponse = McpOAuthClientRegistrationRequest.extend({
32
+ client_id: z.string().min(1).max(256),
33
+ client_id_issued_at: z.number().int().nonnegative(),
34
+ });
35
+ export type McpOAuthClientRegistrationResponse = z.infer<typeof McpOAuthClientRegistrationResponse>;
36
+
37
+ export const McpOAuthTokenResponse = z.object({
38
+ access_token: z.string().min(1),
39
+ token_type: z.literal("Bearer"),
40
+ expires_in: z.number().int().positive(),
41
+ refresh_token: z.string().min(1).optional(),
42
+ scope: z.literal(MCP_OAUTH_SCOPE),
43
+ });
44
+ export type McpOAuthTokenResponse = z.infer<typeof McpOAuthTokenResponse>;
45
+
46
+ export const McpOAuthProtectedResourceMetadata = z.object({
47
+ resource: z.string().url(),
48
+ authorization_servers: z.array(z.string().url()).min(1).max(1),
49
+ scopes_supported: z.array(z.literal(MCP_OAUTH_SCOPE)).length(1),
50
+ bearer_methods_supported: z.array(z.literal("header")).length(1),
51
+ });
52
+ export type McpOAuthProtectedResourceMetadata = z.infer<typeof McpOAuthProtectedResourceMetadata>;
53
+
54
+ export const McpOAuthAuthorizationServerMetadata = z.object({
55
+ issuer: z.string().url(),
56
+ authorization_endpoint: z.string().url(),
57
+ token_endpoint: z.string().url(),
58
+ registration_endpoint: z.string().url(),
59
+ response_types_supported: z.array(z.literal("code")).length(1),
60
+ grant_types_supported: z.array(z.enum(["authorization_code", "refresh_token"])).length(2),
61
+ code_challenge_methods_supported: z.array(z.literal("S256")).length(1),
62
+ token_endpoint_auth_methods_supported: z.array(z.literal("none")).length(1),
63
+ scopes_supported: z.array(z.literal(MCP_OAUTH_SCOPE)).length(1),
64
+ authorization_response_iss_parameter_supported: z.literal(true),
65
+ });
66
+ export type McpOAuthAuthorizationServerMetadata = z.infer<
67
+ typeof McpOAuthAuthorizationServerMetadata
68
+ >;
@@ -0,0 +1,104 @@
1
+ import { z } from "zod";
2
+
3
+ export const MODEL_CONTEXT_SNAPSHOT_VERSION = 1 as const;
4
+ export const MODEL_CONTEXT_INSTRUCTIONS_MAX_UTF8_BYTES = 8 * 1024 * 1024;
5
+ export const MODEL_CONTEXT_SNAPSHOT_MAX_UTF8_BYTES = 16 * 1024 * 1024;
6
+ export const MODEL_CONTEXT_LAYER_MAX_COUNT = 16;
7
+ export const MODEL_CONTEXT_TOOL_MAX_COUNT = 2_048;
8
+ export const MODEL_CONTEXT_SKILL_MAX_COUNT = 1_024;
9
+
10
+ export const ModelContextInstructionLayerId = z.enum([
11
+ "operational_contract",
12
+ "persona_and_core",
13
+ "workspace_governance",
14
+ "session_instructions",
15
+ "workspace_memory",
16
+ "codemode",
17
+ "git_bindings",
18
+ "genesis_title",
19
+ "sdk_capability_instructions",
20
+ "sandbox_preamble",
21
+ "sandbox_filesystem",
22
+ "sent_system_instructions",
23
+ ]);
24
+ export type ModelContextInstructionLayerId = z.infer<typeof ModelContextInstructionLayerId>;
25
+
26
+ export const ModelContextInstructionLayer = z
27
+ .object({
28
+ id: ModelContextInstructionLayerId,
29
+ title: z.string().min(1).max(120),
30
+ content: z.string(),
31
+ utf8Bytes: z.number().int().nonnegative(),
32
+ estimatedTokens: z.number().int().nonnegative(),
33
+ })
34
+ .strict();
35
+ export type ModelContextInstructionLayer = z.infer<typeof ModelContextInstructionLayer>;
36
+
37
+ export const ModelContextToolVisibility = z.enum(["eager", "searchable"]);
38
+ export type ModelContextToolVisibility = z.infer<typeof ModelContextToolVisibility>;
39
+
40
+ export const ModelContextTool = z
41
+ .object({
42
+ name: z.string().min(1).max(256),
43
+ type: z.string().min(1).max(64),
44
+ visibility: ModelContextToolVisibility,
45
+ description: z.string().max(16_384).optional(),
46
+ namespace: z.string().max(256).optional(),
47
+ schema: z.unknown().optional(),
48
+ utf8Bytes: z.number().int().nonnegative(),
49
+ estimatedTokens: z.number().int().nonnegative(),
50
+ })
51
+ .strict();
52
+ export type ModelContextTool = z.infer<typeof ModelContextTool>;
53
+
54
+ export const ModelContextSkillKind = z.enum([
55
+ "preference_descriptor",
56
+ "runtime_skill",
57
+ "native_tool_skill",
58
+ ]);
59
+ export type ModelContextSkillKind = z.infer<typeof ModelContextSkillKind>;
60
+
61
+ export const ModelContextSkill = z
62
+ .object({
63
+ kind: ModelContextSkillKind,
64
+ name: z.string().min(1).max(128),
65
+ description: z.string().max(8_192),
66
+ source: z.string().max(64).optional(),
67
+ path: z.string().max(1_024).optional(),
68
+ })
69
+ .strict();
70
+ export type ModelContextSkill = z.infer<typeof ModelContextSkill>;
71
+
72
+ export const ModelContextTokenCounts = z
73
+ .object({
74
+ instructions: z.number().int().nonnegative(),
75
+ tools: z.number().int().nonnegative(),
76
+ prefix: z.number().int().nonnegative(),
77
+ })
78
+ .strict();
79
+ export type ModelContextTokenCounts = z.infer<typeof ModelContextTokenCounts>;
80
+
81
+ export const ModelContextSnapshot = z
82
+ .object({
83
+ version: z.literal(MODEL_CONTEXT_SNAPSHOT_VERSION),
84
+ capturedAt: z.string().datetime(),
85
+ source: z.literal("model_request"),
86
+ requestIndex: z.number().int().nonnegative(),
87
+ instructions: z.string(),
88
+ layers: z.array(ModelContextInstructionLayer).max(MODEL_CONTEXT_LAYER_MAX_COUNT),
89
+ tools: z.array(ModelContextTool).max(MODEL_CONTEXT_TOOL_MAX_COUNT),
90
+ skills: z.array(ModelContextSkill).max(MODEL_CONTEXT_SKILL_MAX_COUNT),
91
+ tokens: ModelContextTokenCounts,
92
+ })
93
+ .strict();
94
+ export type ModelContextSnapshot = z.infer<typeof ModelContextSnapshot>;
95
+
96
+ export const SessionModelContextResponse = z
97
+ .object({
98
+ sessionId: z.string().uuid(),
99
+ attemptId: z.string().uuid().nullable(),
100
+ turnId: z.string().uuid().nullable(),
101
+ snapshot: ModelContextSnapshot.nullable(),
102
+ })
103
+ .strict();
104
+ export type SessionModelContextResponse = z.infer<typeof SessionModelContextResponse>;
@@ -0,0 +1,87 @@
1
+ export type ModelPickerBillingClass =
2
+ | "opengeni_credits"
3
+ | "external"
4
+ | "codex_subscription"
5
+ | "supergrok_subscription"
6
+ | "byok"
7
+ | "organization_byok";
8
+
9
+ // Every closed billing class has a unique first character. Keeping the order
10
+ // as initials avoids duplicating the full public labels in browser bundles.
11
+ const MODEL_PICKER_BILLING_CLASS_ORDER: readonly ModelPickerBillingClass[] = [
12
+ "opengeni_credits",
13
+ "external",
14
+ "codex_subscription",
15
+ "supergrok_subscription",
16
+ "byok",
17
+ "organization_byok",
18
+ ];
19
+
20
+ export type ModelPickerBillingCandidate = {
21
+ source?: string | undefined;
22
+ cost?: "free" | "credits" | "subscription" | "workspace" | "organization" | undefined;
23
+ billing?:
24
+ | {
25
+ upstreamPayer?: string | undefined;
26
+ metering?: string | undefined;
27
+ }
28
+ | undefined;
29
+ credentialSource?:
30
+ | {
31
+ kind?: string | undefined;
32
+ provider?: string | undefined;
33
+ }
34
+ | undefined;
35
+ };
36
+
37
+ export function modelPickerBillingClassFor(
38
+ model: ModelPickerBillingCandidate,
39
+ ): ModelPickerBillingClass {
40
+ if (model.cost === "credits") return "opengeni_credits";
41
+ if (model.cost === "workspace") return "byok";
42
+ if (model.cost === "organization") return "organization_byok";
43
+ const source = model.source;
44
+ const credential = model.credentialSource;
45
+ const payer = model.billing?.upstreamPayer;
46
+ if (model.billing?.metering === "external" && payer === "deployment") {
47
+ return "external";
48
+ }
49
+ if (
50
+ source === "supergrok" ||
51
+ (credential?.kind === "connected_subscription" && credential.provider === "xai")
52
+ ) {
53
+ return "supergrok_subscription";
54
+ }
55
+ if (
56
+ source === "codex" ||
57
+ credential?.kind === "connected_subscription" ||
58
+ payer === "connected_subscription"
59
+ ) {
60
+ return "codex_subscription";
61
+ }
62
+ if (
63
+ source === "workspace_gateway" ||
64
+ credential?.kind === "workspace_connection" ||
65
+ payer === "workspace"
66
+ ) {
67
+ return "byok";
68
+ }
69
+ if (credential?.kind === "organization_connection" || payer === "organization") {
70
+ return "organization_byok";
71
+ }
72
+ return "opengeni_credits";
73
+ }
74
+
75
+ export function compareModelPickerOrder(
76
+ left: { billingClass: ModelPickerBillingClass; selectable: boolean; label: string },
77
+ right: { billingClass: ModelPickerBillingClass; selectable: boolean; label: string },
78
+ ): number {
79
+ const classDelta =
80
+ MODEL_PICKER_BILLING_CLASS_ORDER.indexOf(left.billingClass) -
81
+ MODEL_PICKER_BILLING_CLASS_ORDER.indexOf(right.billingClass);
82
+ return (
83
+ classDelta ||
84
+ +right.selectable - +left.selectable ||
85
+ (left.label < right.label ? -1 : left.label > right.label ? 1 : 0)
86
+ );
87
+ }
@@ -239,6 +239,26 @@ export const CreateOrganizationResponse = z.object({
239
239
  });
240
240
  export type CreateOrganizationResponse = z.infer<typeof CreateOrganizationResponse>;
241
241
 
242
+ export const CreateAdditionalOrganizationRequest = z
243
+ .object({
244
+ name: z.string().trim().min(1).max(120),
245
+ workspaceName: z.string().trim().min(1).max(120),
246
+ operationId: z.string().uuid(),
247
+ })
248
+ .strict();
249
+ export type CreateAdditionalOrganizationRequest = z.infer<
250
+ typeof CreateAdditionalOrganizationRequest
251
+ >;
252
+
253
+ export const CreateAdditionalOrganizationResponse = z.object({
254
+ organization: OrganizationSummary,
255
+ workspaceId: z.string().uuid(),
256
+ personalWorkspaceId: z.string().uuid(),
257
+ });
258
+ export type CreateAdditionalOrganizationResponse = z.infer<
259
+ typeof CreateAdditionalOrganizationResponse
260
+ >;
261
+
242
262
  export const UpdateOrganizationNameRequest = z.object({
243
263
  name: z.string().trim().min(1).max(120),
244
264
  expectedUpdatedAt: z.string().datetime({ offset: true }),
@@ -0,0 +1,268 @@
1
+ import type { SessionGoalStatus, SessionStatus } from "./session-topology-primitives";
2
+ import type { WorkDiscoveryProjection } from "./work-claims";
3
+ import type { Session, SessionQueueSnapshot } from "./index";
4
+
5
+ /** MCP-only presentation choice. REST/SDK session shapes keep their own defaults. */
6
+ export type SessionMcpDetail = "compact" | "full";
7
+
8
+ /** Search evidence is inseparable from matching; presentation opts never weaken search. */
9
+ export function sessionMcpIncludesRelatedWork(options: {
10
+ detail?: SessionMcpDetail | undefined;
11
+ includeRelatedWork?: boolean | undefined;
12
+ query?: string | undefined;
13
+ subject?: unknown;
14
+ }): boolean {
15
+ return (
16
+ Boolean(options.query?.trim() || options.subject) ||
17
+ (options.includeRelatedWork ?? options.detail === "full")
18
+ );
19
+ }
20
+
21
+ /** A database prefix plus its exact Unicode code-point count, not a JS UTF-16 count. */
22
+ export function boundSessionMcpText(
23
+ value: string | null,
24
+ maxChars = 600,
25
+ originalChars?: number | null,
26
+ ) {
27
+ if (value === null) return { text: null, truncated: false };
28
+ const chars = Array.from(value);
29
+ const sourceChars = Math.max(chars.length, originalChars ?? chars.length);
30
+ if (sourceChars <= maxChars) return { text: value, truncated: false };
31
+ let bodyChars = maxChars;
32
+ let marker = "";
33
+ for (let attempt = 0; attempt < 4; attempt += 1) {
34
+ marker = `…[${Math.max(0, sourceChars - bodyChars)} chars truncated]…`;
35
+ const next = Math.max(0, maxChars - Array.from(marker).length);
36
+ if (next === bodyChars) break;
37
+ bodyChars = next;
38
+ }
39
+ return { text: `${chars.slice(0, bodyChars).join("")}${marker}`, truncated: true };
40
+ }
41
+
42
+ export type SessionMcpControlSource = {
43
+ state: string;
44
+ primaryBlocker: {
45
+ kind: "session" | "workspace";
46
+ sessionId?: string | undefined;
47
+ displayName: string;
48
+ displayNameOriginalChars?: number;
49
+ reason?: string | null;
50
+ } | null;
51
+ additionalBlockerCount: number;
52
+ };
53
+
54
+ /** Call only after related-access projection: this helper never grants ancestor access. */
55
+ export function compactSessionMcpPause(control: SessionMcpControlSource) {
56
+ const blocker = control.primaryBlocker;
57
+ if (control.state === "active" && !blocker && !control.additionalBlockerCount) return undefined;
58
+ const name = blocker
59
+ ? boundSessionMcpText(blocker.displayName, 200, blocker.displayNameOriginalChars)
60
+ : null;
61
+ const reason = blocker?.reason ? boundSessionMcpText(blocker.reason) : null;
62
+ return {
63
+ state: control.state,
64
+ ...(blocker
65
+ ? {
66
+ source: {
67
+ kind: blocker.kind,
68
+ ...(blocker.sessionId ? { sessionId: blocker.sessionId } : {}),
69
+ displayName: name!.text,
70
+ ...(name!.truncated ? { displayNameTruncated: true } : {}),
71
+ ...(reason
72
+ ? { reason: reason.text, ...(reason.truncated ? { reasonTruncated: true } : {}) }
73
+ : {}),
74
+ },
75
+ }
76
+ : {}),
77
+ ...(control.additionalBlockerCount > 0
78
+ ? { additionalBlockerCount: control.additionalBlockerCount }
79
+ : {}),
80
+ };
81
+ }
82
+
83
+ export type SessionMcpListSource = {
84
+ id: string;
85
+ title: string | null;
86
+ titleOriginalChars?: number | null;
87
+ status: SessionStatus;
88
+ parentSessionId: string | null;
89
+ effectiveControl: SessionMcpControlSource;
90
+ goal: { status: SessionGoalStatus; text: string; textOriginalChars?: number } | null;
91
+ updatedAt: string;
92
+ workDiscovery?: WorkDiscoveryProjection;
93
+ treeStats?: {
94
+ attentionDescendants: number;
95
+ pausedDescendants: number;
96
+ failedDescendants: number;
97
+ truncated: boolean;
98
+ };
99
+ };
100
+
101
+ /** Closed allowlist, shared by MCP adapters. Never spread a database/session object. */
102
+ export function compactSessionMcpListRow(
103
+ session: SessionMcpListSource,
104
+ includeRelatedWork = false,
105
+ ) {
106
+ const title = boundSessionMcpText(session.title, 200, session.titleOriginalChars);
107
+ const goal = session.goal
108
+ ? boundSessionMcpText(session.goal.text, 600, session.goal.textOriginalChars)
109
+ : null;
110
+ const pause = compactSessionMcpPause(session.effectiveControl);
111
+ const tree = session.treeStats;
112
+ const attention =
113
+ tree &&
114
+ (tree.attentionDescendants > 0 ||
115
+ tree.pausedDescendants > 0 ||
116
+ tree.failedDescendants > 0 ||
117
+ tree.truncated)
118
+ ? {
119
+ ...(tree.attentionDescendants > 0
120
+ ? { requiresActionDescendants: tree.attentionDescendants }
121
+ : {}),
122
+ ...(tree.pausedDescendants > 0 ? { pausedDescendants: tree.pausedDescendants } : {}),
123
+ ...(tree.failedDescendants > 0 ? { failedDescendants: tree.failedDescendants } : {}),
124
+ ...(tree.truncated ? { truncated: true } : {}),
125
+ }
126
+ : undefined;
127
+ return {
128
+ id: session.id,
129
+ title: title.text,
130
+ status: session.status,
131
+ ...(title.truncated ? { titleTruncated: true } : {}),
132
+ ...(session.parentSessionId ? { parentSessionId: session.parentSessionId } : {}),
133
+ ...(goal
134
+ ? {
135
+ goal: {
136
+ status: session.goal!.status,
137
+ summary: goal.text,
138
+ ...(goal.truncated ? { summaryTruncated: true } : {}),
139
+ },
140
+ }
141
+ : {}),
142
+ ...(pause ? { pause } : {}),
143
+ ...(attention ? { attention } : {}),
144
+ ...(includeRelatedWork && session.workDiscovery ? { relatedWork: session.workDiscovery } : {}),
145
+ updatedAt: session.updatedAt,
146
+ };
147
+ }
148
+
149
+ export type SessionMcpMonitoringSource = {
150
+ goal: {
151
+ status: SessionGoalStatus;
152
+ text: string;
153
+ textOriginalChars: number;
154
+ evidence: string | null;
155
+ evidenceOriginalChars: number | null;
156
+ rationale: string | null;
157
+ rationaleOriginalChars: number | null;
158
+ pausedReason: string | null;
159
+ pausedReasonOriginalChars: number | null;
160
+ } | null;
161
+ progress: {
162
+ sequence: number;
163
+ text: string | null;
164
+ originalChars: number | null;
165
+ occurredAt: string;
166
+ } | null;
167
+ wait: { reason: string; until: string } | null;
168
+ };
169
+
170
+ /** Closed management projection shared by adapters; never includes session configuration.
171
+ * Apply target/ancestor authorization projection to session before calling. */
172
+ export function compactSessionMcpDetail(
173
+ session: Session,
174
+ monitoring: SessionMcpMonitoringSource,
175
+ queue: SessionQueueSnapshot | null,
176
+ ) {
177
+ const title = boundSessionMcpText(session.title, 200);
178
+ const pause = compactSessionMcpPause(session.effectiveControl);
179
+ const goal = monitoring.goal;
180
+ const goalText = goal ? boundSessionMcpText(goal.text, 600, goal.textOriginalChars) : null;
181
+ const optionalText = <Name extends string>(
182
+ name: Name,
183
+ text: string | null,
184
+ originalChars: number | null,
185
+ limit = 600,
186
+ ): Partial<Record<Name, string | null> & Record<`${Name}Truncated`, true>> => {
187
+ if (text === null) return {};
188
+ const bounded = boundSessionMcpText(text, limit, originalChars);
189
+ return {
190
+ [name]: bounded.text,
191
+ ...(bounded.truncated ? { [`${name}Truncated`]: true } : {}),
192
+ } as Partial<Record<Name, string | null> & Record<`${Name}Truncated`, true>>;
193
+ };
194
+ const progress = monitoring.progress;
195
+ const progressText = progress
196
+ ? boundSessionMcpText(progress.text, 600, progress.originalChars)
197
+ : null;
198
+ const waitReason = monitoring.wait ? boundSessionMcpText(monitoring.wait.reason) : null;
199
+ return {
200
+ id: session.id,
201
+ title: title.text,
202
+ status: session.status,
203
+ ...(title.truncated ? { titleTruncated: true } : {}),
204
+ ...(session.parentSessionId ? { parentSessionId: session.parentSessionId } : {}),
205
+ ...(session.activeTurnId ? { activeTurnId: session.activeTurnId } : {}),
206
+ lastSequence: session.lastSequence,
207
+ ...(goal
208
+ ? {
209
+ goal: {
210
+ status: goal.status,
211
+ summary: goalText!.text,
212
+ ...(goalText!.truncated ? { summaryTruncated: true } : {}),
213
+ ...optionalText("evidence", goal.evidence, goal.evidenceOriginalChars, 2000),
214
+ ...optionalText("rationale", goal.rationale, goal.rationaleOriginalChars),
215
+ ...optionalText("pausedReason", goal.pausedReason, goal.pausedReasonOriginalChars),
216
+ },
217
+ }
218
+ : {}),
219
+ ...(progress
220
+ ? {
221
+ progress: {
222
+ sequence: progress.sequence,
223
+ text: progressText!.text,
224
+ occurredAt: progress.occurredAt,
225
+ ...(progressText!.truncated ? { textTruncated: true } : {}),
226
+ },
227
+ }
228
+ : {}),
229
+ ...(pause ? { pause } : {}),
230
+ ...(monitoring.wait
231
+ ? {
232
+ wait: {
233
+ reason: waitReason!.text,
234
+ until: monitoring.wait.until,
235
+ ...(waitReason!.truncated ? { reasonTruncated: true } : {}),
236
+ },
237
+ }
238
+ : {}),
239
+ ...(queue
240
+ ? {
241
+ queue: {
242
+ queuedTurns: queue.items.length,
243
+ pendingInputs: queue.pendingInputs.length,
244
+ ...(queue.stoppingPreviousAttempt ? { stoppingPreviousAttempt: true } : {}),
245
+ },
246
+ }
247
+ : {}),
248
+ ...(session.effectiveControl.settlement || session.effectiveControl.backgroundCommandSettlement
249
+ ? {
250
+ stopping: {
251
+ ...(session.effectiveControl.settlement
252
+ ? { attempts: session.effectiveControl.settlement.attemptCount }
253
+ : {}),
254
+ ...(session.effectiveControl.backgroundCommandSettlement
255
+ ? {
256
+ backgroundCommands:
257
+ session.effectiveControl.backgroundCommandSettlement.commandCount,
258
+ }
259
+ : {}),
260
+ },
261
+ }
262
+ : {}),
263
+ updatedAt: session.updatedAt,
264
+ };
265
+ }
266
+
267
+ export type SessionMcpCompactListRow = ReturnType<typeof compactSessionMcpListRow>;
268
+ export type SessionMcpCompactDetail = ReturnType<typeof compactSessionMcpDetail>;
@@ -15,6 +15,14 @@ export const AUTOMATIC_SESSION_TITLE_MAX_GRAPHEMES = 80;
15
15
  */
16
16
  export const AUTOMATIC_SESSION_TITLE_FALLBACK = "New conversation";
17
17
 
18
+ // Session creation accepts a body far larger than a navigation label. Bound
19
+ // the source before any replace/split/normalization so a persisted large prompt
20
+ // cannot amplify memory or CPU on every client render.
21
+ const PROMPT_PREVIEW_SCAN_MAX_CODE_UNITS = 4_096;
22
+
23
+ const SESSION_ID_PATTERN =
24
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
25
+
18
26
  let titleSegmenter: Intl.Segmenter | null | undefined;
19
27
 
20
28
  const KNOWN_SENSITIVE_VALUE_PATTERNS = [
@@ -329,3 +337,95 @@ export function normalizeAutomaticSessionTitle(value: string): string | null {
329
337
  if (!title || !hasVisibleAutomaticTitleContent(title)) return null;
330
338
  return title;
331
339
  }
340
+
341
+ export type SessionDisplayTitleInput = {
342
+ id?: string | null | undefined;
343
+ title?: string | null | undefined;
344
+ titleSource?: "user" | "agent" | null | undefined;
345
+ initialMessage?: string | null | undefined;
346
+ metadata?: Readonly<Record<string, unknown>> | undefined;
347
+ };
348
+
349
+ export type SessionDisplayTitleOptions = {
350
+ /** Optional metadata fields to try before the opening-prompt preview. */
351
+ metadataKeys?: readonly string[] | undefined;
352
+ };
353
+
354
+ /**
355
+ * Derive a bounded, sensitive-safe preview from the opening prompt.
356
+ *
357
+ * Unsafe leading lines are skipped instead of forcing the whole session back
358
+ * to a generic label. This covers prompts that begin with a pasted URL or
359
+ * identifier followed by an ordinary natural-language request on the next
360
+ * line, without putting the rejected value into navigation surfaces.
361
+ */
362
+ export function deriveAutomaticSessionTitlePreview(value: unknown): string | null {
363
+ if (typeof value !== "string") return null;
364
+
365
+ const lines = value
366
+ .slice(0, PROMPT_PREVIEW_SCAN_MAX_CODE_UNITS)
367
+ .replace(/[\u0000-\u001f\u007f-\u009f]+/gu, "\n")
368
+ .split(/\n+/u);
369
+
370
+ for (const candidate of lines) {
371
+ const line = candidate.trim();
372
+ if (!line || containsSensitiveAutomaticSessionTitleValue(line)) continue;
373
+
374
+ const preview = boundAutomaticSessionTitle(line.replace(/\s+/gu, " "))
375
+ .replace(/[\s.!?,;:\-–—]+$/u, "")
376
+ .trim();
377
+ if (preview) return preview;
378
+ }
379
+
380
+ return null;
381
+ }
382
+
383
+ function automaticSessionReferenceTitle(id: unknown): string {
384
+ const sessionId = typeof id === "string" ? id.trim() : "";
385
+ return SESSION_ID_PATTERN.test(sessionId)
386
+ ? `Conversation ${sessionId.slice(0, 13)}`
387
+ : AUTOMATIC_SESSION_TITLE_FALLBACK;
388
+ }
389
+
390
+ /**
391
+ * Whether the durable title still represents the automatic-title pending state.
392
+ * A user-authored title always wins, even when its literal value is the marker.
393
+ */
394
+ export function sessionTitleIsPending(input: SessionDisplayTitleInput): boolean {
395
+ const title = input.title?.trim() ?? "";
396
+ return input.titleSource !== "user" && (!title || title === AUTOMATIC_SESSION_TITLE_FALLBACK);
397
+ }
398
+
399
+ /**
400
+ * Derive the title a human-facing client should display for a session.
401
+ *
402
+ * A semantic agent title or human rename wins. While automatic naming is still
403
+ * pending, clients show a short, sensitive-safe preview of the opening prompt.
404
+ * If no safe prompt text exists, a UUID-derived reference keeps real sessions
405
+ * distinguishable without exposing prompt bytes. The durable pending marker is
406
+ * therefore an internal lifecycle value rather than the ordinary visible name.
407
+ */
408
+ export function deriveSessionDisplayTitle(
409
+ input: SessionDisplayTitleInput,
410
+ options: SessionDisplayTitleOptions = {},
411
+ ): string {
412
+ const title = input.title?.trim() ?? "";
413
+ if (input.titleSource === "user") {
414
+ return title || automaticSessionReferenceTitle(input.id);
415
+ }
416
+ if (title && !sessionTitleIsPending(input)) {
417
+ return title;
418
+ }
419
+
420
+ for (const key of options.metadataKeys ?? []) {
421
+ const value = input.metadata?.[key];
422
+ if (typeof value === "string" && value.trim().length > 0) {
423
+ return value.trim();
424
+ }
425
+ }
426
+
427
+ return (
428
+ deriveAutomaticSessionTitlePreview(input.initialMessage) ??
429
+ automaticSessionReferenceTitle(input.id)
430
+ );
431
+ }