@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.
@@ -101,6 +101,9 @@ export interface ConversationMessageRow {
101
101
  reactions: Record<string, string[]>;
102
102
  replyCount: number;
103
103
  lastReplyAt: string | null;
104
+ /** When the author last edited the content; null/absent = never edited
105
+ * (absent on older servers). */
106
+ editedAt?: string | null;
104
107
  createdAt: string;
105
108
  /** Thread-root facepile (≤3) — present only on roots with replies. */
106
109
  replyAuthors?: Array<{
@@ -172,6 +175,16 @@ export interface ConversationDetail {
172
175
  * affordances only; the server remains the authority on every action.
173
176
  * Public channels report implicit `write` for non-member teammates. */
174
177
  viewerRole: ConversationMemberRole;
178
+ /** Provenance when `viewerRole` is DERIVED rather than held: the project
179
+ * (ADR-0045 read floor) or attached channel (ADR-0057) the viewer
180
+ * follows this conversation through. Null/absent for real members —
181
+ * clients use it to explain read-only honestly ("You follow this
182
+ * session through <project>…"). Absent on older servers. */
183
+ viewerRoleVia?: {
184
+ kind: "project" | "channel";
185
+ id: string;
186
+ name: string | null;
187
+ } | null;
175
188
  /** SSE replay watermark: the conversation's highest durable stream-event
176
189
  * id at hydrate time — everything at or below it is already folded into
177
190
  * `messages`. Seed the stream's first `Last-Event-ID` from it instead of
@@ -257,6 +270,15 @@ export interface CreateCloudSessionInput {
257
270
  contentB64: string;
258
271
  mode: "write" | "append";
259
272
  }>;
273
+ /** DIFF REVIEW spawn: the conversation whose proposed changes this
274
+ * session is born to review. Server-resolved: on a graph-plane drive
275
+ * the new session's branch is minted as a FORK of the reviewed branch
276
+ * (its working dir IS the proposal, `.review/` comparison materials
277
+ * included); on the legacy plane the spawn degrades to an ordinary
278
+ * session. Requires a human caller and a chat session on the reviewed
279
+ * session's factory; 409 `review_of_review` when the target is itself
280
+ * a review session. */
281
+ reviewOfConversationId?: string;
260
282
  }
261
283
  export interface CloudSessionCreated {
262
284
  conversation: ConversationRow;
@@ -326,6 +348,14 @@ export interface SessionForked {
326
348
  /** The new child conversation to switch to. */
327
349
  conversationId: string;
328
350
  }
351
+ /** Result of holding a session's background-work busy lease
352
+ * (`POST /conversations/:id/background-work`). While the lease is live the
353
+ * between-turns park/suspend leaves the session's VM running; it lapses on
354
+ * its own — re-hold to extend. */
355
+ export interface BackgroundWorkHeld {
356
+ /** ISO timestamp the lease now runs to. */
357
+ leaseUntil: string;
358
+ }
329
359
  /** One changed file on a session's drive branch relative to `main`. */
330
360
  export interface SessionFileChange {
331
361
  /** Factory-relative path. */
@@ -357,6 +387,131 @@ export interface SessionChangeSet {
357
387
  truncated: boolean;
358
388
  /** Open `factory_file_conflicts` rows from this session's prior merges. */
359
389
  openConflicts: number;
390
+ /** MERGE GATE (merge-gate spec, additive — absent on older servers):
391
+ * null = ungated. Present: whether THIS caller's merge lands directly
392
+ * (`canMerge` — an owner or named approver) or stages a kind='merge'
393
+ * approval instead (`mergeSessionChanges` answers 202
394
+ * `SessionMergeGated`), plus the resolved approver set. */
395
+ mergeGate?: {
396
+ enabled: true;
397
+ canMerge: boolean;
398
+ approvers: Array<{
399
+ userId: string;
400
+ label: string | null;
401
+ }>;
402
+ } | null;
403
+ /** The OPEN kind='merge' approval already waiting on this session's
404
+ * branch, if any (additive — absent on older servers). */
405
+ pendingApprovalId?: string | null;
406
+ /** DIFF REVIEW (additive — absent on older servers): notes written back
407
+ * by a spawned review session, whether they predate the served diff
408
+ * (`reviewStale` — the changed-file signature no longer matches), the
409
+ * bound review session's conversation + whether it is still live, and
410
+ * whether the deployment offers reviews at all (`reviewEnabled`; false
411
+ * hides the affordance). Reviews are USER-TRIGGERED only — binding
412
+ * rides `POST /conversations/:id/changes/review`, and notes arrive
413
+ * through the review session's own gated write-back. */
414
+ reviewNotes?: string | null;
415
+ reviewStale?: boolean;
416
+ reviewEnabled?: boolean;
417
+ reviewSessionConversationId?: string | null;
418
+ reviewSessionActive?: boolean;
419
+ /** STRUCTURED review suggestions beside the notes (additive — absent on
420
+ * older servers): individually actionable {id, path, title, rationale,
421
+ * patch} entries the review session wrote back, status-stamped
422
+ * `"proposed"` by the server (`"accepted"`/`"rejected"` are reserved for
423
+ * the human decision pass). */
424
+ reviewSuggestions?: ReviewSuggestion[];
425
+ /** THE REVIEWER MARKER (additive — absent on older servers): non-null ⇒
426
+ * THIS session was born to review that conversation's diff. Its own
427
+ * branch is a fork of the reviewed branch and can never merge
428
+ * (`mergeable` false, merge 409s `fork_branch_unmergeable`), and it is
429
+ * refused as a review target (409 `review_of_review`). */
430
+ reviewOfConversationId?: string | null;
431
+ }
432
+ /** One STRUCTURED, individually actionable suggestion a review session
433
+ * wrote back beside its notes (`SessionChangeSet.reviewSuggestions`). */
434
+ export interface ReviewSuggestion {
435
+ /** Reviewer-minted stable id — survives full-replace re-posts. */
436
+ id: string;
437
+ /** The changed file the suggestion targets (always in the change set —
438
+ * the writeback refuses paths outside it). */
439
+ path: string;
440
+ title: string;
441
+ rationale: string;
442
+ /** Unified-diff hunk targeting the PROPOSED side of `path`. */
443
+ patch: string;
444
+ /** Server-stamped `"proposed"` at writeback; the HUMAN decision routes
445
+ * (`POST …/changes/suggestions/:suggestionId/decide` / `…/decide-all`)
446
+ * are the only writers of the rest — `"accepted"` (the patch landed on
447
+ * the session branch), `"rejected"`, or `"stale"` (an accept found the
448
+ * file drifted since review; nothing was written). */
449
+ status: "proposed" | "accepted" | "rejected" | "stale";
450
+ }
451
+ /** `POST /conversations/:id/changes/suggestions/:suggestionId/decide` —
452
+ * the post-call truth for that suggestion (an accept that found drift
453
+ * answers `"stale"`; repeats on a settled row are no-ops). */
454
+ export interface ReviewSuggestionDecision {
455
+ object: "review_suggestion_decision";
456
+ conversationId: string;
457
+ id: string;
458
+ status: ReviewSuggestion["status"];
459
+ }
460
+ /** `POST /conversations/:id/changes/suggestions/decide-all` — every stored
461
+ * suggestion's post-call status (only `"proposed"` rows flip). */
462
+ export interface ReviewSuggestionDecisions {
463
+ object: "review_suggestion_decisions";
464
+ conversationId: string;
465
+ results: Array<{
466
+ id: string;
467
+ status: ReviewSuggestion["status"];
468
+ }>;
469
+ }
470
+ /** `GET /conversations/:id/changes/stats` — the changes chip's aggregate
471
+ * +added/−removed line counts, cached server-side per (base, theirsHead).
472
+ * `unavailable` = no cheap head anchors (legacy plane, unbranched) — the
473
+ * chip degrades to its file count, never fake zeros. */
474
+ export type SessionChangeStats = {
475
+ object: "session_change_stats";
476
+ conversationId: string;
477
+ state: "unavailable";
478
+ } | {
479
+ object: "session_change_stats";
480
+ conversationId: string;
481
+ state: "ok";
482
+ additions: number;
483
+ deletions: number;
484
+ files: number;
485
+ /** True when the count is partial (list truncated / file cap). */
486
+ truncated: boolean;
487
+ base: string;
488
+ theirsHead: string;
489
+ };
490
+ /** `POST /conversations/:id/changes/review` — binds a just-spawned review
491
+ * session to the reviewed session and stamps the diff signature the
492
+ * review covers. */
493
+ export interface SessionDiffReviewBound {
494
+ object: "session_diff_review";
495
+ conversationId: string;
496
+ reviewSessionConversationId: string;
497
+ reviewDiffSignature: string;
498
+ }
499
+ /** 202 from `POST /conversations/:id/changes/merge` on a merge-GATED
500
+ * session when the caller is not an approver: nothing merged — the ask
501
+ * froze into (`approval_required`) or converged on (`approval_pending`) a
502
+ * kind='merge' approval routed to the approvers. */
503
+ export interface SessionMergeGated {
504
+ object: "session_merge_gated";
505
+ conversationId: string;
506
+ code: "approval_required" | "approval_pending";
507
+ approvalId: string;
508
+ branch?: string;
509
+ changeCount?: number;
510
+ openConflicts?: number;
511
+ approvers: Array<{
512
+ userId: string;
513
+ label: string | null;
514
+ }>;
360
515
  }
361
516
  /** Per-file accounting of one session branch merge — the merge core's
362
517
  * honest report, returned verbatim. */
@@ -425,6 +580,14 @@ export interface SendConversationMessageInput {
425
580
  * streaming; `queued` = a turn is already in flight — the message is owed
426
581
  * work and a coalesced follow-up turn will answer it. NOT an error. */
427
582
  export type ConversationTurnState = "none" | "started" | "queued";
583
+ /** One @-mentioned user the server DROPPED as a non-member — the mention
584
+ * ping went to nobody, and the response says so instead of staying
585
+ * silent. `name` is the label the sender's own text carried for the
586
+ * mention (picked in the typeahead — no new information). */
587
+ export interface UnnotifiedMention {
588
+ userId: string;
589
+ name: string;
590
+ }
428
591
  export interface SendConversationMessageResult {
429
592
  /** The persisted user message's id. */
430
593
  messageId: string;
@@ -437,6 +600,10 @@ export interface SendConversationMessageResult {
437
600
  * each runs its own turn in its own conversation. Absent on older
438
601
  * servers and non-channel sends. */
439
602
  relayedSessionIds?: string[];
603
+ /** Mentioned users whose ping was dropped by the member filter (a
604
+ * private channel pings members only; a DM pings only the pair) —
605
+ * present only when non-empty. Absent on older servers. */
606
+ unnotifiedMentions?: UnnotifiedMention[];
440
607
  }
441
608
  /** Response of the presence heartbeat (ADR-0037 §6): a fresh agent-liveness
442
609
  * snapshot + the roster TTL. The attach roster itself is NOT returned —
@@ -284,6 +284,25 @@ export interface ApiKey {
284
284
  export interface ApiKeyCreated extends ApiKey {
285
285
  key: string;
286
286
  }
287
+ /** Options for `GET /api-keys` — the list is bounded and keyset-paginated
288
+ * (every run step / session boot mints a key row, so "all keys ever" is
289
+ * unbounded by construction). */
290
+ export interface ListApiKeysOptions {
291
+ /** Page size, 1–200 (server default 100). */
292
+ limit?: number;
293
+ /** `active` = unrevoked + unexpired only; `all` (default) includes
294
+ * revoked/expired history. */
295
+ status?: "active" | "all";
296
+ /** Opaque cursor from a previous page's `nextCursor`. */
297
+ cursor?: string;
298
+ }
299
+ /** One page of `GET /api-keys`. */
300
+ export interface ApiKeyPage {
301
+ data: ApiKey[];
302
+ hasMore: boolean;
303
+ /** Pass as `cursor` to fetch the next page; null on the last page. */
304
+ nextCursor: string | null;
305
+ }
287
306
  /** Single rollup row from `GET /api/v1/usage`. */
288
307
  export interface UsageRollupRow {
289
308
  eventType: string;
@@ -300,7 +319,10 @@ export interface UsageResponse {
300
319
  to: string | null;
301
320
  }
302
321
  /** One drive-directory ⇄ GitHub-repo link (`/factories/:slug/repo-links`).
303
- * The webhook secret ref is operator plumbing and never on the wire. */
322
+ * Multi-branch model: ONE link row per (repo, branch); sibling branches
323
+ * of a repo occupy sibling `base@<branch>` placements. The webhook secret
324
+ * ref is operator plumbing and never on the wire — `webhookConfigured` /
325
+ * `webhookVerifiedAt` carry the honest connect status instead. */
304
326
  export interface DriveRepoLink {
305
327
  id: string;
306
328
  factoryId: string;
@@ -310,25 +332,55 @@ export interface DriveRepoLink {
310
332
  provider: string;
311
333
  /** "org/repo". */
312
334
  repoFullName: string;
313
- /** The GitHub branch drive `main` syncs with. */
335
+ /** The GitHub branch this link's placement syncs with. */
314
336
  trackedBranch: string;
315
337
  connectorGrantId: string | null;
316
338
  /** v1 links are always 'write' (the round trip is the feature). */
317
339
  access: "read" | "write";
340
+ /** A webhook secret exists for this link (registration done). NOT proof
341
+ * of delivery — that is `webhookVerifiedAt`. */
342
+ webhookConfigured: boolean;
343
+ /** When a signed GitHub delivery last PROVED the webhook delivers; null
344
+ * = unproven (the link syncs by poll). */
345
+ webhookVerifiedAt: string | null;
346
+ /** Two-way sync: drive edits under the prefix push back to the tracked
347
+ * branch. ON by default for new links. */
348
+ pushOutEnabled: boolean;
349
+ /** Follow-all mode (stamped identically on every sibling link of one
350
+ * repo): pushes to NEW branches auto-link them; GitHub branch deletes
351
+ * auto-unlink. False = explicit selection. */
352
+ followAllBranches: boolean;
353
+ /** Branches follow-all could NOT link (with the reason) — stamped on
354
+ * the repo's PRIMARY link row only; empty everywhere else. */
355
+ autoLinkSkips: Array<{
356
+ branch: string;
357
+ reason: string;
358
+ at: string;
359
+ }>;
318
360
  lastSyncedGitSha: string | null;
319
361
  lastSyncedAcgCommit: string | null;
320
- syncState: "idle" | "syncing" | "diverged" | "reauth_required";
321
- visibility: "team" | "restricted";
362
+ syncState: "idle" | "syncing" | "pushing" | "error" | "conflict" | "diverged" | "reauth_required";
363
+ /** Human-readable detail when syncState is 'error' or 'conflict'. */
364
+ syncError: string | null;
322
365
  createdBy: string | null;
323
366
  lastSyncedAt: string | null;
324
367
  createdAt: string;
325
368
  }
326
369
  /** Input for `POST /factories/:slug/repo-links`. Requires the drive to be
327
- * graph-authoritative (ADR-0058 Phase 4 promoted) — 409 otherwise. */
370
+ * graph-authoritative (ADR-0058 Phase 4 promoted) — 409 otherwise.
371
+ * Exactly ONE of `followAllBranches` / `trackedBranches`:
372
+ * `followAllBranches: true` (the default connect mode) links EVERY
373
+ * branch — default branch primary at the plain `dirPrefix` — and keeps
374
+ * following live (new branches auto-link on push, deleted branches
375
+ * auto-unlink; capped at 100 branches). Explicit `trackedBranches`:
376
+ * index 0 is the PRIMARY and keeps the plain placement; every additional
377
+ * branch lands at the sibling `dirPrefix@<sanitized-branch>` placement. */
328
378
  export interface CreateDriveRepoLinkInput {
329
379
  dirPrefix: string;
330
380
  repoFullName: string;
331
- trackedBranch: string;
381
+ trackedBranches?: string[];
382
+ followAllBranches?: boolean;
332
383
  connectorGrantId: string;
333
- visibility?: "team" | "restricted";
384
+ /** Two-way sync ("push-out") — defaults to TRUE for new links. */
385
+ pushOut?: boolean;
334
386
  }
@@ -283,7 +283,8 @@ export interface ListRunsOptions {
283
283
  factorySlug?: string;
284
284
  /** Substring match on the registered workflow name (`metadata._workflow`). */
285
285
  workflow?: string;
286
- /** Substring match across title / task title / branch / run id. */
286
+ /** Substring match across title / task title / branch — or an exact run
287
+ * id (full UUID). */
287
288
  search?: string;
288
289
  outcome?: string;
289
290
  sort?: "newest" | "oldest" | "fastest" | "slowest";
@@ -146,11 +146,12 @@ export interface InvokePolicy {
146
146
  * today; kept as its own object so finer controls (disk, gpu, …) can be
147
147
  * added later without reshaping `WorkflowMetadata`. */
148
148
  export interface SandboxResources {
149
- /** Machine hardware SKU — one of the `SandboxSize` vCPU strings
150
- * (`2vcpu-4gb` | `4vcpu-8gb` | `8vcpu-16gb` | `32vcpu-64gb`). Maps to
151
- * provider specs at create time (Vercel: 2 / 4 / 8 / 32 vCPU, 2048 MB RAM
152
- * per vCPU). Omit → the smallest SKU. E2B sizing is template-defined and
153
- * ignores this. */
149
+ /** Machine hardware SKU. The vocabulary is `SANDBOX_SIZES` in
150
+ * `sandbox/sizes.ts` — the single source; do not restate it here or
151
+ * anywhere else. Maps to provider specs at create time: Vercel takes the
152
+ * vCPU count and allocates RAM at 2048 MB/vCPU; E2B has no create-time
153
+ * cpu/mem knob at all, so the size selects a PRE-BUILT per-size template
154
+ * (`E2B_TEMPLATE_SIZES`). Omit → `DEFAULT_SANDBOX_SIZE`. */
154
155
  size?: SandboxSize;
155
156
  /** Sandbox provider this workflow's runs execute on — `"vercel"` or
156
157
  * `"e2b"`. Optional and additive: omit and the run resolves to the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-compose/sdk",
3
- "version": "0.8.1",
3
+ "version": "0.8.2",
4
4
  "description": "Client library for agent-compose — define agents, runtimes, and workflows, and invoke them against an agent-compose server.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -173,38 +173,49 @@ there is nothing to type, paste, or screenshot a credential from.
173
173
 
174
174
  ## Recording a demo — the desktop, captured to a video the human can play
175
175
 
176
- "Record a demo of you using X" is a normal ask, and this machine does it:
177
- start a screen recording, drive the app with \`xdotool\` exactly as in Computer
178
- Use, stop the recording, and report the file. (For a LIVE view no recording is
179
- needed — the session header's **Desktop** button already streams this display
180
- to any teammate watching; a recording is the durable, replayable artifact.
181
- Both modes exist; say so when it matters.)
176
+ "Record a demo of you using X" is a normal ask, and this machine does it.
177
+ (For a LIVE view no recording is needed — the session header's **Desktop**
178
+ button already streams this display to any teammate watching; a recording is
179
+ the durable, replayable artifact. Both modes exist; say so when it matters.)
182
180
 
183
- **ffmpeg is NOT pre-installed** — install it first, once per machine:
181
+ **Use \`ac-record\` — the platform recorder is already on PATH** (cloud
182
+ sessions; \`command -v ac-record\` to confirm on older machines):
184
183
 
185
- sudo apt-get update -q && sudo apt-get install -y -q ffmpeg
184
+ ac-record start # begins capturing the desktop (display :0)
185
+ # ... drive the app with xdotool, screenshotting as you go ...
186
+ ac-record stop # finishes + saves to recordings/ in your workspace
187
+ ac-record status # one JSON line: {"recording":true,...}
188
+
189
+ It records the whole display (with desktop audio when the machine has a
190
+ PulseAudio monitor), enforces sane caps (5 min / 200 MB — start a fresh
191
+ recording per scene rather than one long take), keeps the file playable even
192
+ if the machine dies mid-take, and \`stop\` prints the saved path — the file
193
+ lands ON THE DRIVE in \`recordings/\`, visible in Files and playable in the
194
+ dashboard. A human watching the Desktop pane sees the recording indicator
195
+ while you record.
186
196
 
187
- (drop \`sudo\` if you are already root). Then the whole recipe:
197
+ If \`ac-record\` is missing (older machine), record by hand.
198
+ **ffmpeg IS pre-installed** on platform images (\`command -v ffmpeg\`; only
199
+ if absent: \`sudo apt-get update -q && sudo apt-get install -y -q ffmpeg\`):
188
200
 
189
201
  DISPLAY=:0 ffmpeg -f x11grab \\
190
202
  -video_size "$(DISPLAY=:0 xdotool getdisplaygeometry | tr ' ' x)" \\
191
203
  -framerate 10 -i :0 -c:v libvpx -b:v 1M -deadline realtime -cpu-used 8 \\
192
204
  demo.webm &
193
205
  FFMPEG_PID=$!
194
- # ... drive the app with xdotool, screenshotting as you go ...
206
+ # ... drive the app with xdotool ...
195
207
  kill -INT "$FFMPEG_PID" && wait "$FFMPEG_PID"
196
208
 
197
- The gotchas, each one earned:
209
+ The hand-rolled gotchas, each one earned:
198
210
  - **Stop with SIGINT (\`kill -INT\`), never SIGKILL** — ffmpeg finalizes the
199
211
  file on SIGINT; a hard kill truncates the encode mid-write.
200
- - **Record WebM (matroska-family), not MP4** — mp4 writes its moov atom at the
201
- END, so a killed or crashed encode leaves an UNPLAYABLE file; webm stays
202
- playable up to the last written frame and plays natively in the browser.
203
- MP4's only edge is compatibility with some external players — transcode
204
- afterwards if you truly need it, never record straight to it.
212
+ - **Record WebM (matroska-family), not plain MP4** — mp4 writes its moov atom
213
+ at the END, so a killed or crashed encode leaves an UNPLAYABLE file; webm
214
+ stays playable up to the last written frame and plays natively in the
215
+ browser. (\`ac-record\` sidesteps this with fragmented mp4.)
205
216
  - **\`-video_size\` must match the real screen** — x11grab does not default to
206
217
  it; read the geometry from \`xdotool getdisplaygeometry\` as above.
207
- - **10–12 fps is right for a screen demo** — small files, legible UI motion;
218
+ - **10–15 fps is right for a screen demo** — small files, legible UI motion;
208
219
  this is not video production.
209
220
  - **Write to the drive, not /tmp** — the recording must land in your working
210
221
  directory to persist and show up in Files; a file in /tmp dies with the
package/src/client.ts CHANGED
@@ -31,8 +31,8 @@ import type {
31
31
  TeamMember, Mention, CreateMentionsInput,
32
32
  ConversationsPage, SessionsPage, ConversationDetail, ConversationThread,
33
33
  CreateCloudSessionInput, CloudSessionCreated,
34
- SessionPreview, PreviewOpened, OpenPreviewInput, SessionForked,
35
- SessionChangeSet, SessionMergeReport, SessionDiscardReport,
34
+ SessionPreview, PreviewOpened, OpenPreviewInput, SessionForked, BackgroundWorkHeld,
35
+ SessionChangeSet, SessionMergeGated, SessionMergeReport, SessionDiscardReport,
36
36
  SendConversationMessageInput, SendConversationMessageResult,
37
37
  ChannelSessionRow, ChannelSessionsResponse, SessionChannelMessagePosted,
38
38
  ConversationPresenceSnapshot, StreamConversationOptions, AgentListRow,
@@ -52,7 +52,7 @@ import type {
52
52
  FactoryRow, CreateFactoryInput, UpdateFactoryInput,
53
53
  ScheduleRow, CreateScheduleInput,
54
54
  SecretOptions, SetSecretResult, SecretListEntry,
55
- CreateApiKeyInput, ApiKey, ApiKeyCreated, UsageResponse,
55
+ CreateApiKeyInput, ApiKey, ApiKeyCreated, ListApiKeysOptions, ApiKeyPage, UsageResponse,
56
56
  DriveRepoLink, CreateDriveRepoLinkInput,
57
57
  DriveMountSession, CreateDriveMountSessionInput,
58
58
  } from "./types/api-factory.js";
@@ -82,11 +82,12 @@ export type {
82
82
  ConversationMessagePart, ConversationRow, ConversationMessageRow,
83
83
  ConversationsPage, SessionsPage, ConversationDetail,
84
84
  CreateCloudSessionInput, CloudSessionCreated, ConversationThread,
85
- SessionPreview, PreviewOpened, OpenPreviewInput, SessionForked,
86
- SessionFileChange, SessionChangeSet,
87
- SessionMergeReportDetail, SessionMergeReport, SessionDiscardReport,
85
+ SessionPreview, PreviewOpened, OpenPreviewInput, SessionForked, BackgroundWorkHeld,
86
+ SessionFileChange, SessionChangeSet, SessionChangeStats, SessionDiffReviewBound, ReviewSuggestion,
87
+ ReviewSuggestionDecision, ReviewSuggestionDecisions,
88
+ SessionMergeReportDetail, SessionMergeReport, SessionMergeGated, SessionDiscardReport,
88
89
  ConversationPageContext, SendConversationMessageInput, ConversationTurnState,
89
- SendConversationMessageResult, ConversationPresenceSnapshot, AgentListRow, StreamConversationOptions,
90
+ SendConversationMessageResult, UnnotifiedMention, ConversationPresenceSnapshot, AgentListRow, StreamConversationOptions,
90
91
  ChannelSessionStatus, ChannelSessionRow, ChannelSessionsResponse, SessionChannelMessagePosted,
91
92
  } from "./types/api-conversations.js";
92
93
  export type {
@@ -108,7 +109,7 @@ export type {
108
109
  FactoryRow, CreateFactoryInput, UpdateFactoryInput,
109
110
  ScheduleRow, CreateScheduleInput,
110
111
  SecretOptions, SetSecretResult, SecretListEntry,
111
- CreateApiKeyInput, ApiKey, ApiKeyCreated,
112
+ CreateApiKeyInput, ApiKey, ApiKeyCreated, ListApiKeysOptions, ApiKeyPage,
112
113
  UsageRollupRow, UsageResponse,
113
114
  DriveRepoLink, CreateDriveRepoLinkInput,
114
115
  DriveMountSession, CreateDriveMountSessionInput,
@@ -984,6 +985,28 @@ export class AgentComposeClient {
984
985
  return body.closed;
985
986
  }
986
987
 
988
+ /** Hold (or extend — the stamp is monotone) a cloud session's
989
+ * background-work busy lease: while it is live, the between-turns
990
+ * park/suspend leaves the session's VM running so work the turn left
991
+ * behind keeps executing. The lease lapses on its own — re-hold to
992
+ * extend past `minutes` (server-capped). Write-tier. */
993
+ holdBackgroundWork(conversationId: string, input?: { minutes?: number }): Promise<BackgroundWorkHeld> {
994
+ return this.fetch<BackgroundWorkHeld>(
995
+ `/api/v1/conversations/${encodeURIComponent(conversationId)}/background-work`,
996
+ { method: "POST", body: { ...(input?.minutes !== undefined ? { minutes: input.minutes } : {}) } },
997
+ );
998
+ }
999
+
1000
+ /** Release the session's background-work lease (the work finished).
1001
+ * Idempotent — `released` is false when no lease was held. */
1002
+ async releaseBackgroundWork(conversationId: string): Promise<boolean> {
1003
+ const body = await this.fetch<{ released: boolean }>(
1004
+ `/api/v1/conversations/${encodeURIComponent(conversationId)}/background-work`,
1005
+ { method: "DELETE" },
1006
+ );
1007
+ return body.released;
1008
+ }
1009
+
987
1010
  /** Fork a cloud session from HEAD: snapshot the VM + branch the drive + seed
988
1011
  * a new conversation from the transcript so far, booting from both. Returns
989
1012
  * the child conversation id to switch to. Write-tier; "Branch from here." */
@@ -1017,6 +1040,11 @@ export class AgentComposeClient {
1017
1040
  * `main`. Write-tier + human caller. Pass `opts.branch` (the branch you
1018
1041
  * reviewed) to fail 409 `branch_changed` if it moved since.
1019
1042
  *
1043
+ * On a merge-GATED session (merge-gate spec) a non-approver's call does
1044
+ * NOT merge: it answers 202 `SessionMergeGated` — the ask froze into (or
1045
+ * converged on) a kind='merge' approval routed to the session's
1046
+ * approvers. Discriminate on `object`.
1047
+ *
1020
1048
  * Failures: 403 `role_read_only` (read-only member) or a plain 403 for
1021
1049
  * session toolbelt keys (agents cannot self-approve); 409 `turn_active`
1022
1050
  * (a turn is running — retry when idle) | `branch_changed`; 400
@@ -1026,8 +1054,8 @@ export class AgentComposeClient {
1026
1054
  mergeSessionChanges(
1027
1055
  conversationId: string,
1028
1056
  opts: { branch?: string } = {},
1029
- ): Promise<SessionMergeReport> {
1030
- return this.fetch<SessionMergeReport>(
1057
+ ): Promise<SessionMergeReport | SessionMergeGated> {
1058
+ return this.fetch<SessionMergeReport | SessionMergeGated>(
1031
1059
  `/api/v1/conversations/${encodeURIComponent(conversationId)}/changes/merge`,
1032
1060
  { method: "POST", body: opts.branch ? { branch: opts.branch } : {} },
1033
1061
  );
@@ -1608,17 +1636,18 @@ export class AgentComposeClient {
1608
1636
  return body.links;
1609
1637
  }
1610
1638
 
1611
- /** Link a drive directory to a GitHub repo + tracked branch (`manage`
1612
- * scope). Requires the drive to be graph-authoritative — 409 names the
1613
- * promotion prerequisite otherwise. */
1639
+ /** Link a drive directory to a GitHub repo's tracked BRANCHES (`manage`
1640
+ * scope) — one link row per branch, created in one action. Requires the
1641
+ * drive to be graph-authoritative — 409 names the promotion
1642
+ * prerequisite otherwise. */
1614
1643
  async createRepoLink(
1615
1644
  input: CreateDriveRepoLinkInput, opts?: { factorySlug?: string },
1616
- ): Promise<DriveRepoLink> {
1645
+ ): Promise<DriveRepoLink[]> {
1617
1646
  const slug = opts?.factorySlug ?? DEFAULT_FACTORY;
1618
- const body = await this.fetch<{ link: DriveRepoLink }>(
1647
+ const body = await this.fetch<{ links: DriveRepoLink[] }>(
1619
1648
  `/api/v1/factories/${encodeURIComponent(slug)}/repo-links`,
1620
1649
  { method: "POST", body: input });
1621
- return body.link;
1650
+ return body.links;
1622
1651
  }
1623
1652
 
1624
1653
  /** Unlink (§6.3: the prefix's files and history stay on the drive). */
@@ -1668,10 +1697,17 @@ export class AgentComposeClient {
1668
1697
  }
1669
1698
 
1670
1699
  /** List API keys on the caller's team (metadata only — plaintext keys are
1671
- * never returned). */
1672
- async listApiKeys(): Promise<ApiKey[]> {
1673
- const body = await this.fetch<{ object: "list"; data: ApiKey[]; has_more: boolean }>("/api-keys");
1674
- return body.data;
1700
+ * never returned). Bounded + keyset-paginated: pass `cursor` from the
1701
+ * previous page's `nextCursor` to walk forward. */
1702
+ async listApiKeys(opts?: ListApiKeysOptions): Promise<ApiKeyPage> {
1703
+ const qs = new URLSearchParams();
1704
+ if (opts?.limit !== undefined) qs.set("limit", String(opts.limit));
1705
+ if (opts?.status !== undefined) qs.set("status", opts.status);
1706
+ if (opts?.cursor !== undefined) qs.set("cursor", opts.cursor);
1707
+ const suffix = qs.toString() ? `?${qs.toString()}` : "";
1708
+ const body = await this.fetch<{ object: "list"; data: ApiKey[]; has_more: boolean; next_cursor: string | null }>(
1709
+ `/api-keys${suffix}`);
1710
+ return { data: body.data, hasMore: body.has_more, nextCursor: body.next_cursor ?? null };
1675
1711
  }
1676
1712
 
1677
1713
  // ── Usage ──────────────────────────────────────────────────────────────────
package/src/index.ts CHANGED
@@ -125,7 +125,7 @@ export type {
125
125
  RunListEntry, ListRunsOptions, RunDetail,
126
126
  FundingLane, RunFundingStamp, RunFundingUsageRow, RunFundingResponse,
127
127
  RegisteredRuntime, RunState, RunStatus, FactoryRow, SnapshotListEntry, SnapshotListResponse,
128
- ApiKey, ApiKeyCreated,
128
+ ApiKey, ApiKeyCreated, ListApiKeysOptions, ApiKeyPage,
129
129
  UsageRollupRow, UsageResponse,
130
130
  CancelRunResponse,
131
131
  RequestAgentPauseOptions, RequestAgentPauseResponse,
@@ -135,13 +135,15 @@ export type {
135
135
  ConversationMessagePart, ConversationRow, ConversationMessageRow,
136
136
  ConversationsPage, ConversationDetail, ConversationThread,
137
137
  ConversationPageContext, SendConversationMessageInput, SendConversationMessageResult,
138
+ UnnotifiedMention,
138
139
  ConversationTurnState, ConversationPresenceSnapshot, StreamConversationOptions, AgentListRow,
139
140
  CreateCloudSessionInput, CloudSessionCreated,
140
141
  // Cloud-session developer surface (ADR-0052)
141
- SessionPreview, PreviewOpened, OpenPreviewInput, SessionForked,
142
+ SessionPreview, PreviewOpened, OpenPreviewInput, SessionForked, BackgroundWorkHeld,
142
143
  // Session branch proposals (ADR-0053)
143
- SessionFileChange, SessionChangeSet,
144
- SessionMergeReportDetail, SessionMergeReport, SessionDiscardReport,
144
+ SessionFileChange, SessionChangeSet, SessionChangeStats, SessionDiffReviewBound, ReviewSuggestion,
145
+ ReviewSuggestionDecision, ReviewSuggestionDecisions,
146
+ SessionMergeReportDetail, SessionMergeReport, SessionMergeGated, SessionDiscardReport,
145
147
  // User drive mounts (`agentc files mount` on a human's own machine)
146
148
  DriveMountSession, CreateDriveMountSessionInput,
147
149
  // Channel-attached sessions (ADR-0057)
@@ -278,8 +280,11 @@ export { createSandbox, reconnectSandbox, killAllSandboxes, killSandboxById,
278
280
  getSandboxQuotas, listOwnedSandboxes, deleteSandboxSnapshot, snapshotResolves,
279
281
  makeSandboxProvider, makeDesktopSandboxProvider,
280
282
  parseSseExecStream, AGENT_COMPOSE_TAG,
281
- SANDBOX_VCPUS, DEFAULT_SANDBOX_SIZE, E2B_TEMPLATE_SIZES,
282
- isE2bSupportedSize, e2bMachineSpec, e2bBaseTemplate, e2bAgentEnvTemplate,
283
+ SANDBOX_SIZES, SANDBOX_MACHINES, SANDBOX_VCPUS, DEFAULT_SANDBOX_SIZE, SESSION_DEFAULT_SANDBOX_SIZE,
284
+ E2B_TEMPLATE_SIZES, E2B_MAX_VCPUS, E2B_MAX_MEMORY_MB,
285
+ VERCEL_MEMORY_MB_PER_VCPU,
286
+ isE2bSupportedSize, isVercelSupportedSize, sandboxSizeLabel,
287
+ e2bMachineSpec, e2bBaseTemplate, e2bAgentEnvTemplate,
283
288
  isPlatformE2bTemplateAlias,
284
289
  // ADR-0038 "Computer" — the per-member persistent desktop machine template.
285
290
  E2B_DEVBOX_TEMPLATE, E2B_DEVBOX_SPEC, E2B_DEVBOX_RECIPE_VERSION,