@agent-compose/sdk 0.8.2 → 0.8.3

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 (40) hide show
  1. package/dist/agent/__tests__/perf-sampler.test.d.ts +10 -0
  2. package/dist/agent/agent-context.d.ts +1 -1
  3. package/dist/agent/agent-loop.d.ts +5 -1
  4. package/dist/agent/desktop-open.d.ts +184 -0
  5. package/dist/agent/perf-sampler.d.ts +99 -0
  6. package/dist/agent/services-manifest.d.ts +88 -0
  7. package/dist/agent/services-restore.d.ts +58 -0
  8. package/dist/client.d.ts +164 -8
  9. package/dist/display.d.ts +17 -0
  10. package/dist/index.d.ts +13 -4
  11. package/dist/index.js +1374 -51
  12. package/dist/runtimes/_cli-agent.d.ts +347 -2
  13. package/dist/runtimes/claude-code.d.ts +12 -0
  14. package/dist/runtimes/codex.d.ts +8 -0
  15. package/dist/runtimes/openai-desktop.js +1312 -51
  16. package/dist/runtimes/session-env.test.d.ts +14 -0
  17. package/dist/types/api-conversations.d.ts +309 -1
  18. package/dist/types/api-factory.d.ts +115 -10
  19. package/dist/types/api-runs.d.ts +21 -0
  20. package/dist/types/protocol.d.ts +32 -1
  21. package/dist/types/runtime.d.ts +120 -0
  22. package/package.json +1 -1
  23. package/src/agent/agent-context.ts +100 -11
  24. package/src/agent/agent-loop.ts +10 -3
  25. package/src/agent/desktop-open.ts +418 -0
  26. package/src/agent/perf-sampler.ts +202 -0
  27. package/src/agent/services-manifest.ts +356 -0
  28. package/src/agent/services-restore.ts +195 -0
  29. package/src/client.ts +328 -12
  30. package/src/display.ts +44 -1
  31. package/src/index.ts +63 -1
  32. package/src/runtimes/_cli-agent.ts +891 -35
  33. package/src/runtimes/claude-code.ts +187 -12
  34. package/src/runtimes/codex.ts +58 -1
  35. package/src/sandbox/providers/local.ts +16 -4
  36. package/src/types/api-conversations.ts +307 -3
  37. package/src/types/api-factory.ts +118 -10
  38. package/src/types/api-runs.ts +23 -0
  39. package/src/types/protocol.ts +30 -1
  40. package/src/types/runtime.ts +122 -0
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Session env sourcing (session secrets) — the turn-launch half of the
3
+ * contract. The server writes `$HOME/.agent-compose/session-env.sh` at
4
+ * sandbox acquire; the detached JSONL launch sources it fresh EVERY turn
5
+ * via `sessionEnvSourceFragment`. Pinned here:
6
+ *
7
+ * - the fragment sources exactly the configured $HOME-relative path,
8
+ * silently, and never fails the turn when the file is absent;
9
+ * - hostile/malformed paths (quotes, spaces, `..`, absolute) are DROPPED
10
+ * — a bad option must not break the launch or escape $HOME;
11
+ * - no option ⇒ empty fragment (local/BYOM runs never read a user's
12
+ * dotfiles by surprise).
13
+ */
14
+ export {};
@@ -92,10 +92,35 @@ export interface ConversationRow {
92
92
  * callers; key callers see 0. */
93
93
  unreadCount?: number;
94
94
  }
95
+ /** Agent-author identity decoration (mirrors the server's `agentAuthor`
96
+ * on message rows and stream payloads): rows authored by a user's agent
97
+ * that is not the conversation's resident agent — the assistant's
98
+ * delegated sends, review briefs, and PEER-SESSION messages
99
+ * (`sourceChannel: 'session'`). The persona `name` is the author label;
100
+ * `avatarSeed` is the agent's own generated orb; the owner names WHOSE
101
+ * agent it is. */
102
+ export interface ConversationAgentAuthor {
103
+ agentId: string;
104
+ name: string;
105
+ avatarSeed: string;
106
+ ownerUserId: string | null;
107
+ ownerName: string | null;
108
+ }
95
109
  export interface ConversationMessageRow {
96
110
  id: string;
97
111
  authorKind: string;
98
112
  authorId: string | null;
113
+ /** The author's display name, resolved server-side — a HUMAN author's
114
+ * display name, or an attributable AGENT author's persona name (see
115
+ * `agentAuthor`). Null for resident-agent/system rows; absent on older
116
+ * servers. */
117
+ authorLabel?: string | null;
118
+ /** Sender identity for foreign-agent and peer-session rows — render the
119
+ * persona name + its avatarSeed FIRST, before any session/runtime
120
+ * fallback (the one resolution order; streamed and settled renders are
121
+ * identical). Null/absent on resident-agent, session channel-post, and
122
+ * older-server rows. */
123
+ agentAuthor?: ConversationAgentAuthor | null;
99
124
  parts: ConversationMessagePart[];
100
125
  threadRootId: string | null;
101
126
  reactions: Record<string, string[]>;
@@ -115,6 +140,12 @@ export interface ConversationMessageRow {
115
140
  * rows to the session (title + runtime mark), never a resident agent.
116
141
  * Null once the session is deleted; absent on older servers. */
117
142
  sourceSessionId?: string | null;
143
+ /** THE ORIGIN SURFACE (ADR-0073 §6.5): the ingress the row arrived over —
144
+ * 'sms' / 'phone' / 'voice' on inbound user rows, 'assistant' on
145
+ * delegated sends, 'session' on peer session→session messages (whose
146
+ * author identity rides `agentAuthor`). Null/absent = an ordinary
147
+ * in-app send or an older server. */
148
+ sourceChannel?: "sms" | "phone" | "voice" | "assistant" | "session" | null;
118
149
  /** Set on a session-transcript row RELAYED from a channel mention
119
150
  * (ADR-0057 Seam 3) — the channel it arrived from. Null once the
120
151
  * channel is deleted; absent on older servers. */
@@ -131,13 +162,49 @@ export interface ConversationsPage {
131
162
  /** Sessions page — each row additionally carries its resident
132
163
  * executor substrate (ADR-0037 §8): `"cloud"` = persistent server-side
133
164
  * sandbox (pickers render the ☁ marker), `"local"` = bridge daemon,
134
- * null = no durable session record yet. */
165
+ * null = no durable session record yet — and, on diff-review spawns,
166
+ * `reviewOf`: the session this row was born to REVIEW (id + its current
167
+ * alias/title; a deleted reviewed conversation degrades to a bare id
168
+ * ref). Null/absent for ordinary sessions and older servers. */
135
169
  export interface SessionsPage {
136
170
  conversations: Array<ConversationRow & {
137
171
  sessionExecutor: "local" | "cloud" | null;
172
+ reviewOf?: {
173
+ conversationId: string;
174
+ alias: string | null;
175
+ title: string | null;
176
+ head?: string | null;
177
+ } | null;
138
178
  }>;
139
179
  next_cursor: string | null;
140
180
  }
181
+ /** One 15-minute sandbox perf rollup bucket from
182
+ * GET /conversations/:id/perf-history (session_perf_rollups). Null
183
+ * utilization fields = the guest never reported that resource in the
184
+ * bucket — never zero-filled. */
185
+ export interface SessionPerfBucket {
186
+ /** Start of the bucket, ISO 8601. */
187
+ bucket_start: string;
188
+ /** Guest samples folded into the bucket (turn + sweep lanes). */
189
+ samples: number;
190
+ cpu_busy_p50: number | null;
191
+ cpu_busy_p95: number | null;
192
+ mem_used_p50: number | null;
193
+ mem_used_p95: number | null;
194
+ disk_used_p95: number | null;
195
+ load1_max: number | null;
196
+ /** Seconds within the bucket any resource sat ≥90% (capped at 900). */
197
+ saturated_seconds: number;
198
+ vcpu: number | null;
199
+ provider: string;
200
+ }
201
+ /** Newest-first, keyset-paginated (house list envelope). */
202
+ export interface SessionPerfHistoryPage {
203
+ object: "list";
204
+ data: SessionPerfBucket[];
205
+ has_more: boolean;
206
+ next_cursor: string | null;
207
+ }
141
208
  export interface ConversationDetail {
142
209
  conversation: ConversationRow;
143
210
  /** Resident executor substrate (ADR-0037 §8): `"local"` = a developer's
@@ -170,6 +237,10 @@ export interface ConversationDetail {
170
237
  factorySlug: string;
171
238
  branch: string;
172
239
  } | null;
240
+ /** THE REVIEWER MARKER (diff-review spawns): non-null ⇒ this session was
241
+ * born to review that conversation's diff. Null for ordinary sessions;
242
+ * absent on older servers (read loosely). */
243
+ sessionReviewOf?: string | null;
173
244
  viewerLastReadAt: string | null;
174
245
  /** The caller's role in this conversation (ADR-0045) — drives client
175
246
  * affordances only; the server remains the authority on every action.
@@ -190,6 +261,11 @@ export interface ConversationDetail {
190
261
  * `messages`. Seed the stream's first `Last-Event-ID` from it instead of
191
262
  * 0 (0 replays the entire history). Absent on older servers. */
192
263
  streamCursor?: number;
264
+ /** Windowed hydrate (2026-08-17): `messages` is the NEWEST window
265
+ * (default 50; `limit` caps at 200). Non-null = older transcript
266
+ * exists — page it with `GET /conversations/:id/messages?before=` +
267
+ * this opaque cursor. Absent on older servers. */
268
+ olderCursor?: string | null;
193
269
  messages: ConversationMessageRow[];
194
270
  }
195
271
  /** Input for `createCloudSession` (ADR-0037 Phase 3b / ADR-0055 §9). The
@@ -387,6 +463,13 @@ export interface SessionChangeSet {
387
463
  truncated: boolean;
388
464
  /** Open `factory_file_conflicts` rows from this session's prior merges. */
389
465
  openConflicts: number;
466
+ /** Graph plane only (additive — absent elsewhere): the opaque cursor for
467
+ * the NEXT page of `changes` — pass it back as `after` on
468
+ * `getSessionChanges`. Null = last page. */
469
+ nextAfter?: string | null;
470
+ /** The serving plane (additive): `"graph"` when the change set is served
471
+ * from the commit graph (paginated, head-anchored); absent = legacy. */
472
+ plane?: "graph";
390
473
  /** MERGE GATE (merge-gate spec, additive — absent on older servers):
391
474
  * null = ungated. Present: whether THIS caller's merge lands directly
392
475
  * (`canMerge` — an owner or named approver) or stages a kind='merge'
@@ -416,6 +499,22 @@ export interface SessionChangeSet {
416
499
  reviewEnabled?: boolean;
417
500
  reviewSessionConversationId?: string | null;
418
501
  reviewSessionActive?: boolean;
502
+ /** BOUND (additive): the bound review session — the FOLLOW-UP LOOP's
503
+ * driver — exists and is not ended (a parked reviewer between turns is
504
+ * bound but not active). Not publish rights: every reviewer of the
505
+ * diff owns its own document; concurrent reviewers are first-class,
506
+ * bounded at spawn (`reviewer_cap`). */
507
+ reviewSessionBound?: boolean;
508
+ /** The AUTOMATIC follow-up loop (additive): armed (default true) + how
509
+ * many automatic rounds ran since the last human-triggered review
510
+ * (hard cap 2; a manual re-review resets it). */
511
+ reviewAutoFollowup?: boolean;
512
+ reviewAutoRounds?: number;
513
+ /** The OPT-IN main-advance auto-rebase reflex (additive — absent on
514
+ * older servers; default OFF): armed ⇒ when main moves and a preflight
515
+ * proves the fold clean, the platform rebases this session's branch
516
+ * from main headlessly. Toggled via `setSessionAutoRebase`. */
517
+ autoRebaseFromMain?: boolean;
419
518
  /** STRUCTURED review suggestions beside the notes (additive — absent on
420
519
  * older servers): individually actionable {id, path, title, rationale,
421
520
  * patch} entries the review session wrote back, status-stamped
@@ -428,6 +527,33 @@ export interface SessionChangeSet {
428
527
  * (`mergeable` false, merge 409s `fork_branch_unmergeable`), and it is
429
528
  * refused as a review target (409 `review_of_review`). */
430
529
  reviewOfConversationId?: string | null;
530
+ /** PER-REVIEWER review documents (additive — absent on older servers;
531
+ * owner ask 2026-08-22): EVERY reviewer's published document for this
532
+ * diff, newest publish first — parallel reviewers each own one. The
533
+ * flat `reviewNotes`/`reviewSuggestions` above are derived aggregates
534
+ * (the primary document's notes; the reviewer-stamped union); this is
535
+ * the grouped truth the review panel renders. Page 1 only — cursor
536
+ * pages serve an empty array. */
537
+ reviewDocuments?: SessionReviewDocument[];
538
+ }
539
+ /** One reviewer's published review document
540
+ * (`SessionChangeSet.reviewDocuments`). `stale` compares the head this
541
+ * document was published against with the served diff. */
542
+ export interface SessionReviewDocument {
543
+ reviewerConversationId: string;
544
+ /** The reviewer session's @alias / title, when its conversation still
545
+ * exists (a deleted reviewer degrades to the bare id). */
546
+ reviewerAlias: string | null;
547
+ reviewerTitle: string | null;
548
+ notes: string;
549
+ suggestions: ReviewSuggestion[];
550
+ /** ISO timestamp of this document's last publish. */
551
+ publishedAt: string;
552
+ /** The diff head published against (null = pre-stamp reviewer). */
553
+ signature: string | null;
554
+ /** Display form of `signature` ("head cd741f27"). */
555
+ headShort: string | null;
556
+ stale: boolean;
431
557
  }
432
558
  /** One STRUCTURED, individually actionable suggestion a review session
433
559
  * wrote back beside its notes (`SessionChangeSet.reviewSuggestions`). */
@@ -447,6 +573,45 @@ export interface ReviewSuggestion {
447
573
  * the session branch), `"rejected"`, or `"stale"` (an accept found the
448
574
  * file drifted since review; nothing was written). */
449
575
  status: "proposed" | "accepted" | "rejected" | "stale";
576
+ /** REVIEWER IDENTITY (additive — present on the flat
577
+ * `SessionChangeSet.reviewSuggestions` union; owner ask 2026-08-22):
578
+ * which reviewer's document this entry came from. Ids are unique only
579
+ * PER REVIEWER — the decide-one route takes `reviewerConversationId`
580
+ * when two reviewers reused an id (409 `ambiguous_suggestion`
581
+ * otherwise). */
582
+ reviewerConversationId?: string;
583
+ reviewerAlias?: string | null;
584
+ }
585
+ /** One suggestion in the review write-back (`publishReviewNotes` /
586
+ * `agentc review publish`): `ReviewSuggestion` minus `status` — the server
587
+ * stamps every stored entry `"proposed"`, so a reviewer can never claim
588
+ * acceptance. `path` must name a file in the change set (refused
589
+ * otherwise). */
590
+ export interface ReviewSuggestionInput {
591
+ id: string;
592
+ path: string;
593
+ title: string;
594
+ rationale: string;
595
+ patch: string;
596
+ }
597
+ /** `POST /conversations/:id/review-notes` — the review session publishing
598
+ * its whole review document (full replace: notes AND suggestions
599
+ * together). Gated to the bound review session's own toolbelt key. */
600
+ export interface ReviewNotesPublished {
601
+ ok: true;
602
+ }
603
+ /** `POST /conversations/:id/review-git-credential` — one fresh short-lived
604
+ * credential for the review git remote, minted per git operation by the
605
+ * sandbox's credential helper (`agentc review git-credential`). Gated to
606
+ * the review session's own toolbelt key; never stored, never reused. */
607
+ export interface ReviewGitCredential {
608
+ /** The stable, credential-free remote URL (`https://host/git/<disk>`). */
609
+ url: string;
610
+ username: string;
611
+ password: string;
612
+ /** ISO timestamp the credential expires — informational (the helper
613
+ * mints anew per operation). */
614
+ expiresAt: string;
450
615
  }
451
616
  /** `POST /conversations/:id/changes/suggestions/:suggestionId/decide` —
452
617
  * the post-call truth for that suggestion (an accept that found drift
@@ -456,6 +621,9 @@ export interface ReviewSuggestionDecision {
456
621
  conversationId: string;
457
622
  id: string;
458
623
  status: ReviewSuggestion["status"];
624
+ /** PER-REVIEWER documents (additive): whose document the decided
625
+ * suggestion lives in — null on older servers / legacy ids. */
626
+ reviewerConversationId?: string | null;
459
627
  }
460
628
  /** `POST /conversations/:id/changes/suggestions/decide-all` — every stored
461
629
  * suggestion's post-call status (only `"proposed"` rows flip). */
@@ -465,6 +633,7 @@ export interface ReviewSuggestionDecisions {
465
633
  results: Array<{
466
634
  id: string;
467
635
  status: ReviewSuggestion["status"];
636
+ reviewerConversationId?: string | null;
468
637
  }>;
469
638
  }
470
639
  /** `GET /conversations/:id/changes/stats` — the changes chip's aggregate
@@ -556,6 +725,89 @@ export interface SessionDiscardReport {
556
725
  newBranch: string;
557
726
  discarded: true;
558
727
  }
728
+ /** One row of the conflict preflight (`GET /conversations/:id/changes/preflight`)
729
+ * — the branch-side change decorated with the REAL merge's own per-file
730
+ * disposition, computed report-only: `fastForward` lands as-is (main
731
+ * untouched at the path), `skip` is already-equal on both sides,
732
+ * `conflict` would 409 the merge today. `autoMerge` is the legacy plane's
733
+ * clean-diff3 class — never emitted on the graph plane. */
734
+ export interface SessionPreflightFileRow {
735
+ path: string;
736
+ kind: "add" | "modify" | "delete";
737
+ disposition: "fastForward" | "autoMerge" | "conflict" | "skip";
738
+ /** Set on `conflict` rows only. */
739
+ conflictReason: "same_region" | "binary" | "delete_vs_edit" | null;
740
+ }
741
+ /** `GET /conversations/:id/changes/preflight` — see conflicts AHEAD of the
742
+ * merge: report-only, no writes, no locks. `unavailable` = no cheap head
743
+ * anchors (legacy plane, unbranched) or the classification faulted — the
744
+ * caller degrades honestly, never fake zeros. `pending` = a cold compute
745
+ * outlived the first-response deadline; poll again to collect it. */
746
+ export type SessionChangePreflight = {
747
+ object: "session_change_preflight";
748
+ conversationId: string;
749
+ state: "unavailable";
750
+ } | {
751
+ object: "session_change_preflight";
752
+ conversationId: string;
753
+ state: "pending";
754
+ base: string;
755
+ oursHead: string;
756
+ theirsHead: string;
757
+ } | {
758
+ object: "session_change_preflight";
759
+ conversationId: string;
760
+ state: "ok";
761
+ branch: string;
762
+ base: string;
763
+ /** main's head — the exact commit the dispositions were computed against. */
764
+ oursHead: string;
765
+ /** the session branch's head. */
766
+ theirsHead: string;
767
+ files: SessionPreflightFileRow[];
768
+ conflictCount: number;
769
+ /** Page caps hit — dispositions cover a floor, honestly. */
770
+ truncated: boolean;
771
+ };
772
+ /** Result of `POST /conversations/:id/changes/rebase` — current main
773
+ * folded INTO the session branch (the reverse merge; main untouched).
774
+ * `rebased` advanced the branch by one merge commit (`rebaseCommit`)
775
+ * and future diffs/merges use the new base. `rebased_with_conflicts`
776
+ * folded the clean remainder (`foldCommit`, null when nothing folded) and
777
+ * KEPT the branch's version for `conflictPaths` — each is recorded as a
778
+ * conflict row and the session was told in-conversation to reconcile. */
779
+ export interface SessionRebaseReport {
780
+ object: "session_rebase_report";
781
+ conversationId: string;
782
+ branch: string;
783
+ outcome: "noop" | "rebased" | "rebased_with_conflicts";
784
+ /** main's head at rebase — the branch's new effective base on a clean
785
+ * rebase. */
786
+ mainHead: string;
787
+ rebaseCommit?: string;
788
+ foldCommit?: string | null;
789
+ folded?: {
790
+ added: number;
791
+ modified: number;
792
+ deleted: number;
793
+ skipped: number;
794
+ };
795
+ baselineAdvanced?: boolean;
796
+ /** Capped at 100 on the wire; `conflictCount` is the true total. */
797
+ conflictPaths?: string[];
798
+ conflictCount?: number;
799
+ conflictsTruncated?: boolean;
800
+ /** True when the gateway truncated its conflict list — the fold was
801
+ * skipped whole (an unlisted conflicted path must never be overwritten). */
802
+ foldSkipped?: boolean;
803
+ }
804
+ /** Result of `POST /conversations/:id/changes/autorebase` — the opt-in
805
+ * main-advance auto-rebase reflex's new state. */
806
+ export interface SessionAutoRebaseState {
807
+ object: "session_autorebase";
808
+ conversationId: string;
809
+ autoRebaseFromMain: boolean;
810
+ }
559
811
  /** The sender's page stamp (HUD bar sends) — persisted server-side, never
560
812
  * echoed back on the wire. Mirrors the server's `PageContext` schema. */
561
813
  export interface ConversationPageContext {
@@ -661,6 +913,62 @@ export interface SessionChannelMessagePosted {
661
913
  * null for a room post. Absent on older servers. */
662
914
  threadRootId?: string | null;
663
915
  }
916
+ /** Result of `POST /session-messages` — one session's agent messaging
917
+ * ANOTHER session's conversation (`agentc session message @alias`). Unlike
918
+ * a channel post, this DOES wake the target's turn machinery: `turn` is
919
+ * the server's verdict ('started' | 'queued' | 'parked' — the agent-to-
920
+ * agent exchange damper landed the message without waking anyone). */
921
+ export interface SessionDirectMessageSent {
922
+ object: "session_message";
923
+ messageId: string;
924
+ target: {
925
+ conversationId: string;
926
+ alias: string | null;
927
+ title: string | null;
928
+ };
929
+ turn: "started" | "queued" | "parked";
930
+ /** Honest posture line — delivery is not completion. */
931
+ note: string;
932
+ }
933
+ /** One branch claim, as the claim/release routes serialize it. */
934
+ export interface BranchClaimInfo {
935
+ id: string;
936
+ branch: string;
937
+ targetConversationId: string;
938
+ holderConversationId: string;
939
+ targetLabel: string;
940
+ holderLabel: string;
941
+ reason: string | null;
942
+ claimedAt: string;
943
+ }
944
+ /** Result of `POST /session-branch-claims` — write authority over the
945
+ * target session's drive branch transferred to the calling session, plus
946
+ * the mount material the CLI uses to FUSE-mount the branch in-guest. */
947
+ export interface BranchClaimGranted {
948
+ object: "branch_claim";
949
+ claim: BranchClaimInfo;
950
+ /** True = the caller already held this claim (re-mint / re-mount path). */
951
+ reclaimedOwn: boolean;
952
+ branch: string;
953
+ token: string;
954
+ gatewayWsUrl: string;
955
+ expiresAtS: number;
956
+ diskId: string;
957
+ }
958
+ /** Result of `POST /session-branch-claims/release` (and the human reclaim
959
+ * door) — the claims this call closed. */
960
+ export interface BranchClaimsReleased {
961
+ object: "branch_claim.release";
962
+ released: BranchClaimInfo[];
963
+ }
964
+ /** `GET /conversations/:id/branch-claim` — one session's claim state: the
965
+ * live claim ON its branch (null = writable there), and the claims it
966
+ * HOLDS on other sessions' branches. */
967
+ export interface BranchClaimState {
968
+ object: "branch_claim.state";
969
+ claimedBy: BranchClaimInfo | null;
970
+ holds: BranchClaimInfo[];
971
+ }
664
972
  /** One agent on the team, as listed by `GET /api/v1/agents`. Bridge-runtime
665
973
  * rows additionally carry live presence (`online`, from the daemon's
666
974
  * heartbeat within the server's TTL) and the hosting machine's label. */
@@ -180,6 +180,46 @@ export interface FactoryFileSearchResult {
180
180
  /** True when more directories matched than the server's folder bound. */
181
181
  folders_truncated?: boolean;
182
182
  }
183
+ /** One file row of `GET /factories/:slug/files` — the flat recursive
184
+ * listing. Same row shape live and at a pinned head; rows arrive
185
+ * camelCase on the wire (mirrors `FactoryFileSearchRow`). */
186
+ export interface FactoryFileListRow {
187
+ id: string;
188
+ path: string;
189
+ sizeBytes: number;
190
+ contentType: string | null;
191
+ contentHash: string;
192
+ deletedAt: string | null;
193
+ createdAt: string;
194
+ updatedAt: string;
195
+ }
196
+ export interface ListFactoryFilesOptions {
197
+ factorySlug?: string;
198
+ /** Only paths under this prefix. */
199
+ prefix?: string;
200
+ /** Path cursor from a prior page's `nextCursor`. */
201
+ cursor?: string;
202
+ limit?: number;
203
+ /** List AS OF a drive branch (e.g. a session's `session-<uuid>`). */
204
+ branch?: string;
205
+ /** Browse-at-head: a retained 64-hex commit of the MAIN drive (or, with
206
+ * `branch`, a pin of that graph branch). The server answers 410
207
+ * `at_unavailable` for a garbage-collected/unknown head. */
208
+ at?: string;
209
+ }
210
+ /** One page of `GET /factories/:slug/files`. */
211
+ export interface FactoryFileListPage {
212
+ data: FactoryFileListRow[];
213
+ hasMore: boolean;
214
+ /** Pass as `cursor` to fetch the next page; null on the last page. */
215
+ nextCursor: string | null;
216
+ /** The pinned head this page was read at — non-null exactly when the
217
+ * request carried `at`. `committedAt` is RFC3339 or null (unknown). */
218
+ at: {
219
+ head: string;
220
+ committedAt: string | null;
221
+ } | null;
222
+ }
183
223
  export interface FactoryFileWriteResult {
184
224
  path: string;
185
225
  contentHash: string;
@@ -216,6 +256,27 @@ export interface FactoryRow {
216
256
  createdAt: string;
217
257
  updatedAt: string;
218
258
  }
259
+ /** GET /factories/:slug/perf-summary — team-level sandbox perf aggregates
260
+ * over a window (session_perf_rollups). Aggregate-only by design: sessions
261
+ * are member-gated, so no conversation ids appear here; per-session detail
262
+ * is the member-gated perf-history surface. */
263
+ export interface FactoryPerfSummary {
264
+ /** The aggregated window, ISO 8601. */
265
+ from: string;
266
+ to: string;
267
+ /** Distinct sessions with any rollup bucket in the window. */
268
+ sessions: number;
269
+ /** Rollup buckets (15-min) in the window. */
270
+ buckets: number;
271
+ /** Guest samples folded into those buckets. */
272
+ samples: number;
273
+ cpu_busy_p95_max: number | null;
274
+ mem_used_p95_max: number | null;
275
+ disk_used_p95_max: number | null;
276
+ load1_max: number | null;
277
+ /** Total seconds any session sat ≥90% on a resource, summed. */
278
+ saturated_seconds: number;
279
+ }
219
280
  export interface CreateFactoryInput {
220
281
  slug: string;
221
282
  name: string;
@@ -257,6 +318,48 @@ export interface SecretListEntry {
257
318
  createdAt: string;
258
319
  updatedAt: string;
259
320
  }
321
+ /** One SESSION secret on the wire (metadata only — values are write-only).
322
+ * `kind` 'value' = a session-own secret; 'factory' = an attachment by name
323
+ * to the session factory's secret tier. `ownerUserId` is the user who set
324
+ * it — only the owner or a team admin may replace/remove it. */
325
+ export interface SessionSecretEntry {
326
+ secretKey: string;
327
+ kind: "value" | "factory";
328
+ ownerUserId: string;
329
+ /** Delivery mode (vault v2): 'injected' = the session env file;
330
+ * 'brokered' = an egress-ruleset header — the value never enters the
331
+ * sandbox (call the host WITHOUT auth headers; the platform adds them). */
332
+ delivery: "injected" | "brokered";
333
+ /** Brokered entries: the API host the header rides on. */
334
+ brokerHost: string | null;
335
+ /** Non-null = an ephemeral use-and-scrub copy, and when its TTL lapses. */
336
+ ephemeralExpiresAt: string | null;
337
+ createdAt: string;
338
+ updatedAt: string;
339
+ }
340
+ /** One entry of a session-secret set: a literal value, or a factory
341
+ * attachment by name (`source: "factory"`). */
342
+ export type SessionSecretInput = {
343
+ key: string;
344
+ value: string;
345
+ } | {
346
+ key: string;
347
+ source: "factory";
348
+ };
349
+ /** A freshly minted vault link (session secret request). `url` is the
350
+ * single-use page a session writer opens to provide the named values. */
351
+ export interface SessionSecretRequestCreated {
352
+ requestId: string;
353
+ url: string;
354
+ expiresAt: string;
355
+ }
356
+ /** A vault link's polled status. */
357
+ export interface SessionSecretRequestStatus {
358
+ requestId: string;
359
+ status: "pending" | "fulfilled" | "cancelled" | "expired";
360
+ keys: string[];
361
+ expiresAt: string;
362
+ }
260
363
  export interface CreateApiKeyInput {
261
364
  name?: string;
262
365
  scopes?: string[];
@@ -320,9 +423,9 @@ export interface UsageResponse {
320
423
  }
321
424
  /** One drive-directory ⇄ GitHub-repo link (`/factories/:slug/repo-links`).
322
425
  * 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. */
426
+ * of a repo occupy sibling `base@<branch>` placements. Event delivery is
427
+ * app-level (the platform GitHub App's own webhook) or polling — there
428
+ * is no per-repo webhook setup (retired 2026-08-18). */
326
429
  export interface DriveRepoLink {
327
430
  id: string;
328
431
  factoryId: string;
@@ -337,11 +440,13 @@ export interface DriveRepoLink {
337
440
  connectorGrantId: string | null;
338
441
  /** v1 links are always 'write' (the round trip is the feature). */
339
442
  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). */
443
+ /** HOW events reach this link. "app": the platform GitHub App's own
444
+ * registered webhook delivers every installed repo's events (zero
445
+ * per-repo setup). "polling": no app webhook configured — the sweep
446
+ * alone. */
447
+ eventDelivery: "app" | "polling";
448
+ /** When an app-signed GitHub delivery last PROVED event delivery
449
+ * reaches this link; null = unproven. */
345
450
  webhookVerifiedAt: string | null;
346
451
  /** Two-way sync: drive edits under the prefix push back to the tracked
347
452
  * branch. ON by default for new links. */
@@ -368,8 +473,8 @@ export interface DriveRepoLink {
368
473
  }
369
474
  /** Input for `POST /factories/:slug/repo-links`. Requires the drive to be
370
475
  * graph-authoritative (ADR-0058 Phase 4 promoted) — 409 otherwise.
371
- * Exactly ONE of `followAllBranches` / `trackedBranches`:
372
- * `followAllBranches: true` (the default connect mode) links EVERY
476
+ * At most one of `followAllBranches` / `trackedBranches`; omitting BOTH
477
+ * is the follow-all default. Follow-all links EVERY
373
478
  * branch — default branch primary at the plain `dirPrefix` — and keeps
374
479
  * following live (new branches auto-link on push, deleted branches
375
480
  * auto-unlink; capped at 100 branches). Explicit `trackedBranches`:
@@ -262,6 +262,27 @@ export interface RunFundingResponse {
262
262
  truncated: boolean;
263
263
  };
264
264
  }
265
+ /** One step's attested platform token totals — `GET
266
+ * /workflows/:id/step-usage`. Derived server-side from the spend ledger
267
+ * by step time window (steps run serially, so every gateway call falls in
268
+ * exactly one). Steps with no ledger rows are absent from the list. */
269
+ export interface RunStepUsage {
270
+ stepIndex: number;
271
+ costUsd: number;
272
+ promptTokens: number;
273
+ completionTokens: number;
274
+ cacheReadTokens: number;
275
+ cacheCreationTokens: number;
276
+ calls: number;
277
+ }
278
+ /** `GET /workflows/:id/step-usage`. Empty `steps` is an honest answer: a
279
+ * legacy run with no ledger rows, or a run funded outside the platform
280
+ * lane (that money is never metered — ADR-0048). */
281
+ export interface RunStepUsageResponse {
282
+ object: "run.step_usage";
283
+ runId: string;
284
+ steps: RunStepUsage[];
285
+ }
265
286
  /** One row from the factory runs list (`GET /factories/:slug/runs`) — the
266
287
  * summary shape, a strict subset of what the route returns. */
267
288
  export interface RunListEntry {
@@ -113,7 +113,38 @@ export interface AgentMessagePlan extends AgentMessageBase {
113
113
  status: "pending" | "in_progress" | "completed";
114
114
  }[];
115
115
  }
116
- export type AgentMessage = AgentMessageInit | AgentMessageText | AgentMessageTextDelta | AgentMessageThinking | AgentMessageToolUse | AgentMessageToolResult | AgentMessageDone | AgentMessageError | AgentMessageUsage | AgentMessageUsageDelta | AgentMessagePlan;
116
+ /** A harness `<task-notification>` announcing a BACKGROUND task stopped —
117
+ * an async Agent spawn finishing, a background command exiting, a workflow
118
+ * completing. claude-code injects these as user-role text blocks; the
119
+ * normaliser parses them into structure so downstream can complete the
120
+ * spawning call's card (status, final report, usage) instead of dropping
121
+ * the only completion evidence a background subagent ever emits. Internal
122
+ * plumbing in the raw block (output-file paths, resume hints) is
123
+ * deliberately NOT forwarded — renderers must never see it. Additive
124
+ * kind: existing producers never emit it. */
125
+ export interface AgentMessageTaskNotification extends AgentMessageBase {
126
+ type: "task_notification";
127
+ /** The harness's background task id (`<task-id>`). */
128
+ taskId: string;
129
+ /** The SPAWNING tool_use id (`<tool-use-id>`) when the notification names
130
+ * one — the correlation key back to the Agent/Task call. */
131
+ toolUseId?: string;
132
+ /** Terminal status word — "completed" | "failed" | "stopped" | "killed"
133
+ * (verbatim from the harness; "finished" when the block carried none). */
134
+ status: string;
135
+ /** One-line what-happened ("Agent \"…\" finished"), collapsed + clamped. */
136
+ summary: string;
137
+ /** The task's final report (`<result>`), entity-unescaped and clamped.
138
+ * Absent when the notification carried none. */
139
+ report?: string;
140
+ /** The completion's usage block, when carried. */
141
+ usage?: {
142
+ tokens?: number;
143
+ toolUses?: number;
144
+ durationMs?: number;
145
+ };
146
+ }
147
+ export type AgentMessage = AgentMessageInit | AgentMessageText | AgentMessageTextDelta | AgentMessageThinking | AgentMessageToolUse | AgentMessageToolResult | AgentMessageDone | AgentMessageError | AgentMessageUsage | AgentMessageUsageDelta | AgentMessagePlan | AgentMessageTaskNotification;
117
148
  /** Status block the agent emits to signal iteration completion or blockers. */
118
149
  export interface AgentStatus {
119
150
  summary: string;