@agent-compose/sdk 0.8.1 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,27 +1,68 @@
1
1
  /**
2
- * Sandbox machine sizes + the E2B template aliases derived from them.
2
+ * Sandbox machine sizes — THE single source of the size vocabulary.
3
3
  *
4
- * A coarse hardware knob that maps to provider machine specs at create time:
5
- * Vercel honours it natively via `resources.vcpus`; E2B sizing is baked into
6
- * the template, so on E2B a size resolves to a pre-built per-size template.
4
+ * Everything that names a size (the server's `SANDBOX_DEFAULT_SIZE` env enum,
5
+ * the register/invoke zod schemas, the run + session row types, the CLI's
6
+ * `--size` flag, the dashboard pickers via `GET /v1/sandbox-sizes`) derives
7
+ * from `SANDBOX_SIZES` / `SandboxSize` here. Adding a size is a ONE-LINE edit
8
+ * to `SANDBOX_MACHINES`: the type, the enums, the E2B build matrix and the
9
+ * pickers all follow. Do NOT re-declare the union inline anywhere.
10
+ *
11
+ * A size is a coarse hardware knob that maps to provider machine specs:
12
+ * Vercel honours it natively via `resources.vcpus`; E2B sizing is BAKED INTO
13
+ * THE TEMPLATE (e2b 2.30.5 has no create-time cpu/mem knob — `NewSandbox`
14
+ * carries only `templateID`), so on E2B a size resolves to a pre-built
15
+ * per-size template.
7
16
  */
8
17
 
9
- /** Sandbox hardware SKU. Named for the actual machine spec (vCPU + RAM) rather
10
- * than abstract t-shirt sizes. Memory is always 2048 MB per vCPU:
11
- * 2vcpu-4gb = 2 vCPU / 4 GiB (Vercel's own default machine)
12
- * 4vcpu-8gb = 4 vCPU / 8 GiB
13
- * 8vcpu-16gb = 8 vCPU / 16 GiB (per-sandbox ceiling on STANDARD accounts —
14
- * probed live: 16 & 32 vCPU 400 on dev)
15
- * 32vcpu-64gb = 32 vCPU / 64 GiB (ENTERPRISE ONLY — standard accounts reject >8 vCPU) */
16
- export type SandboxSize = "2vcpu-4gb" | "4vcpu-8gb" | "8vcpu-16gb" | "32vcpu-64gb";
17
-
18
- /** SKU → Vercel vCPU count (RAM follows at 2048 MB/vCPU). */
19
- export const SANDBOX_VCPUS: Record<SandboxSize, number> = {
20
- "2vcpu-4gb": 2,
21
- "4vcpu-8gb": 4,
22
- "8vcpu-16gb": 8,
23
- "32vcpu-64gb": 32,
24
- };
18
+ /** Every sandbox hardware SKU, with its real machine spec. Named for the
19
+ * machine (vCPU + RAM) rather than abstract t-shirt sizes, so a size can
20
+ * never quietly mean something different than it says.
21
+ *
22
+ * RAM is 2048 MB/vCPU everywhere EXCEPT `8vcpu-8gb`, which exists because
23
+ * E2B caps a sandbox at 8 vCPU / 8192 MB (e2b.dev/docs/billing: Hobby and
24
+ * Pro both "8 vCPU / 8 GB", raised only by arrangement): 8 vCPU at the 2 GB
25
+ * rule would need 16 GiB and cannot be built. `8vcpu-8gb` is the CPU ceiling
26
+ * at the memory ceiling — the only way to get 8 cores on E2B today, and a
27
+ * spec already proven bakeable by the devbox (`E2B_DEVBOX_SPEC`).
28
+ *
29
+ * Adding an entry here automatically: widens `SandboxSize`, widens every
30
+ * derived enum, and — if it fits under the E2B caps — adds it to
31
+ * `E2B_TEMPLATE_SIZES`, which is what `infra/e2b-template/build.ts` and the
32
+ * `sandbox-images` CI job loop over. Two templates get baked per size, so
33
+ * the matrix is not free; see that workflow's header. */
34
+ export const SANDBOX_MACHINES = {
35
+ /** 1 vCPU / 2 GiB — the cheap floor. Plenty for a terminal session or a
36
+ * shell-shaped agent; tight for a big `bun install` or a browser. */
37
+ "1vcpu-2gb": { vcpus: 1, memoryMB: 2048 },
38
+ /** 2 vCPU / 4 GiB — the default (Vercel's own default machine too). */
39
+ "2vcpu-4gb": { vcpus: 2, memoryMB: 4096 },
40
+ /** 4 vCPU / 8 GiB — comfortable for builds and multi-tool agent turns. */
41
+ "4vcpu-8gb": { vcpus: 4, memoryMB: 8192 },
42
+ /** 8 vCPU / 8 GiB — E2B's per-sandbox CEILING (cores maxed at the memory
43
+ * cap). NOT expressible on Vercel, whose RAM follows vCPUs at 2 GB each. */
44
+ "8vcpu-8gb": { vcpus: 8, memoryMB: 8192 },
45
+ /** 8 vCPU / 16 GiB — Vercel only; exceeds E2B's 8 GiB memory cap. */
46
+ "8vcpu-16gb": { vcpus: 8, memoryMB: 16384 },
47
+ /** 32 vCPU / 64 GiB — Vercel Enterprise only; far past every E2B cap. */
48
+ "32vcpu-64gb": { vcpus: 32, memoryMB: 65536 },
49
+ } as const satisfies Record<string, { vcpus: number; memoryMB: number }>;
50
+
51
+ /** Sandbox hardware SKU. Derived from `SANDBOX_MACHINES` — never re-spelled
52
+ * as an inline union. */
53
+ export type SandboxSize = keyof typeof SANDBOX_MACHINES;
54
+
55
+ /** The vocabulary as an ordered, smallest-first array — the shape zod
56
+ * (`z.enum`), the CLI's `--size` validation, and the wire catalogue want.
57
+ * Ordering is the pickers' display order, so keep it ascending. */
58
+ export const SANDBOX_SIZES = Object.keys(SANDBOX_MACHINES) as readonly SandboxSize[] as
59
+ readonly [SandboxSize, ...SandboxSize[]];
60
+
61
+ /** SKU → Vercel vCPU count (Vercel's RAM follows automatically at 2048
62
+ * MB/vCPU — which is why `isVercelSupportedSize` exists). */
63
+ export const SANDBOX_VCPUS: Record<SandboxSize, number> = Object.fromEntries(
64
+ SANDBOX_SIZES.map((s) => [s, SANDBOX_MACHINES[s].vcpus]),
65
+ ) as Record<SandboxSize, number>;
25
66
 
26
67
  /** SDK fallback size when neither the caller nor the deployment specifies one.
27
68
  * Deliberately conservative — the OPERATIONAL default is the server's
@@ -29,37 +70,79 @@ export const SANDBOX_VCPUS: Record<SandboxSize, number> = {
29
70
  * small matters because Vercel rate-limits creation by vCPUs-per-window
30
71
  * (`api-sandboxes-vcpus-creation`); a large default 429s bursty/simultaneous
31
72
  * creates. Workloads that need more RAM/CPU declare `resources.size` on the
32
- * workflow rather than inflating the default for everyone. */
73
+ * workflow rather than inflating the default for everyone.
74
+ *
75
+ * NOT `1vcpu-2gb`: the floor is an opt-IN for cheap sessions, not a quiet
76
+ * downgrade of every existing run's machine. */
33
77
  export const DEFAULT_SANDBOX_SIZE: SandboxSize = "2vcpu-4gb";
34
78
 
35
- /** The E2B sizes we pre-build a template for. E2B sizing is template-baked
36
- * (no per-create cpu/mem knob), so honouring `resources.size` on E2B means
37
- * ONE pre-built template per size. `32vcpu-64gb` is absent (E2B has no
38
- * >8-vCPU equivalent). `8vcpu-16gb` is also absent: it needs 16 GiB RAM, but
39
- * the E2B account caps memory at 8 GiB (`Template.build` 400s with
40
- * "Memory can't be higher than 8192 MiB"). Add it back here (and rebuild the
41
- * templates) only once the account's memory limit is raised. The register/
42
- * invoke guards reject an unsupported E2B size before it can reach here. */
43
- export const E2B_TEMPLATE_SIZES: readonly SandboxSize[] = [
44
- "2vcpu-4gb",
45
- "4vcpu-8gb",
46
- ];
47
-
48
- /** Is `size` one E2B can be built/booted at? `32vcpu-64gb` (no >8-vCPU E2B
49
- * equivalent) and `8vcpu-16gb` (exceeds the account's 8 GiB memory cap) are
50
- * not — the guards lean on this so the "no E2B equivalent" decision lives in
51
- * exactly one place. */
79
+ /** SESSION default — deliberately one size up from the run default
80
+ * (2026-08-13): a session's sandbox carries the full desktop toolbelt
81
+ * (VS Code + Chromium + dockerd) plus the KasmVNC encoder at the 60fps
82
+ * cap, and that stack swap-thrashes on 4 GiB while the encoder starves on
83
+ * 2 shared vCPUs. Workflow runs keep DEFAULT_SANDBOX_SIZE — no desktop,
84
+ * no toolbelt weight. Sessions bill active time only (parked = storage),
85
+ * so the delta applies to active hours, not the fleet. */
86
+ export const SESSION_DEFAULT_SANDBOX_SIZE: SandboxSize = "4vcpu-8gb";
87
+
88
+ /** E2B's per-sandbox ceiling on the plans we run (e2b.dev/docs/billing —
89
+ * Hobby: "8 vCPU / 8 GB"; Pro: the same, "8+" only by arrangement with
90
+ * support). Recorded live too: `Template.build` 400s with "Memory can't be
91
+ * higher than 8192 MiB" past the memory cap.
92
+ *
93
+ * These two numbers are the ONLY knob for which sizes get an E2B template —
94
+ * raise them after E2B raises the account limit and the build matrix (and
95
+ * therefore the session picker) widens on its own. */
96
+ export const E2B_MAX_VCPUS = 8;
97
+ export const E2B_MAX_MEMORY_MB = 8192;
98
+
99
+ /** The E2B sizes we pre-build a template for — DERIVED from the caps, not
100
+ * hand-listed, so a new `SANDBOX_MACHINES` entry can never be offered
101
+ * without a template or omitted despite fitting. E2B sizing is
102
+ * template-baked (no per-create cpu/mem knob), so honouring `resources.size`
103
+ * on E2B means ONE pre-built template per size; `infra/e2b-template/build.ts`
104
+ * loops exactly this list. The register / invoke / session-spawn / resize
105
+ * guards all reject an unsupported E2B size before it can reach a create. */
106
+ export const E2B_TEMPLATE_SIZES: readonly SandboxSize[] = SANDBOX_SIZES.filter(
107
+ (s) => SANDBOX_MACHINES[s].vcpus <= E2B_MAX_VCPUS
108
+ && SANDBOX_MACHINES[s].memoryMB <= E2B_MAX_MEMORY_MB,
109
+ );
110
+
111
+ /** Is `size` one E2B can be built/booted at? False for the sizes past E2B's
112
+ * 8 vCPU / 8 GiB ceiling (`8vcpu-16gb`, `32vcpu-64gb`) — those run on Vercel.
113
+ * The "no E2B equivalent" decision lives in exactly one place: the caps. */
52
114
  export function isE2bSupportedSize(size: SandboxSize): boolean {
53
115
  return E2B_TEMPLATE_SIZES.includes(size);
54
116
  }
55
117
 
56
- /** Machine spec for a SandboxSize, in the shape `Template.build` wants. RAM is
57
- * always 2048 MB/vCPU, matching the size name + Vercel parity
58
- * (`SANDBOX_VCPUS` × 2048). Used by `infra/e2b-template/build.ts` to stamp the
59
- * per-size base + agent-env templates. */
118
+ /** Vercel's fixed memory-per-vCPU ratio. Vercel takes `resources.vcpus` and
119
+ * allocates RAM itself at this rate — there is no independent memory knob. */
120
+ export const VERCEL_MEMORY_MB_PER_VCPU = 2048;
121
+
122
+ /** Is `size` expressible on Vercel? Only when its RAM matches what Vercel
123
+ * would allocate for that vCPU count — otherwise asking for it would hand
124
+ * the caller a machine that does not match the name (`8vcpu-8gb` would come
125
+ * back with 16 GiB). Vercel's own ceiling (32 vCPU, Enterprise) is a plan
126
+ * matter, not a shape matter, so it is not encoded here. */
127
+ export function isVercelSupportedSize(size: SandboxSize): boolean {
128
+ const m = SANDBOX_MACHINES[size];
129
+ return m.memoryMB === m.vcpus * VERCEL_MEMORY_MB_PER_VCPU;
130
+ }
131
+
132
+ /** Machine spec for a SandboxSize, in the shape `Template.build` wants. Used
133
+ * by `infra/e2b-template/build.ts` to stamp the per-size base + agent-env
134
+ * templates. Reads the explicit table rather than deriving RAM from vCPUs —
135
+ * `8vcpu-8gb` is deliberately off the 2048 MB/vCPU line. */
60
136
  export function e2bMachineSpec(size: SandboxSize): { cpuCount: number; memoryMB: number } {
61
- const cpuCount = SANDBOX_VCPUS[size];
62
- return { cpuCount, memoryMB: cpuCount * 2048 };
137
+ const m = SANDBOX_MACHINES[size];
138
+ return { cpuCount: m.vcpus, memoryMB: m.memoryMB };
139
+ }
140
+
141
+ /** Human label for a size — "2 vCPU · 4 GB". The wire catalogue carries it so
142
+ * the dashboard never has to parse the id back into numbers. */
143
+ export function sandboxSizeLabel(size: SandboxSize): string {
144
+ const m = SANDBOX_MACHINES[size];
145
+ return `${m.vcpus} vCPU · ${Math.round(m.memoryMB / 1024)} GB`;
63
146
  }
64
147
 
65
148
  /** Stable E2B template ALIAS for the platform base at a given size
package/src/sandbox.ts CHANGED
@@ -23,10 +23,18 @@ export { DOT_SEGMENT_PATH_RE2, toVercelNetworkPolicy, toE2bNetwork } from "./san
23
23
 
24
24
  export type { SandboxSize } from "./sandbox/sizes.js";
25
25
  export {
26
+ SANDBOX_SIZES,
27
+ SANDBOX_MACHINES,
26
28
  SANDBOX_VCPUS,
27
29
  DEFAULT_SANDBOX_SIZE,
30
+ SESSION_DEFAULT_SANDBOX_SIZE,
28
31
  E2B_TEMPLATE_SIZES,
32
+ E2B_MAX_VCPUS,
33
+ E2B_MAX_MEMORY_MB,
34
+ VERCEL_MEMORY_MB_PER_VCPU,
29
35
  isE2bSupportedSize,
36
+ isVercelSupportedSize,
37
+ sandboxSizeLabel,
30
38
  e2bMachineSpec,
31
39
  e2bBaseTemplate,
32
40
  e2bAgentEnvTemplate,
@@ -104,6 +104,9 @@ export interface ConversationMessageRow {
104
104
  reactions: Record<string, string[]>;
105
105
  replyCount: number;
106
106
  lastReplyAt: string | null;
107
+ /** When the author last edited the content; null/absent = never edited
108
+ * (absent on older servers). */
109
+ editedAt?: string | null;
107
110
  createdAt: string;
108
111
  /** Thread-root facepile (≤3) — present only on roots with replies. */
109
112
  replyAuthors?: Array<{ kind: string; id: string | null }>;
@@ -167,6 +170,12 @@ export interface ConversationDetail {
167
170
  * affordances only; the server remains the authority on every action.
168
171
  * Public channels report implicit `write` for non-member teammates. */
169
172
  viewerRole: ConversationMemberRole;
173
+ /** Provenance when `viewerRole` is DERIVED rather than held: the project
174
+ * (ADR-0045 read floor) or attached channel (ADR-0057) the viewer
175
+ * follows this conversation through. Null/absent for real members —
176
+ * clients use it to explain read-only honestly ("You follow this
177
+ * session through <project>…"). Absent on older servers. */
178
+ viewerRoleVia?: { kind: "project" | "channel"; id: string; name: string | null } | null;
170
179
  /** SSE replay watermark: the conversation's highest durable stream-event
171
180
  * id at hydrate time — everything at or below it is already folded into
172
181
  * `messages`. Seed the stream's first `Last-Event-ID` from it instead of
@@ -251,6 +260,15 @@ export interface CreateCloudSessionInput {
251
260
  * seeds re-applied on every fresh acquire. Paths under .claude/ only;
252
261
  * strict base64; 256KB decoded total (server-capped). */
253
262
  seedFiles?: Array<{ path: string; contentB64: string; mode: "write" | "append" }>;
263
+ /** DIFF REVIEW spawn: the conversation whose proposed changes this
264
+ * session is born to review. Server-resolved: on a graph-plane drive
265
+ * the new session's branch is minted as a FORK of the reviewed branch
266
+ * (its working dir IS the proposal, `.review/` comparison materials
267
+ * included); on the legacy plane the spawn degrades to an ordinary
268
+ * session. Requires a human caller and a chat session on the reviewed
269
+ * session's factory; 409 `review_of_review` when the target is itself
270
+ * a review session. */
271
+ reviewOfConversationId?: string;
254
272
  }
255
273
 
256
274
  export interface CloudSessionCreated {
@@ -327,6 +345,15 @@ export interface SessionForked {
327
345
  conversationId: string;
328
346
  }
329
347
 
348
+ /** Result of holding a session's background-work busy lease
349
+ * (`POST /conversations/:id/background-work`). While the lease is live the
350
+ * between-turns park/suspend leaves the session's VM running; it lapses on
351
+ * its own — re-hold to extend. */
352
+ export interface BackgroundWorkHeld {
353
+ /** ISO timestamp the lease now runs to. */
354
+ leaseUntil: string;
355
+ }
356
+
330
357
  // ── Session branch proposals (ADR-0053) ─────────────────────────────────────
331
358
  // Every cloud session works on its own factory-drive branch; the whole branch
332
359
  // is the unit of review, like a PR. Wire shapes mirror
@@ -364,6 +391,121 @@ export interface SessionChangeSet {
364
391
  truncated: boolean;
365
392
  /** Open `factory_file_conflicts` rows from this session's prior merges. */
366
393
  openConflicts: number;
394
+ /** MERGE GATE (merge-gate spec, additive — absent on older servers):
395
+ * null = ungated. Present: whether THIS caller's merge lands directly
396
+ * (`canMerge` — an owner or named approver) or stages a kind='merge'
397
+ * approval instead (`mergeSessionChanges` answers 202
398
+ * `SessionMergeGated`), plus the resolved approver set. */
399
+ mergeGate?: {
400
+ enabled: true;
401
+ canMerge: boolean;
402
+ approvers: Array<{ userId: string; label: string | null }>;
403
+ } | null;
404
+ /** The OPEN kind='merge' approval already waiting on this session's
405
+ * branch, if any (additive — absent on older servers). */
406
+ pendingApprovalId?: string | null;
407
+ /** DIFF REVIEW (additive — absent on older servers): notes written back
408
+ * by a spawned review session, whether they predate the served diff
409
+ * (`reviewStale` — the changed-file signature no longer matches), the
410
+ * bound review session's conversation + whether it is still live, and
411
+ * whether the deployment offers reviews at all (`reviewEnabled`; false
412
+ * hides the affordance). Reviews are USER-TRIGGERED only — binding
413
+ * rides `POST /conversations/:id/changes/review`, and notes arrive
414
+ * through the review session's own gated write-back. */
415
+ reviewNotes?: string | null;
416
+ reviewStale?: boolean;
417
+ reviewEnabled?: boolean;
418
+ reviewSessionConversationId?: string | null;
419
+ reviewSessionActive?: boolean;
420
+ /** STRUCTURED review suggestions beside the notes (additive — absent on
421
+ * older servers): individually actionable {id, path, title, rationale,
422
+ * patch} entries the review session wrote back, status-stamped
423
+ * `"proposed"` by the server (`"accepted"`/`"rejected"` are reserved for
424
+ * the human decision pass). */
425
+ reviewSuggestions?: ReviewSuggestion[];
426
+ /** THE REVIEWER MARKER (additive — absent on older servers): non-null ⇒
427
+ * THIS session was born to review that conversation's diff. Its own
428
+ * branch is a fork of the reviewed branch and can never merge
429
+ * (`mergeable` false, merge 409s `fork_branch_unmergeable`), and it is
430
+ * refused as a review target (409 `review_of_review`). */
431
+ reviewOfConversationId?: string | null;
432
+ }
433
+
434
+ /** One STRUCTURED, individually actionable suggestion a review session
435
+ * wrote back beside its notes (`SessionChangeSet.reviewSuggestions`). */
436
+ export interface ReviewSuggestion {
437
+ /** Reviewer-minted stable id — survives full-replace re-posts. */
438
+ id: string;
439
+ /** The changed file the suggestion targets (always in the change set —
440
+ * the writeback refuses paths outside it). */
441
+ path: string;
442
+ title: string;
443
+ rationale: string;
444
+ /** Unified-diff hunk targeting the PROPOSED side of `path`. */
445
+ patch: string;
446
+ /** Server-stamped `"proposed"` at writeback; the HUMAN decision routes
447
+ * (`POST …/changes/suggestions/:suggestionId/decide` / `…/decide-all`)
448
+ * are the only writers of the rest — `"accepted"` (the patch landed on
449
+ * the session branch), `"rejected"`, or `"stale"` (an accept found the
450
+ * file drifted since review; nothing was written). */
451
+ status: "proposed" | "accepted" | "rejected" | "stale";
452
+ }
453
+
454
+ /** `POST /conversations/:id/changes/suggestions/:suggestionId/decide` —
455
+ * the post-call truth for that suggestion (an accept that found drift
456
+ * answers `"stale"`; repeats on a settled row are no-ops). */
457
+ export interface ReviewSuggestionDecision {
458
+ object: "review_suggestion_decision";
459
+ conversationId: string;
460
+ id: string;
461
+ status: ReviewSuggestion["status"];
462
+ }
463
+
464
+ /** `POST /conversations/:id/changes/suggestions/decide-all` — every stored
465
+ * suggestion's post-call status (only `"proposed"` rows flip). */
466
+ export interface ReviewSuggestionDecisions {
467
+ object: "review_suggestion_decisions";
468
+ conversationId: string;
469
+ results: Array<{ id: string; status: ReviewSuggestion["status"] }>;
470
+ }
471
+
472
+ /** `GET /conversations/:id/changes/stats` — the changes chip's aggregate
473
+ * +added/−removed line counts, cached server-side per (base, theirsHead).
474
+ * `unavailable` = no cheap head anchors (legacy plane, unbranched) — the
475
+ * chip degrades to its file count, never fake zeros. */
476
+ export type SessionChangeStats =
477
+ | { object: "session_change_stats"; conversationId: string; state: "unavailable" }
478
+ | {
479
+ object: "session_change_stats"; conversationId: string; state: "ok";
480
+ additions: number; deletions: number; files: number;
481
+ /** True when the count is partial (list truncated / file cap). */
482
+ truncated: boolean;
483
+ base: string; theirsHead: string;
484
+ };
485
+
486
+ /** `POST /conversations/:id/changes/review` — binds a just-spawned review
487
+ * session to the reviewed session and stamps the diff signature the
488
+ * review covers. */
489
+ export interface SessionDiffReviewBound {
490
+ object: "session_diff_review";
491
+ conversationId: string;
492
+ reviewSessionConversationId: string;
493
+ reviewDiffSignature: string;
494
+ }
495
+
496
+ /** 202 from `POST /conversations/:id/changes/merge` on a merge-GATED
497
+ * session when the caller is not an approver: nothing merged — the ask
498
+ * froze into (`approval_required`) or converged on (`approval_pending`) a
499
+ * kind='merge' approval routed to the approvers. */
500
+ export interface SessionMergeGated {
501
+ object: "session_merge_gated";
502
+ conversationId: string;
503
+ code: "approval_required" | "approval_pending";
504
+ approvalId: string;
505
+ branch?: string;
506
+ changeCount?: number;
507
+ openConflicts?: number;
508
+ approvers: Array<{ userId: string; label: string | null }>;
367
509
  }
368
510
 
369
511
  /** Per-file accounting of one session branch merge — the merge core's
@@ -435,6 +577,15 @@ export interface SendConversationMessageInput {
435
577
  * work and a coalesced follow-up turn will answer it. NOT an error. */
436
578
  export type ConversationTurnState = "none" | "started" | "queued";
437
579
 
580
+ /** One @-mentioned user the server DROPPED as a non-member — the mention
581
+ * ping went to nobody, and the response says so instead of staying
582
+ * silent. `name` is the label the sender's own text carried for the
583
+ * mention (picked in the typeahead — no new information). */
584
+ export interface UnnotifiedMention {
585
+ userId: string;
586
+ name: string;
587
+ }
588
+
438
589
  export interface SendConversationMessageResult {
439
590
  /** The persisted user message's id. */
440
591
  messageId: string;
@@ -447,6 +598,10 @@ export interface SendConversationMessageResult {
447
598
  * each runs its own turn in its own conversation. Absent on older
448
599
  * servers and non-channel sends. */
449
600
  relayedSessionIds?: string[];
601
+ /** Mentioned users whose ping was dropped by the member filter (a
602
+ * private channel pings members only; a DM pings only the pair) —
603
+ * present only when non-empty. Absent on older servers. */
604
+ unnotifiedMentions?: UnnotifiedMention[];
450
605
  }
451
606
 
452
607
  /** Response of the presence heartbeat (ADR-0037 §6): a fresh agent-liveness
@@ -313,6 +313,27 @@ export interface ApiKey {
313
313
  * shown once at creation, never retrievable again. */
314
314
  export interface ApiKeyCreated extends ApiKey { key: string }
315
315
 
316
+ /** Options for `GET /api-keys` — the list is bounded and keyset-paginated
317
+ * (every run step / session boot mints a key row, so "all keys ever" is
318
+ * unbounded by construction). */
319
+ export interface ListApiKeysOptions {
320
+ /** Page size, 1–200 (server default 100). */
321
+ limit?: number;
322
+ /** `active` = unrevoked + unexpired only; `all` (default) includes
323
+ * revoked/expired history. */
324
+ status?: "active" | "all";
325
+ /** Opaque cursor from a previous page's `nextCursor`. */
326
+ cursor?: string;
327
+ }
328
+
329
+ /** One page of `GET /api-keys`. */
330
+ export interface ApiKeyPage {
331
+ data: ApiKey[];
332
+ hasMore: boolean;
333
+ /** Pass as `cursor` to fetch the next page; null on the last page. */
334
+ nextCursor: string | null;
335
+ }
336
+
316
337
  /** Single rollup row from `GET /api/v1/usage`. */
317
338
  export interface UsageRollupRow {
318
339
  eventType: string;
@@ -333,7 +354,10 @@ export interface UsageResponse {
333
354
  // ── GitHub-linked drive directories (ADR-0030 P1) ───────────────────────────
334
355
 
335
356
  /** One drive-directory ⇄ GitHub-repo link (`/factories/:slug/repo-links`).
336
- * The webhook secret ref is operator plumbing and never on the wire. */
357
+ * Multi-branch model: ONE link row per (repo, branch); sibling branches
358
+ * of a repo occupy sibling `base@<branch>` placements. The webhook secret
359
+ * ref is operator plumbing and never on the wire — `webhookConfigured` /
360
+ * `webhookVerifiedAt` carry the honest connect status instead. */
337
361
  export interface DriveRepoLink {
338
362
  id: string;
339
363
  factoryId: string;
@@ -343,26 +367,52 @@ export interface DriveRepoLink {
343
367
  provider: string;
344
368
  /** "org/repo". */
345
369
  repoFullName: string;
346
- /** The GitHub branch drive `main` syncs with. */
370
+ /** The GitHub branch this link's placement syncs with. */
347
371
  trackedBranch: string;
348
372
  connectorGrantId: string | null;
349
373
  /** v1 links are always 'write' (the round trip is the feature). */
350
374
  access: "read" | "write";
375
+ /** A webhook secret exists for this link (registration done). NOT proof
376
+ * of delivery — that is `webhookVerifiedAt`. */
377
+ webhookConfigured: boolean;
378
+ /** When a signed GitHub delivery last PROVED the webhook delivers; null
379
+ * = unproven (the link syncs by poll). */
380
+ webhookVerifiedAt: string | null;
381
+ /** Two-way sync: drive edits under the prefix push back to the tracked
382
+ * branch. ON by default for new links. */
383
+ pushOutEnabled: boolean;
384
+ /** Follow-all mode (stamped identically on every sibling link of one
385
+ * repo): pushes to NEW branches auto-link them; GitHub branch deletes
386
+ * auto-unlink. False = explicit selection. */
387
+ followAllBranches: boolean;
388
+ /** Branches follow-all could NOT link (with the reason) — stamped on
389
+ * the repo's PRIMARY link row only; empty everywhere else. */
390
+ autoLinkSkips: Array<{ branch: string; reason: string; at: string }>;
351
391
  lastSyncedGitSha: string | null;
352
392
  lastSyncedAcgCommit: string | null;
353
- syncState: "idle" | "syncing" | "diverged" | "reauth_required";
354
- visibility: "team" | "restricted";
393
+ syncState: "idle" | "syncing" | "pushing" | "error" | "conflict" | "diverged" | "reauth_required";
394
+ /** Human-readable detail when syncState is 'error' or 'conflict'. */
395
+ syncError: string | null;
355
396
  createdBy: string | null;
356
397
  lastSyncedAt: string | null;
357
398
  createdAt: string;
358
399
  }
359
400
 
360
401
  /** Input for `POST /factories/:slug/repo-links`. Requires the drive to be
361
- * graph-authoritative (ADR-0058 Phase 4 promoted) — 409 otherwise. */
402
+ * graph-authoritative (ADR-0058 Phase 4 promoted) — 409 otherwise.
403
+ * Exactly ONE of `followAllBranches` / `trackedBranches`:
404
+ * `followAllBranches: true` (the default connect mode) links EVERY
405
+ * branch — default branch primary at the plain `dirPrefix` — and keeps
406
+ * following live (new branches auto-link on push, deleted branches
407
+ * auto-unlink; capped at 100 branches). Explicit `trackedBranches`:
408
+ * index 0 is the PRIMARY and keeps the plain placement; every additional
409
+ * branch lands at the sibling `dirPrefix@<sanitized-branch>` placement. */
362
410
  export interface CreateDriveRepoLinkInput {
363
411
  dirPrefix: string;
364
412
  repoFullName: string;
365
- trackedBranch: string;
413
+ trackedBranches?: string[];
414
+ followAllBranches?: boolean;
366
415
  connectorGrantId: string;
367
- visibility?: "team" | "restricted";
416
+ /** Two-way sync ("push-out") — defaults to TRUE for new links. */
417
+ pushOut?: boolean;
368
418
  }
@@ -303,7 +303,8 @@ export interface ListRunsOptions {
303
303
  factorySlug?: string;
304
304
  /** Substring match on the registered workflow name (`metadata._workflow`). */
305
305
  workflow?: string;
306
- /** Substring match across title / task title / branch / run id. */
306
+ /** Substring match across title / task title / branch — or an exact run
307
+ * id (full UUID). */
307
308
  search?: string;
308
309
  outcome?: string;
309
310
  sort?: "newest" | "oldest" | "fastest" | "slowest";
@@ -154,11 +154,12 @@ export interface InvokePolicy {
154
154
  * today; kept as its own object so finer controls (disk, gpu, …) can be
155
155
  * added later without reshaping `WorkflowMetadata`. */
156
156
  export interface SandboxResources {
157
- /** Machine hardware SKU — one of the `SandboxSize` vCPU strings
158
- * (`2vcpu-4gb` | `4vcpu-8gb` | `8vcpu-16gb` | `32vcpu-64gb`). Maps to
159
- * provider specs at create time (Vercel: 2 / 4 / 8 / 32 vCPU, 2048 MB RAM
160
- * per vCPU). Omit → the smallest SKU. E2B sizing is template-defined and
161
- * ignores this. */
157
+ /** Machine hardware SKU. The vocabulary is `SANDBOX_SIZES` in
158
+ * `sandbox/sizes.ts` — the single source; do not restate it here or
159
+ * anywhere else. Maps to provider specs at create time: Vercel takes the
160
+ * vCPU count and allocates RAM at 2048 MB/vCPU; E2B has no create-time
161
+ * cpu/mem knob at all, so the size selects a PRE-BUILT per-size template
162
+ * (`E2B_TEMPLATE_SIZES`). Omit → `DEFAULT_SANDBOX_SIZE`. */
162
163
  size?: SandboxSize;
163
164
  /** Sandbox provider this workflow's runs execute on — `"vercel"` or
164
165
  * `"e2b"`. Optional and additive: omit and the run resolves to the