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