@otto-code/protocol 0.7.5 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/dist/agent-attention-notification.d.ts +2 -0
  2. package/dist/agent-attention-notification.js +2 -1
  3. package/dist/agent-deep-link.d.ts +8 -0
  4. package/dist/agent-deep-link.js +49 -0
  5. package/dist/agent-labels.js +4 -4
  6. package/dist/agent-personalities.d.ts +2 -2
  7. package/dist/agent-personalities.js +17 -17
  8. package/dist/agent-teams.d.ts +4 -4
  9. package/dist/agent-teams.js +5 -5
  10. package/dist/agent-types.d.ts +12 -10
  11. package/dist/agent-types.js +1 -1
  12. package/dist/artifacts/rpc-schemas.js +4 -4
  13. package/dist/artifacts/types.js +3 -3
  14. package/dist/binary-frames/file-transfer.d.ts +1 -0
  15. package/dist/binary-frames/file-transfer.js +1 -0
  16. package/dist/browser-automation/rpc-schemas.js +1 -1
  17. package/dist/client-capabilities.d.ts +3 -0
  18. package/dist/client-capabilities.js +9 -0
  19. package/dist/daemon-endpoints.d.ts +1 -1
  20. package/dist/default-personalities.js +11 -11
  21. package/dist/effort.d.ts +1 -1
  22. package/dist/effort.js +2 -2
  23. package/dist/forge-manifest.d.ts +66 -0
  24. package/dist/forge-manifest.js +92 -0
  25. package/dist/generated/validation/ws-outbound.aot.js +77653 -51519
  26. package/dist/git-hosting.js +3 -3
  27. package/dist/git-remote.d.ts +17 -0
  28. package/dist/git-remote.js +23 -2
  29. package/dist/judge-verdict.d.ts +1 -1
  30. package/dist/judge-verdict.js +2 -2
  31. package/dist/messages.d.ts +10634 -135
  32. package/dist/messages.js +1744 -193
  33. package/dist/model-tiers.d.ts +3 -3
  34. package/dist/model-tiers.js +4 -4
  35. package/dist/observed-subagent-title.d.ts +2 -2
  36. package/dist/observed-subagent-title.js +4 -4
  37. package/dist/orchestration.d.ts +3 -3
  38. package/dist/orchestration.js +32 -32
  39. package/dist/otto-config-schema.d.ts +21 -0
  40. package/dist/otto-config-schema.js +23 -1
  41. package/dist/provider-config.d.ts +92 -3
  42. package/dist/provider-config.js +58 -6
  43. package/dist/provider-manifest.d.ts +11 -1
  44. package/dist/provider-manifest.js +53 -5
  45. package/dist/schedule/cadence.d.ts +6 -0
  46. package/dist/schedule/cadence.js +28 -0
  47. package/dist/schedule/rpc-schemas.d.ts +4 -4
  48. package/dist/schedule/rpc-schemas.js +1 -1
  49. package/dist/schedule/types.d.ts +1 -1
  50. package/dist/schedule/types.js +3 -3
  51. package/dist/tool-call-display.d.ts +1 -1
  52. package/dist/tool-call-display.js +3 -3
  53. package/dist/tool-name-normalization.d.ts +2 -2
  54. package/dist/tool-name-normalization.js +2 -2
  55. package/dist/validation/ws-outbound-schema-metadata.d.ts +1950 -52
  56. package/dist/widgets/bridge.d.ts +5 -5
  57. package/dist/widgets/bridge.js +5 -5
  58. package/dist/widgets/document.d.ts +1 -1
  59. package/dist/widgets/document.js +4 -4
  60. package/dist/widgets/icons.d.ts +1 -1
  61. package/dist/widgets/icons.js +1 -1
  62. package/dist/widgets/theme.d.ts +2 -2
  63. package/dist/widgets/theme.js +2 -2
  64. package/dist/widgets/types.d.ts +5 -5
  65. package/dist/widgets/types.js +5 -5
  66. package/package.json +1 -1
package/dist/messages.js CHANGED
@@ -51,7 +51,7 @@ const MutableMetadataGenerationConfigSchema = z
51
51
  // true preserves today's behavior. Read by the generation path (WP-B).
52
52
  enabled: z.boolean().default(true),
53
53
  // When true, metadata generation prefers a role-matched Writer personality
54
- // over the cheap default tier. Default false cheap-tier routing is the
54
+ // over the cheap default tier. Default false - cheap-tier routing is the
55
55
  // default. Read by the generation routing (WP-B).
56
56
  preferWriterPersonalities: z.boolean().default(false),
57
57
  })
@@ -97,11 +97,11 @@ const MutableBrowserToolsConfigSchema = z
97
97
  .passthrough();
98
98
  /**
99
99
  * Language-server code intelligence, host-scoped because the servers are processes
100
- * on the daemon's machine they follow the host, not the client.
100
+ * on the daemon's machine - they follow the host, not the client.
101
101
  *
102
102
  * `enabled` defaults **on** and that is safe: nothing spawns until a
103
103
  * code-intelligence action needs a language in a workspace, so an unused language
104
- * costs nothing. What the switch guarantees is that off means off no server
104
+ * costs nothing. What the switch guarantees is that off means off - no server
105
105
  * spawns for any workspace, and the ctags index still serves the outline and the
106
106
  * fuzzy finder.
107
107
  *
@@ -121,16 +121,16 @@ const MutableLspConfigSchema = z
121
121
  })
122
122
  .passthrough();
123
123
  /**
124
- * "Microsoft .NET Solution Management" the Solution view's own switch.
124
+ * "Microsoft .NET Solution Management" - the Solution view's own switch.
125
125
  *
126
126
  * **A sibling of `lsp`, not a member of it.** Turning C# code intelligence off does not turn
127
127
  * this off and vice versa: they are independent capabilities that happen to share a language,
128
128
  * and nesting this inside the LSP settings object would imply exactly the coupling that
129
- * decision rejects. (It would also be wrong on the facts LSP has no project-structure
129
+ * decision rejects. (It would also be wrong on the facts - LSP has no project-structure
130
130
  * request, so nothing here rides on a language server.)
131
131
  *
132
132
  * Defaults **off**: the feature spawns a process and evaluates MSBuild. Disabled is genuinely
133
- * off, not merely hidden no discovery walk, no `.sln` read, no `.csproj` parse, no sidecar,
133
+ * off, not merely hidden - no discovery walk, no `.sln` read, no `.csproj` parse, no sidecar,
134
134
  * no cache, no watcher, and no view switcher. The daemon reads this before scheduling any work,
135
135
  * so a disabled feature costs exactly one boolean check.
136
136
  */
@@ -214,26 +214,26 @@ export const MutableGitHostingConfigSchema = z
214
214
  .passthrough();
215
215
  // Canonical personality roles, in display order. Kept as an exported const so
216
216
  // the daemon and app share one vocabulary, but the wire schema stores roles as
217
- // plain strings (below) adding a role later must never break an older peer's
217
+ // plain strings (below) - adding a role later must never break an older peer's
218
218
  // parsing. Consumers filter incoming role arrays to this known set. The retired
219
219
  // "worker" role is mapped to "coder" on the way in (see LEGACY_ROLE_ALIASES in
220
220
  // agent-personalities.ts) so personalities persisted before the split keep their
221
221
  // role rather than silently losing it.
222
222
  export const PERSONALITY_ROLES = [
223
- // Surfaces the interactive / host-facing entry points.
223
+ // Surfaces - the interactive / host-facing entry points.
224
224
  "chatter",
225
225
  "artificer",
226
226
  "scheduler",
227
- // Thinking workers read-only, return structured findings, never edit.
227
+ // Thinking workers - read-only, return structured findings, never edit.
228
228
  "researcher",
229
229
  "planner",
230
230
  "judger",
231
231
  "advisor",
232
- // Making workers produce code, design, or short text.
232
+ // Making workers - produce code, design, or short text.
233
233
  "coder",
234
234
  "designer",
235
235
  "writer",
236
- // Conductor the sole role whose whole job is planning and driving a team.
236
+ // Conductor - the sole role whose whole job is planning and driving a team.
237
237
  "orchestrator",
238
238
  ];
239
239
  // Two glow colors for the personality's thinking spinner (BlobLoader glowA/glowB).
@@ -243,8 +243,8 @@ const AgentPersonalitySpinnerSchema = z
243
243
  glowB: z.string().min(1),
244
244
  })
245
245
  .passthrough();
246
- // A TTS voice for the personality's spoken identity. Stored self-describing
247
- // provider + model + voice name because voice names are namespaced per TTS
246
+ // A TTS voice for the personality's spoken identity. Stored self-describing -
247
+ // provider + model + voice name - because voice names are namespaced per TTS
248
248
  // engine/model (the same speaker index maps to different names across models),
249
249
  // so a bare name is ambiguous across hosts. All plain strings (like the speech
250
250
  // config) for forward-compat. This is a soft binding: an unavailable voice
@@ -258,17 +258,17 @@ const AgentPersonalityVoiceSchema = z
258
258
  })
259
259
  .passthrough();
260
260
  // The Visualizer lifecycle moments a personality voice-cue line can belong to.
261
- // Protocol owns this vocabulary the daemon's cue generator, the personality
261
+ // Protocol owns this vocabulary - the daemon's cue generator, the personality
262
262
  // editor, and the Visualizer playback hook all import it from here.
263
263
  // "waiting" is the parent's turn ending while its observed sub-agents are still
264
264
  // running; it DEFERS "done" rather than replacing it (see docs/visualizer.md).
265
265
  export const CUE_MOMENTS = ["join", "thinking", "waiting", "done"];
266
266
  // Pre-generated (and user-editable) spoken "voice cue" lines for the personality
267
- // a few short variations for each Visualizer moment (its node joins the graph,
267
+ // - a few short variations for each Visualizer moment (its node joins the graph,
268
268
  // first starts thinking, finishes its turn but waits on sub-agents, completes).
269
269
  // Stored on the personality so they're deterministic and hand-tunable in the
270
270
  // editor; the Visualizer reads them directly (no runtime generation). All groups
271
- // optional/loose a personality may have none, or only some (personalities
271
+ // optional/loose - a personality may have none, or only some (personalities
272
272
  // authored before "waiting" existed simply stay silent for that moment).
273
273
  // See docs/visualizer.md "Voice cues".
274
274
  const AgentPersonalityVoiceCuesSchema = z
@@ -326,7 +326,7 @@ const MutableAgentPersonalitiesConfigPatchSchema = z
326
326
  })
327
327
  .passthrough();
328
328
  // A team's avatar. v1 ships only `color` (hex, validated at the editor like
329
- // spinner colors); `imageId` is reserved for the future themed avatar set
329
+ // spinner colors); `imageId` is reserved for the future themed avatar set -
330
330
  // when present it wins over color, and color stays the fallback so an old
331
331
  // client that doesn't know `imageId` keeps rendering the swatch. Plain
332
332
  // strings for forward compat.
@@ -340,7 +340,7 @@ const AgentTeamAvatarSchema = z
340
340
  // template: which personalities are on deck, plus a shared team prompt stacked
341
341
  // directly ahead of the member's personality prompt at spawn. `id` is the
342
342
  // stable identity everything binds to; `name` is a freely-renamable label.
343
- // `memberIds` bind personality ids (order = display order) an entry pointing
343
+ // `memberIds` bind personality ids (order = display order) - an entry pointing
344
344
  // at a deleted personality is tolerated and ignored everywhere, then pruned on
345
345
  // the next save of that team. Membership is many-to-many.
346
346
  export const AgentTeamSchema = z
@@ -376,7 +376,7 @@ export const ModelTierSchema = z.enum(["deep", "standard", "fast"]);
376
376
  // A user's explicit tier tag for one model of one provider. The daemon stamps
377
377
  // `model.tier` at ingest, preferring a matching override here over inference
378
378
  // (see model-tiers.ts). Stored as an array (not a nested record) so a patch
379
- // replaces it wholesale that's how a tag gets cleared, since deep-merge can't
379
+ // replaces it wholesale - that's how a tag gets cleared, since deep-merge can't
380
380
  // delete a record key.
381
381
  export const ModelTierOverrideSchema = z
382
382
  .object({
@@ -390,13 +390,13 @@ export const ModelTierOverrideSchema = z
390
390
  // pick instead of re-typing the key. Entries are scoped by the connection
391
391
  // env-var pair they belong to (OPENAI_BASE_URL/OPENAI_API_KEY vs
392
392
  // ANTHROPIC_BASE_URL/ANTHROPIC_AUTH_TOKEN), which is exactly what the provider
393
- // settings sheet keys its dropdown off so every openai-compatible provider
393
+ // settings sheet keys its dropdown off - so every openai-compatible provider
394
394
  // entry on the host shares one pool, and Claude-compatible entries share
395
395
  // another. Deliberately `z.string()` rather than an enum: a future env-var
396
396
  // family must not make old entries unparseable.
397
397
  export const SavedProviderEndpointSchema = z
398
398
  .object({
399
- /** Stable identity, `${baseUrlKey}::${baseUrl}` dedupes on re-save. */
399
+ /** Stable identity, `${baseUrlKey}::${baseUrl}` - dedupes on re-save. */
400
400
  id: z.string().min(1),
401
401
  baseUrlKey: z.string().min(1),
402
402
  apiKeyKey: z.string().min(1),
@@ -431,6 +431,12 @@ export const MutableBrainRemoteConfigSchema = z
431
431
  secure: z.boolean().default(false),
432
432
  // Secret: masked with DAEMON_CONFIG_SECRET_SENTINEL on the way out.
433
433
  authToken: z.string().nullable().default(null),
434
+ // SHA-256 fingerprint of the remote brain's TLS certificate (openssl's
435
+ // "AB:CD:..." form; colons optional). When set, the daemon pins HTTPS
436
+ // connections to exactly this certificate instead of the system trust
437
+ // store - required for a brain serving tls.mode=self-signed. When null,
438
+ // the certificate must validate against the system trust store.
439
+ certFingerprint: z.string().nullable().default(null),
434
440
  })
435
441
  .passthrough();
436
442
  export const MutableBrainConfigSchema = z
@@ -446,6 +452,7 @@ export const MutableBrainConfigSchema = z
446
452
  port: 1234,
447
453
  secure: false,
448
454
  authToken: null,
455
+ certFingerprint: null,
449
456
  }),
450
457
  listen: z
451
458
  .object({
@@ -477,13 +484,13 @@ export const MutableBrainConfigSchema = z
477
484
  }),
478
485
  })
479
486
  .passthrough();
480
- // The brain PATCH schema deliberately NOT `MutableBrainConfigSchema.partial()`.
487
+ // The brain PATCH schema - deliberately NOT `MutableBrainConfigSchema.partial()`.
481
488
  // Every field of the full schema carries a `.default()` (so an old daemon's
482
489
  // half-written config still parses as a well-formed OFF section), and Zod keeps
483
490
  // those defaults through `.partial()`: `MutableBrainConfigSchema.partial().parse(
484
491
  // { allowRemoteConfig: true })` expands to the FULL object with every other field
485
492
  // defaulted. The daemon deep-merges the parsed patch over the stored config, so a
486
- // single-field patch would silently reset the entire brain block to defaults
493
+ // single-field patch would silently reset the entire brain block to defaults -
487
494
  // turning sharing off (host back to loopback), wiping the auth token, and
488
495
  // disabling the server. Mirroring the shape WITHOUT defaults keeps an omitted
489
496
  // field omitted, so the deep-merge preserves it. Every level is deep-partial so a
@@ -507,6 +514,7 @@ const MutableBrainRemotePatchSchema = z
507
514
  port: z.number().int(),
508
515
  secure: z.boolean(),
509
516
  authToken: z.string().nullable(),
517
+ certFingerprint: z.string().nullable(),
510
518
  })
511
519
  .partial()
512
520
  .passthrough();
@@ -538,7 +546,7 @@ export const DEFAULT_MUTABLE_BRAIN_CONFIG = {
538
546
  enabled: false,
539
547
  autoStart: false,
540
548
  mode: "local",
541
- remote: { host: "", port: 1234, secure: false, authToken: null },
549
+ remote: { host: "", port: 1234, secure: false, authToken: null, certFingerprint: null },
542
550
  listen: { host: "127.0.0.1", port: 1234 },
543
551
  defaultModel: null,
544
552
  lockModel: false,
@@ -567,7 +575,7 @@ export const MutableDaemonConfigSchema = z
567
575
  toolGroups: z.array(z.enum(OTTO_TOOL_GROUPS)).optional(),
568
576
  })
569
577
  .passthrough(),
570
- // Defaults off, matching the daemon's own resolution browser tools are an
578
+ // Defaults off, matching the daemon's own resolution - browser tools are an
571
579
  // explicit opt-in, so an omitted section must never read as on.
572
580
  browserTools: MutableBrowserToolsConfigSchema.default({ enabled: false }),
573
581
  // Daemon-wide agent behavior toggles (Claude-tier capabilities). Defaults to
@@ -659,11 +667,12 @@ export const MutableDaemonConfigPatchSchema = z
659
667
  // Gated by server_info features.agentBehaviorToggles; patches deep-merge.
660
668
  agentBehaviors: MutableAgentBehaviorsConfigSchema.partial().optional(),
661
669
  // A null entry removes the provider's config entirely (custom provider
662
- // uninstall). Gated by server_info features.providerRemove old daemons
670
+ // uninstall). Gated by server_info features.providerRemove - old daemons
663
671
  // reject null values.
664
672
  providers: z
665
673
  .record(z.string(), MutableDaemonProviderConfigSchema.partial().passthrough().nullable())
666
674
  .optional(),
675
+ removeProviders: z.array(z.string().min(1)).optional(),
667
676
  metadataGeneration: MutableMetadataGenerationConfigSchema.partial().optional(),
668
677
  autoArchiveAfterMerge: z.boolean().optional(),
669
678
  // Gated by server_info features.hideMergeIntoBaseSetting.
@@ -745,7 +754,7 @@ export const AgentRateLimitInfoSchema = z.object({
745
754
  // does not report it (Claude only includes it near the limit).
746
755
  utilizationPercent: z.number().optional(),
747
756
  // Provider-reported window identifier, e.g. "five_hour" | "seven_day".
748
- // Open set display code falls back to a generic label for unknown values.
757
+ // Open set - display code falls back to a generic label for unknown values.
749
758
  limitType: z.string().optional(),
750
759
  // ISO 8601 timestamp when the window resets.
751
760
  resetsAt: z.string().optional(),
@@ -796,6 +805,7 @@ export const ProviderSnapshotEntrySchema = z.object({
796
805
  provider: AgentProviderSchema,
797
806
  status: ProviderStatusSchema,
798
807
  enabled: z.boolean().optional().default(true),
808
+ source: z.enum(["builtin", "custom"]).optional(),
799
809
  error: z.string().optional(),
800
810
  models: z.array(AgentModelDefinitionSchema).optional(),
801
811
  modes: z.array(AgentModeSchema).optional(),
@@ -848,7 +858,7 @@ const AgentUsageSchema = z.object({
848
858
  // Provider-graded context breakdown for the visualizer ring/bar; absent ⇒
849
859
  // occupancy only (pre-composition behavior). See ContextComposition.
850
860
  contextComposition: ContextCompositionSchema.optional(),
851
- // The provider's own labelled split same accounting as
861
+ // The provider's own labelled split - same accounting as
852
862
  // `agent.context.get_usage`, pushed on the snapshot. Preferred over
853
863
  // contextComposition; absent ⇒ fall back to the estimate, then to occupancy.
854
864
  contextCategories: z.array(AgentContextCategorySchema).optional(),
@@ -1069,6 +1079,7 @@ export const AgentTimelineItemPayloadSchema = z.union([
1069
1079
  type: z.literal("user_message"),
1070
1080
  text: z.string(),
1071
1081
  messageId: z.string().optional(),
1082
+ clientMessageId: z.string().optional(),
1072
1083
  }),
1073
1084
  z.object({
1074
1085
  type: z.literal("assistant_message"),
@@ -1095,7 +1106,9 @@ export const AgentTimelineItemPayloadSchema = z.union([
1095
1106
  type: z.literal("compaction"),
1096
1107
  // COMPAT(compactionFailedStatus): "failed" added in v0.4.3. Clients older
1097
1108
  // than that drop the whole timeline event on parse and keep showing the
1098
- // loading row exactly their pre-"failed" behavior, so no gate is needed.
1109
+ // loading row - exactly their pre-"failed" behavior, so no gate is needed.
1110
+ // Nothing to remove: this tag records why the enum could be widened without
1111
+ // a shim, so it has no cleanup date by design.
1099
1112
  status: z.enum(["loading", "completed", "failed"]),
1100
1113
  trigger: z.enum(["auto", "manual"]).optional(),
1101
1114
  preTokens: z.number().optional(),
@@ -1157,6 +1170,7 @@ export const AgentStreamEventPayloadSchema = z.discriminatedUnion("type", [
1157
1170
  body: z.string(),
1158
1171
  data: z.object({
1159
1172
  serverId: z.string(),
1173
+ workspaceId: z.string().optional(),
1160
1174
  agentId: z.string(),
1161
1175
  reason: z.enum(["finished", "error", "permission"]),
1162
1176
  }),
@@ -1199,7 +1213,7 @@ const AgentRuntimeInfoSchema = z.object({
1199
1213
  /**
1200
1214
  * One message parked for delivery as an agent's NEXT turn (`delivery: "queue"`).
1201
1215
  * The daemon owns the queue; this is the read-only projection the Queue track
1202
- * renders. Declared above AgentSnapshotPayloadSchema zod-aot emits schemas in
1216
+ * renders. Declared above AgentSnapshotPayloadSchema - zod-aot emits schemas in
1203
1217
  * source order, so a forward reference is a build-time ReferenceError.
1204
1218
  */
1205
1219
  export const QueuedAgentMessagePayloadSchema = z.object({
@@ -1211,14 +1225,14 @@ export const QueuedAgentMessagePayloadSchema = z.object({
1211
1225
  /**
1212
1226
  * Who parked the message. Absent (from an older daemon) or "user" is a normal
1213
1227
  * user turn; "system" marks a system-injected entry (a chat mention, a
1214
- * scheduled fire) that the daemon's drain never merges into a user turn the
1228
+ * scheduled fire) that the daemon's drain never merges into a user turn - the
1215
1229
  * client must likewise exclude it from "Send all".
1216
1230
  */
1217
1231
  source: z.enum(["user", "system"]).optional(),
1218
1232
  });
1219
1233
  /**
1220
1234
  * An agent's LIFETIME SPEND, kept as the real token split plus the provider's
1221
- * own cost the raw material for "what did this chat cost".
1235
+ * own cost - the raw material for "what did this chat cost".
1222
1236
  *
1223
1237
  * Deliberately distinct from context-window occupancy (`agent.context.get_usage`
1224
1238
  * and `lastUsage.contextWindow*`), which answers "how full am I" and shares no
@@ -1227,12 +1241,12 @@ export const QueuedAgentMessagePayloadSchema = z.object({
1227
1241
  *
1228
1242
  * `costUsd` is only ever a provider's OWN reported cost, already de-inflated so
1229
1243
  * a parent never carries what its sub-agents reported. It is NEVER derived from
1230
- * a $/M rate table a rate keyed off a model id misprices a gateway serving
1244
+ * a $/M rate table - a rate keyed off a model id misprices a gateway serving
1231
1245
  * that model at its own prices (docs/subagent-accounting.md, pricing invariant).
1232
1246
  * `costCoverage` says how far it can be trusted, so a surface can show a floor
1233
1247
  * or an honest blank rather than a confident wrong figure.
1234
1248
  *
1235
- * Declared above AgentSnapshotPayloadSchema zod-aot emits schemas in source
1249
+ * Declared above AgentSnapshotPayloadSchema - zod-aot emits schemas in source
1236
1250
  * order, so a forward reference is a build-time ReferenceError.
1237
1251
  */
1238
1252
  export const AgentCumulativeUsageSchema = z.object({
@@ -1243,9 +1257,9 @@ export const AgentCumulativeUsageSchema = z.object({
1243
1257
  /** Provider-reported cost booked so far. Absent ⇒ nothing was priceable. */
1244
1258
  costUsd: z.number().optional(),
1245
1259
  /**
1246
- * `complete` every token-bearing turn was priced; `costUsd` is the total.
1247
- * `partial` some turns were unpriced; `costUsd` is a FLOOR, present it as
1248
- * one. `none` nothing priceable; show tokens and a blank, never an estimate.
1260
+ * `complete` - every token-bearing turn was priced; `costUsd` is the total.
1261
+ * `partial` - some turns were unpriced; `costUsd` is a FLOOR, present it as
1262
+ * one. `none` - nothing priceable; show tokens and a blank, never an estimate.
1249
1263
  */
1250
1264
  costCoverage: z.enum(["complete", "partial", "none"]).optional(),
1251
1265
  });
@@ -1270,7 +1284,7 @@ export const AgentSnapshotPayloadSchema = z.object({
1270
1284
  runtimeInfo: AgentRuntimeInfoSchema.optional(),
1271
1285
  lastUsage: AgentUsageSchema.optional(),
1272
1286
  // Honest cumulative token total (Σ across the whole run) from the provider,
1273
- // for the subagents-track cost readout the only currency that works for
1287
+ // for the subagents-track cost readout - the only currency that works for
1274
1288
  // cost-less local models. Observed subagents source it from the provider's
1275
1289
  // per-task usage.total_tokens (already cumulative-per-subagent). Purely
1276
1290
  // additive; absent ⇒ no readout. Old clients ignore it.
@@ -1287,16 +1301,18 @@ export const AgentSnapshotPayloadSchema = z.object({
1287
1301
  cumulativeUsage: AgentCumulativeUsageSchema.optional(),
1288
1302
  // Liveness signals for the sub-agents track: how much work this agent has done
1289
1303
  // (`toolUseCount`, cumulative and monotonic) and what it is doing right now
1290
- // (`currentTool`, the latest tool name, cleared once the agent is terminal
1304
+ // (`currentTool`, the latest tool name, cleared once the agent is terminal -
1291
1305
  // a finished agent isn't "running Bash"). Both are purely additive optional
1292
1306
  // leaves: a provider that can't report them leaves them absent and the row
1293
1307
  // omits the readout rather than showing a wrong value.
1294
- // COMPAT(subagentLiveness): added in v0.6.7; old clients ignore both.
1308
+ // COMPAT(subagentLiveness): added in v0.6.7, drop the optional gate when the
1309
+ // floor is >= v0.6.7. Old clients ignore both fields and show the row without
1310
+ // its liveness readouts, which is their pre-v0.6.7 behaviour.
1295
1311
  // See docs/chat-lifecycle.md (the subagents track).
1296
1312
  toolUseCount: z.number().optional(),
1297
1313
  currentTool: z.string().optional(),
1298
1314
  // Messages parked for delivery as this agent's NEXT turn (delivery: "queue").
1299
- // Daemon-owned and ephemeral the composer's Queue track renders straight
1315
+ // Daemon-owned and ephemeral - the composer's Queue track renders straight
1300
1316
  // from this. Absent/empty ⇒ nothing queued; old clients ignore it.
1301
1317
  // COMPAT(steerQueue): added in v0.6.8, drop the gate when floor >= v0.6.8.
1302
1318
  queuedMessages: z.array(QueuedAgentMessagePayloadSchema).optional(),
@@ -1309,7 +1325,7 @@ export const AgentSnapshotPayloadSchema = z.object({
1309
1325
  archivedAt: z.string().nullable().optional(),
1310
1326
  providerUnavailable: z.boolean().optional(),
1311
1327
  // Attendability. "observed" marks a provider-managed subagent (Claude Task /
1312
- // ultracode fan-out) that the user can watch but not prompt or reconfigure
1328
+ // ultracode fan-out) that the user can watch but not prompt or reconfigure -
1313
1329
  // the daemon refuses attended operations and the client renders it read-only.
1314
1330
  // COMPAT(observedSubagents): added in v0.4.3; absent ⇒ "attended". Drop the
1315
1331
  // gate when daemon floor >= v0.4.3. See projects/observed-subagents/observed-subagents.md.
@@ -1419,7 +1435,7 @@ export const CloseItemsRequestMessageSchema = z.object({
1419
1435
  // OpenCode sessions) is deliberately left in place: Otto never created it,
1420
1436
  // another tool still reads it, and silently deleting another tool's state is not
1421
1437
  // ours to do. There is intentionally **no** opt-in flag for provider data on the
1422
- // wire the UI discloses what stays behind instead. See docs/chat-lifecycle.md.
1438
+ // wire - the UI discloses what stays behind instead. See docs/chat-lifecycle.md.
1423
1439
  // Gated by server_info.features.historyDelete.
1424
1440
  export const HistoryAgentsClearArchivedRequestSchema = z.object({
1425
1441
  type: z.literal("history.agents.clear_archived.request"),
@@ -1433,7 +1449,7 @@ export const HistoryAgentsClearArchivedRequestSchema = z.object({
1433
1449
  export const HistoryAgentsClearArchivedResponseSchema = z.object({
1434
1450
  type: z.literal("history.agents.clear_archived.response"),
1435
1451
  payload: z.object({
1436
- // How many archived records the cutoff selected the number the confirm
1452
+ // How many archived records the cutoff selected - the number the confirm
1437
1453
  // dialog quotes back after a dry run.
1438
1454
  matched: z.number().int().nonnegative(),
1439
1455
  deleted: z.number().int().nonnegative(),
@@ -1490,7 +1506,7 @@ export const AttachmentsImagesClearRequestSchema = z.object({
1490
1506
  olderThanDays: z.number().int().min(0).default(0),
1491
1507
  // Safe by default: a request that omits the flag previews instead of deleting.
1492
1508
  // The client always sends it explicitly. Same contract as
1493
- // history.agents.clear_archived, and for the same reason the client cannot
1509
+ // history.agents.clear_archived, and for the same reason - the client cannot
1494
1510
  // enumerate the set, and there is no undo.
1495
1511
  dryRun: z.boolean().default(true),
1496
1512
  requestId: z.string(),
@@ -1511,6 +1527,27 @@ export const AttachmentsImagesClearResponseSchema = z.object({
1511
1527
  // server_info.features.brainControl (lifecycle) and features.brainStatus
1512
1528
  // (evals). Live status streaming (subscribe_brain_status + brain_status_changed)
1513
1529
  // is added alongside its client/daemon consumers.
1530
+ /**
1531
+ * What the brain on the far side can actually serve. Passthrough so it can grow.
1532
+ *
1533
+ * Declared here rather than beside the Brain Console RPCs below because
1534
+ * BrainHostStatusSchema carries it, and a zod const must be initialised before
1535
+ * the schema that references it (see docs on the AOT validator's declaration
1536
+ * ordering). Moving it back down produces a module-evaluation TDZ error.
1537
+ */
1538
+ export const BrainCapabilitiesSchema = z
1539
+ .object({
1540
+ profiles: z.boolean().default(false),
1541
+ budget: z.boolean().default(false),
1542
+ logs: z.boolean().default(false),
1543
+ delete: z.boolean().default(false),
1544
+ load: z.boolean().default(false),
1545
+ resources: z.boolean().default(false),
1546
+ inventory: z.boolean().default(false),
1547
+ /** Whether writes are permitted right now (the brain's allowRemoteConfig). */
1548
+ writable: z.boolean().default(false),
1549
+ })
1550
+ .passthrough();
1514
1551
  // The brain's host status, as the daemon derives it: liveness plus the fields
1515
1552
  // proxied from the brain's own `/__host/status`. Passthrough on the opaque
1516
1553
  // sub-objects so the brain can evolve them without a protocol bump.
@@ -1533,6 +1570,64 @@ export const BrainHostStatusSchema = z
1533
1570
  telemetry: z.record(z.string(), z.unknown()).nullable().optional(),
1534
1571
  scheduler: z.record(z.string(), z.unknown()).nullable().optional(),
1535
1572
  recent: z.array(z.record(z.string(), z.unknown())).optional(),
1573
+ /**
1574
+ * What the brain on the far side can serve. Carried here rather than fetched
1575
+ * from /__host/capabilities so the client gets it with the status it is
1576
+ * already polling, and so it cannot go stale when the owner toggles remote
1577
+ * configuration. Null from a brain that predates the management API.
1578
+ */
1579
+ capabilities: BrainCapabilitiesSchema.nullable().optional(),
1580
+ /** Live CPU/RAM/GPU/slot telemetry, only when the request asked for it. */
1581
+ resources: z.record(z.string(), z.unknown()).nullable().optional(),
1582
+ /** How many log lines the brain currently holds, for the Logs tab. */
1583
+ logLineCount: z.number().nullable().optional(),
1584
+ /**
1585
+ * Which long-running op currently owns the brain, if any. Cheap to carry:
1586
+ * the brain reads it from a small file beside its pid file, so this rides on
1587
+ * the liveness poll rather than needing `resources: true`.
1588
+ *
1589
+ * `kind` is a plain string, not an enum: the brain may grow ops this client
1590
+ * has never heard of, and the protocol contract forbids narrowing a field
1591
+ * later. Unknown kinds fall through to the ordinary busy states.
1592
+ */
1593
+ activity: z
1594
+ .object({
1595
+ kind: z.string(),
1596
+ target: z.string().nullable().optional(),
1597
+ /** Completion in [0,1], for ops that can measure it. */
1598
+ progress: z.number().nullable().optional(),
1599
+ startedAt: z.string().nullable().optional(),
1600
+ })
1601
+ .passthrough()
1602
+ .nullable()
1603
+ .optional(),
1604
+ /**
1605
+ * Whether any in-flight completion is currently mid-reasoning: reasoning
1606
+ * deltas have arrived and no content has yet. Also on the liveness poll,
1607
+ * because it is one boolean the router already knows.
1608
+ */
1609
+ reasoning: z.boolean().nullable().optional(),
1610
+ /**
1611
+ * Slot occupancy split by phase, so a client can tell a prompt being
1612
+ * ingested from a model that has started answering. Rides here rather than
1613
+ * inside `resources` on purpose: `resources` costs an `nvidia-smi` spawn and
1614
+ * is off by default, and this is the half the status rail needs every poll.
1615
+ */
1616
+ slots: z
1617
+ .object({
1618
+ total: z.number().nullable().optional(),
1619
+ busy: z.number().nullable().optional(),
1620
+ idle: z.number().nullable().optional(),
1621
+ prefill: z.number().nullable().optional(),
1622
+ decode: z.number().nullable().optional(),
1623
+ })
1624
+ .passthrough()
1625
+ .nullable()
1626
+ .optional(),
1627
+ /** Jobs the scheduler is holding because no slot is free, or a swap is mid-flight. */
1628
+ queued: z.number().nullable().optional(),
1629
+ /** Whether the daemon reached the brain on its last probe. */
1630
+ reachable: z.boolean().nullable().optional(),
1536
1631
  })
1537
1632
  .passthrough();
1538
1633
  const BrainHostStatusResultSchema = z.object({
@@ -1542,6 +1637,13 @@ const BrainHostStatusResultSchema = z.object({
1542
1637
  });
1543
1638
  export const BrainHostStatusRequestSchema = z.object({
1544
1639
  type: z.literal("brain.host.status.request"),
1640
+ /**
1641
+ * Ask for live CPU/RAM/GPU/slot telemetry alongside the status. Off by default
1642
+ * on purpose: it costs an `nvidia-smi` spawn plus a /slots round trip on the
1643
+ * brain, and this RPC is also the liveness poll. Only a surface actually
1644
+ * rendering the numbers should turn it on.
1645
+ */
1646
+ resources: z.boolean().default(false),
1545
1647
  requestId: z.string(),
1546
1648
  });
1547
1649
  export const BrainHostStatusResponseSchema = z.object({
@@ -1779,7 +1881,7 @@ const BrainJobsResultSchema = z.object({
1779
1881
  error: z.string().nullable(),
1780
1882
  requestId: z.string(),
1781
1883
  });
1782
- // Installed models `otto-brain scan`.
1884
+ // Installed models - `otto-brain scan`.
1783
1885
  export const BrainModelsScanRequestSchema = z.object({
1784
1886
  type: z.literal("brain.models.scan.request"),
1785
1887
  requestId: z.string(),
@@ -1792,7 +1894,7 @@ export const BrainModelsScanResponseSchema = z.object({
1792
1894
  requestId: z.string(),
1793
1895
  }),
1794
1896
  });
1795
- // Downloadable catalog `otto-brain catalog`.
1897
+ // Downloadable catalog - `otto-brain catalog`.
1796
1898
  export const BrainCatalogListRequestSchema = z.object({
1797
1899
  type: z.literal("brain.catalog.list.request"),
1798
1900
  requestId: z.string(),
@@ -1805,7 +1907,7 @@ export const BrainCatalogListResponseSchema = z.object({
1805
1907
  requestId: z.string(),
1806
1908
  }),
1807
1909
  });
1808
- // Installed runtimes `otto-brain runtime list`.
1910
+ // Installed runtimes - `otto-brain runtime list`.
1809
1911
  export const BrainRuntimeListRequestSchema = z.object({
1810
1912
  type: z.literal("brain.runtime.list.request"),
1811
1913
  requestId: z.string(),
@@ -1818,7 +1920,7 @@ export const BrainRuntimeListResponseSchema = z.object({
1818
1920
  requestId: z.string(),
1819
1921
  }),
1820
1922
  });
1821
- // Download a catalog model starts a `pull` job.
1923
+ // Download a catalog model - starts a `pull` job.
1822
1924
  export const BrainModelsPullRequestSchema = z.object({
1823
1925
  type: z.literal("brain.models.pull.request"),
1824
1926
  // Catalog id or name fragment.
@@ -1829,7 +1931,7 @@ export const BrainModelsPullResponseSchema = z.object({
1829
1931
  type: z.literal("brain.models.pull.response"),
1830
1932
  payload: BrainJobResultSchema,
1831
1933
  });
1832
- // Install a llama.cpp runtime starts a `runtime-install` job.
1934
+ // Install a llama.cpp runtime - starts a `runtime-install` job.
1833
1935
  export const BrainRuntimeInstallRequestSchema = z.object({
1834
1936
  type: z.literal("brain.runtime.install.request"),
1835
1937
  // Optional llama.cpp release build tag; null = the brain's default.
@@ -1840,7 +1942,7 @@ export const BrainRuntimeInstallResponseSchema = z.object({
1840
1942
  type: z.literal("brain.runtime.install.response"),
1841
1943
  payload: BrainJobResultSchema,
1842
1944
  });
1843
- // Measure real KV bytes/token for a model starts a `calibrate` job. Needs a
1945
+ // Measure real KV bytes/token for a model - starts a `calibrate` job. Needs a
1844
1946
  // runtime + GPU; refused with a helpful error otherwise.
1845
1947
  export const BrainCalibrateRequestSchema = z.object({
1846
1948
  type: z.literal("brain.calibrate.request"),
@@ -1851,7 +1953,7 @@ export const BrainCalibrateResponseSchema = z.object({
1851
1953
  type: z.literal("brain.calibrate.response"),
1852
1954
  payload: BrainJobResultSchema,
1853
1955
  });
1854
- // Find the best reasoning budget for a model starts a `sweep` job.
1956
+ // Find the best reasoning budget for a model - starts a `sweep` job.
1855
1957
  export const BrainSweepRequestSchema = z.object({
1856
1958
  type: z.literal("brain.sweep.request"),
1857
1959
  model: z.string(),
@@ -1861,7 +1963,7 @@ export const BrainSweepResponseSchema = z.object({
1861
1963
  type: z.literal("brain.sweep.response"),
1862
1964
  payload: BrainJobResultSchema,
1863
1965
  });
1864
- // Run the agentic-coding benchmark starts a `bench` job. `model` is an
1966
+ // Run the agentic-coding benchmark - starts a `bench` job. `model` is an
1865
1967
  // optional comma list of name fragments; null lets the brain pick.
1866
1968
  export const BrainBenchRequestSchema = z.object({
1867
1969
  type: z.literal("brain.bench.request"),
@@ -1908,7 +2010,7 @@ export const BrainHfSearchResultSchema = z
1908
2010
  installed: z.boolean().default(false),
1909
2011
  })
1910
2012
  .passthrough();
1911
- // One downloadable quantization of a repo `otto-brain add <repo> --list-quants`.
2013
+ // One downloadable quantization of a repo - `otto-brain add <repo> --list-quants`.
1912
2014
  export const BrainRepoQuantSchema = z
1913
2015
  .object({
1914
2016
  quant: z.string().default(""),
@@ -1919,7 +2021,7 @@ export const BrainRepoQuantSchema = z
1919
2021
  installed: z.boolean().default(false),
1920
2022
  })
1921
2023
  .passthrough();
1922
- // Search Hugging Face for GGUF models `otto-brain search <query>`.
2024
+ // Search Hugging Face for GGUF models - `otto-brain search <query>`.
1923
2025
  export const BrainHfSearchRequestSchema = z.object({
1924
2026
  type: z.literal("brain.hf.search.request"),
1925
2027
  query: z.string(),
@@ -1934,7 +2036,7 @@ export const BrainHfSearchResponseSchema = z.object({
1934
2036
  requestId: z.string(),
1935
2037
  }),
1936
2038
  });
1937
- // List the quantizations a repo offers `otto-brain add <repo> --list-quants`.
2039
+ // List the quantizations a repo offers - `otto-brain add <repo> --list-quants`.
1938
2040
  export const BrainHfQuantsRequestSchema = z.object({
1939
2041
  type: z.literal("brain.hf.quants.request"),
1940
2042
  repo: z.string(),
@@ -1948,7 +2050,7 @@ export const BrainHfQuantsResponseSchema = z.object({
1948
2050
  requestId: z.string(),
1949
2051
  }),
1950
2052
  });
1951
- // Download a chosen quant of an arbitrary HF repo starts a `pull` job.
2053
+ // Download a chosen quant of an arbitrary HF repo - starts a `pull` job.
1952
2054
  export const BrainModelsAddRequestSchema = z.object({
1953
2055
  type: z.literal("brain.models.add.request"),
1954
2056
  repo: z.string(),
@@ -1959,6 +2061,283 @@ export const BrainModelsAddResponseSchema = z.object({
1959
2061
  type: z.literal("brain.models.add.response"),
1960
2062
  payload: BrainJobResultSchema,
1961
2063
  });
2064
+ // --- Brain Console: the management API, proxied ---------------------------
2065
+ // Unlike the job RPCs above (which shell out to `otto-brain <verb> --json` and
2066
+ // are therefore local-only), these proxy the brain's own `/__host/*` HTTP API.
2067
+ // The daemon already resolves that endpoint by mode, so a local child and a
2068
+ // remote brain are reached by the same code with no branch on either side.
2069
+ // All gated by server_info.features.brainConsole.
2070
+ //
2071
+ // Two versions matter and they move independently: the daemon (does it know how
2072
+ // to proxy?) and the brain (does it serve it?). features.brainConsole answers
2073
+ // the first; `capabilities` on brain.host.status answers the second. A brain too
2074
+ // old for a capability is reported honestly rather than reimplemented.
2075
+ /**
2076
+ * A model's hosting profile. Passthrough because the brain persists more than it
2077
+ * exposes for editing (batchSize, extraArgs, the reasoning-budget message) and
2078
+ * those must survive a round trip untouched rather than being dropped here.
2079
+ */
2080
+ export const BrainProfileSchema = z
2081
+ .object({
2082
+ contextSize: z.number().default(0),
2083
+ cacheTypeK: z.string().default(""),
2084
+ cacheTypeV: z.string().default(""),
2085
+ flashAttention: z.boolean().default(true),
2086
+ gpuLayers: z.number().default(0),
2087
+ vision: z.boolean().default(false),
2088
+ reasoningBudget: z.number().default(0),
2089
+ parallelSlots: z.number().default(1),
2090
+ })
2091
+ .passthrough();
2092
+ /** One editable field, as the brain describes it, so the editor cannot drift. */
2093
+ export const BrainProfileFieldSchema = z
2094
+ .object({
2095
+ key: z.string(),
2096
+ label: z.string().default(""),
2097
+ kind: z.string().default("number"),
2098
+ step: z.number().nullable().optional(),
2099
+ min: z.number().nullable().optional(),
2100
+ max: z.number().nullable().optional(),
2101
+ options: z.array(z.union([z.string(), z.number()])).default([]),
2102
+ optionLabels: z.array(z.string()).default([]),
2103
+ available: z.boolean().default(true),
2104
+ unavailableReason: z.string().nullable().optional(),
2105
+ })
2106
+ .passthrough();
2107
+ /** A note on a field. `blocksStart` means this combination cannot load at all. */
2108
+ export const BrainProfileWarningSchema = z
2109
+ .object({
2110
+ field: z.string().nullable().default(null),
2111
+ severity: z.string().default("info"),
2112
+ message: z.string().default(""),
2113
+ blocksStart: z.boolean().default(false),
2114
+ })
2115
+ .passthrough();
2116
+ /**
2117
+ * Where the KV bytes/token figure came from. `inherited` means it was measured
2118
+ * on a relative with the same attention geometry and rescaled, which the UI must
2119
+ * never present as measured on this file.
2120
+ */
2121
+ export const BrainCalibrationInfoSchema = z
2122
+ .object({
2123
+ state: z.string().default("unknown"),
2124
+ kvBytesPerToken: z.number().nullable().default(null),
2125
+ measuredAt: z.string().nullable().default(null),
2126
+ measuredOn: z.string().nullable().default(null),
2127
+ })
2128
+ .passthrough();
2129
+ /** The VRAM breakdown for a profile. Raw bytes; the client formats at the edge. */
2130
+ export const BrainBudgetSchema = z
2131
+ .object({
2132
+ weightsBytes: z.number().default(0),
2133
+ mmprojBytes: z.number().default(0),
2134
+ kvBytes: z.number().default(0),
2135
+ overheadBytes: z.number().default(0),
2136
+ totalBytes: z.number().default(0),
2137
+ usableBytes: z.number().default(0),
2138
+ totalVramBytes: z.number().default(0),
2139
+ reserveBytes: z.number().default(0),
2140
+ kvBytesPerToken: z.number().default(0),
2141
+ source: z.string().default("unknown"),
2142
+ theoreticalKvBytesPerToken: z.number().nullable().default(null),
2143
+ fits: z.boolean().default(false),
2144
+ headroomBytes: z.number().default(0),
2145
+ utilization: z.number().default(0),
2146
+ })
2147
+ .passthrough();
2148
+ /** A model's benchmark standing, joined onto its inventory row. */
2149
+ export const BrainModelScoreSchema = z
2150
+ .object({
2151
+ id: z.string().nullable().default(null),
2152
+ displayName: z.string().default(""),
2153
+ overall: z.number().default(0),
2154
+ runs: z.number().default(0),
2155
+ std: z.number().default(0),
2156
+ grade: z.string().default(""),
2157
+ rank: z.number().nullable().optional(),
2158
+ })
2159
+ .passthrough();
2160
+ /**
2161
+ * One installed model with everything the Models tab shows, joined by the brain.
2162
+ * The client must not have to correlate the scan, the metadata and the rankings
2163
+ * itself: they key on different things and only the brain knows the file layout.
2164
+ */
2165
+ export const BrainInventoryModelSchema = z
2166
+ .object({
2167
+ id: z.string(),
2168
+ displayName: z.string().default(""),
2169
+ publisher: z.string().nullable().default(null),
2170
+ quant: z.string().nullable().default(null),
2171
+ sizeBytes: z.number().default(0),
2172
+ mmprojBytes: z.number().default(0),
2173
+ origin: z.string().nullable().default(null),
2174
+ arch: z.string().nullable().default(null),
2175
+ contextLength: z.number().nullable().default(null),
2176
+ blockCount: z.number().nullable().default(null),
2177
+ headCountKv: z.number().nullable().default(null),
2178
+ hasProjector: z.boolean().default(false),
2179
+ reasoning: z.boolean().default(false),
2180
+ mtp: z.boolean().default(false),
2181
+ distilled: z.boolean().default(false),
2182
+ useCases: z.array(z.string()).default([]),
2183
+ tier: z.string().nullable().default(null),
2184
+ profile: BrainProfileSchema.nullable().default(null),
2185
+ calibration: BrainCalibrationInfoSchema.nullable().default(null),
2186
+ budget: BrainBudgetSchema.nullable().default(null),
2187
+ maxContextThatFits: z.number().nullable().default(null),
2188
+ score: BrainModelScoreSchema.nullable().default(null),
2189
+ state: z.string().default("not-loaded"),
2190
+ warnings: z.array(BrainProfileWarningSchema).default([]),
2191
+ })
2192
+ .passthrough();
2193
+ export const BrainDiskUsageSchema = z
2194
+ .object({
2195
+ freeBytes: z.number().default(0),
2196
+ totalBytes: z.number().default(0),
2197
+ modelBytes: z.number().default(0),
2198
+ })
2199
+ .passthrough();
2200
+ // The joined inventory - GET /__host/models.
2201
+ export const BrainModelsInventoryRequestSchema = z.object({
2202
+ type: z.literal("brain.models.inventory.request"),
2203
+ requestId: z.string(),
2204
+ });
2205
+ export const BrainModelsInventoryResponseSchema = z.object({
2206
+ type: z.literal("brain.models.inventory.response"),
2207
+ payload: z.object({
2208
+ models: z.array(BrainInventoryModelSchema).default([]),
2209
+ disk: BrainDiskUsageSchema.nullable().default(null),
2210
+ error: z.string().nullable(),
2211
+ requestId: z.string(),
2212
+ }),
2213
+ });
2214
+ // Read a model's profile plus the descriptors an editor renders from.
2215
+ export const BrainModelProfileGetRequestSchema = z.object({
2216
+ type: z.literal("brain.model.profile.get.request"),
2217
+ modelId: z.string(),
2218
+ requestId: z.string(),
2219
+ });
2220
+ export const BrainModelProfileGetResponseSchema = z.object({
2221
+ type: z.literal("brain.model.profile.get.response"),
2222
+ payload: z.object({
2223
+ profile: BrainProfileSchema.nullable().default(null),
2224
+ fields: z.array(BrainProfileFieldSchema).default([]),
2225
+ warnings: z.array(BrainProfileWarningSchema).default([]),
2226
+ calibration: BrainCalibrationInfoSchema.nullable().default(null),
2227
+ error: z.string().nullable(),
2228
+ requestId: z.string(),
2229
+ }),
2230
+ });
2231
+ // Write the editable fields. `patch` carries only the eight the brain accepts;
2232
+ // anything else is ignored there rather than rejected, so an older client that
2233
+ // sends a field a newer brain dropped still succeeds.
2234
+ export const BrainModelProfileSetRequestSchema = z.object({
2235
+ type: z.literal("brain.model.profile.set.request"),
2236
+ modelId: z.string(),
2237
+ patch: z.record(z.string(), z.unknown()),
2238
+ requestId: z.string(),
2239
+ });
2240
+ export const BrainModelProfileSetResponseSchema = z.object({
2241
+ type: z.literal("brain.model.profile.set.response"),
2242
+ payload: z.object({
2243
+ profile: BrainProfileSchema.nullable().default(null),
2244
+ /** Human-readable notes about anything clamped or ignored. */
2245
+ adjustments: z.array(z.string()).default([]),
2246
+ warnings: z.array(BrainProfileWarningSchema).default([]),
2247
+ calibration: BrainCalibrationInfoSchema.nullable().default(null),
2248
+ budget: BrainBudgetSchema.nullable().default(null),
2249
+ maxContextThatFits: z.number().nullable().default(null),
2250
+ /** True when the edited model is the resident one, so a restart applies it. */
2251
+ requiresRestart: z.boolean().default(false),
2252
+ error: z.string().nullable(),
2253
+ requestId: z.string(),
2254
+ }),
2255
+ });
2256
+ // The budget for a hypothetical profile - GET /__host/model/budget. `overrides`
2257
+ // are string-encoded field values so the UI can preview a budget while a control
2258
+ // is mid-drag, without persisting a value the user is scrubbing past.
2259
+ export const BrainModelBudgetGetRequestSchema = z.object({
2260
+ type: z.literal("brain.model.budget.get.request"),
2261
+ modelId: z.string(),
2262
+ overrides: z.record(z.string(), z.string()).default({}),
2263
+ requestId: z.string(),
2264
+ });
2265
+ export const BrainModelBudgetGetResponseSchema = z.object({
2266
+ type: z.literal("brain.model.budget.get.response"),
2267
+ payload: z.object({
2268
+ profile: BrainProfileSchema.nullable().default(null),
2269
+ budget: BrainBudgetSchema.nullable().default(null),
2270
+ maxContextThatFits: z.number().nullable().default(null),
2271
+ gpu: z.record(z.string(), z.unknown()).nullable().default(null),
2272
+ warnings: z.array(BrainProfileWarningSchema).default([]),
2273
+ error: z.string().nullable(),
2274
+ requestId: z.string(),
2275
+ }),
2276
+ });
2277
+ // Load a model into the running brain. Distinct from brain.host.start, which
2278
+ // restarts the daemon's child and has no remote equivalent.
2279
+ export const BrainModelLoadRequestSchema = z.object({
2280
+ type: z.literal("brain.model.load.request"),
2281
+ modelId: z.string(),
2282
+ requestId: z.string(),
2283
+ });
2284
+ export const BrainModelLoadResponseSchema = z.object({
2285
+ type: z.literal("brain.model.load.response"),
2286
+ payload: z.object({
2287
+ status: BrainHostStatusSchema.nullable().default(null),
2288
+ /** The profile actually used: the brain clamps context to fit VRAM. */
2289
+ profile: BrainProfileSchema.nullable().default(null),
2290
+ error: z.string().nullable(),
2291
+ requestId: z.string(),
2292
+ }),
2293
+ });
2294
+ export const BrainModelUnloadRequestSchema = z.object({
2295
+ type: z.literal("brain.model.unload.request"),
2296
+ requestId: z.string(),
2297
+ });
2298
+ export const BrainModelUnloadResponseSchema = z.object({
2299
+ type: z.literal("brain.model.unload.response"),
2300
+ payload: z.object({
2301
+ status: BrainHostStatusSchema.nullable().default(null),
2302
+ error: z.string().nullable(),
2303
+ requestId: z.string(),
2304
+ }),
2305
+ });
2306
+ // Delete a model's files. The brain refuses while that model is loaded.
2307
+ export const BrainModelDeleteRequestSchema = z.object({
2308
+ type: z.literal("brain.model.delete.request"),
2309
+ modelId: z.string(),
2310
+ requestId: z.string(),
2311
+ });
2312
+ export const BrainModelDeleteResponseSchema = z.object({
2313
+ type: z.literal("brain.model.delete.response"),
2314
+ payload: z.object({
2315
+ deleted: z.array(z.string()).default([]),
2316
+ freedBytes: z.number().default(0),
2317
+ includesProjector: z.boolean().default(false),
2318
+ /** How many models remain after the re-scan. */
2319
+ remaining: z.number().default(0),
2320
+ error: z.string().nullable(),
2321
+ requestId: z.string(),
2322
+ }),
2323
+ });
2324
+ // Tail the brain's llama-server log.
2325
+ export const BrainLogsTailRequestSchema = z.object({
2326
+ type: z.literal("brain.logs.tail.request"),
2327
+ limit: z.number().nullable().default(null),
2328
+ requestId: z.string(),
2329
+ });
2330
+ export const BrainLogsTailResponseSchema = z.object({
2331
+ type: z.literal("brain.logs.tail.response"),
2332
+ payload: z.object({
2333
+ lines: z.array(z.string()).default([]),
2334
+ total: z.number().default(0),
2335
+ state: z.string().nullable().default(null),
2336
+ command: z.string().nullable().default(null),
2337
+ error: z.string().nullable(),
2338
+ requestId: z.string(),
2339
+ }),
2340
+ });
1962
2341
  export const ProjectRenameRequestSchema = z.object({
1963
2342
  type: z.literal("project.rename.request"),
1964
2343
  projectId: z.string(),
@@ -2002,6 +2381,22 @@ export const WorkspaceTitleSetRequestSchema = z.object({
2002
2381
  title: z.string().nullable(),
2003
2382
  requestId: z.string(),
2004
2383
  });
2384
+ export const WorkspacePinSetRequestSchema = z.object({
2385
+ type: z.literal("workspace.pin.set.request"),
2386
+ workspaceId: z.string(),
2387
+ pinned: z.boolean(),
2388
+ requestId: z.string(),
2389
+ });
2390
+ export const WorkspaceRecoveryInspectRequestSchema = z.object({
2391
+ type: z.literal("workspace.recovery.inspect.request"),
2392
+ workspaceId: z.string(),
2393
+ requestId: z.string(),
2394
+ });
2395
+ export const WorkspaceRecoveryRestoreRequestSchema = z.object({
2396
+ type: z.literal("workspace.recovery.restore.request"),
2397
+ workspaceId: z.string(),
2398
+ requestId: z.string(),
2399
+ });
2005
2400
  export const SetVoiceModeMessageSchema = z.object({
2006
2401
  type: z.literal("set_voice_mode"),
2007
2402
  enabled: z.boolean(),
@@ -2018,6 +2413,18 @@ export const GitHubPrAttachmentSchema = z.object({
2018
2413
  baseRefName: z.string().nullable().optional(),
2019
2414
  headRefName: z.string().nullable().optional(),
2020
2415
  });
2416
+ export const ForgeChangeRequestAttachmentSchema = z.object({
2417
+ type: z.literal("forge_change_request"),
2418
+ mimeType: z.literal("application/otto-forge-change-request"),
2419
+ forge: z.string().optional().default("github"),
2420
+ number: z.number().int().positive(),
2421
+ title: z.string(),
2422
+ url: z.string(),
2423
+ body: z.string().nullable().optional(),
2424
+ projectPath: z.string().optional(),
2425
+ baseRefName: z.string().nullable().optional(),
2426
+ headRefName: z.string().nullable().optional(),
2427
+ });
2021
2428
  export const GitHubIssueAttachmentSchema = z.object({
2022
2429
  type: z.literal("github_issue"),
2023
2430
  mimeType: z.literal("application/github-issue"),
@@ -2026,10 +2433,13 @@ export const GitHubIssueAttachmentSchema = z.object({
2026
2433
  url: z.string(),
2027
2434
  body: z.string().nullable().optional(),
2028
2435
  });
2029
- // Provider-neutral successors to github_pr/github_issue. New clients send
2030
- // these when server_info features.gitHostingProviders is set; the github_*
2031
- // kinds remain accepted forever (protocol contract) and are still what a new
2032
- // client sends to an old daemon for GitHub projects.
2436
+ // COMPAT(hostingAttachments): added in v0.7.6, remove after 2027-02-01.
2437
+ // These were the provider-neutral successors to github_pr/github_issue. The
2438
+ // forge merge replaced them with forge_change_request/forge_issue, so no
2439
+ // current client sends them - they stay accepted (protocol contract) purely so
2440
+ // a client from before that merge can still attach a PR or an issue. The
2441
+ // daemon renders them at
2442
+ // server/src/server/agent/prompt-attachments.ts; retire both halves together.
2033
2443
  export const HostingPrAttachmentSchema = z.object({
2034
2444
  type: z.literal("hosting_pr"),
2035
2445
  mimeType: z.literal("application/otto-hosting-pr"),
@@ -2050,6 +2460,16 @@ export const HostingIssueAttachmentSchema = z.object({
2050
2460
  url: z.string(),
2051
2461
  body: z.string().nullable().optional(),
2052
2462
  });
2463
+ export const ForgeIssueAttachmentSchema = z.object({
2464
+ type: z.literal("forge_issue"),
2465
+ mimeType: z.literal("application/otto-forge-issue"),
2466
+ forge: z.string().optional().default("github"),
2467
+ number: z.number().int().positive(),
2468
+ title: z.string(),
2469
+ url: z.string(),
2470
+ body: z.string().nullable().optional(),
2471
+ projectPath: z.string().optional(),
2472
+ });
2053
2473
  export const TextAttachmentSchema = z
2054
2474
  .object({
2055
2475
  type: z.literal("text"),
@@ -2096,6 +2516,8 @@ export const UploadedFileAttachmentSchema = z.object({
2096
2516
  path: z.string(),
2097
2517
  });
2098
2518
  export const AgentAttachmentSchema = z.discriminatedUnion("type", [
2519
+ ForgeChangeRequestAttachmentSchema,
2520
+ ForgeIssueAttachmentSchema,
2099
2521
  GitHubPrAttachmentSchema,
2100
2522
  GitHubIssueAttachmentSchema,
2101
2523
  HostingPrAttachmentSchema,
@@ -2118,6 +2540,12 @@ function normalizeAgentAttachments(input) {
2118
2540
  return normalized;
2119
2541
  }
2120
2542
  const AgentAttachmentsSchema = z.unknown().transform(normalizeAgentAttachments).optional();
2543
+ export const ChangeRequestCheckoutSourceSchema = z.object({
2544
+ kind: z.literal("change_request"),
2545
+ forge: z.string().optional(),
2546
+ number: z.number().int().positive(),
2547
+ projectPath: z.string().optional(),
2548
+ });
2121
2549
  const ImageAttachmentSchema = z.object({
2122
2550
  data: z.string(), // base64 encoded image
2123
2551
  mimeType: z.string(), // e.g., "image/jpeg", "image/png"
@@ -2192,6 +2620,10 @@ export const FetchWorkspacesRequestMessageSchema = z.object({
2192
2620
  })
2193
2621
  .optional(),
2194
2622
  });
2623
+ export const ProjectListRequestMessageSchema = z.object({
2624
+ type: z.literal("project.list.request"),
2625
+ requestId: z.string(),
2626
+ });
2195
2627
  export const FetchAgentHistoryRequestMessageSchema = z.object({
2196
2628
  type: z.literal("fetch_agent_history_request"),
2197
2629
  requestId: z.string(),
@@ -2289,6 +2721,21 @@ export const DaemonGetPairingOfferRequestSchema = z.object({
2289
2721
  type: z.literal("daemon.get_pairing_offer.request"),
2290
2722
  requestId: z.string(),
2291
2723
  });
2724
+ export const HubManagementDaemonConnectRequestSchema = z.object({
2725
+ type: z.literal("hub.management.daemon.connect.request"),
2726
+ requestId: z.string(),
2727
+ hubUrl: z.string(),
2728
+ token: z.string(),
2729
+ });
2730
+ export const HubManagementDaemonGetStatusRequestSchema = z.object({
2731
+ type: z.literal("hub.management.daemon.get_status.request"),
2732
+ requestId: z.string(),
2733
+ });
2734
+ export const HubManagementDaemonDisconnectRequestSchema = z.object({
2735
+ type: z.literal("hub.management.daemon.disconnect.request"),
2736
+ requestId: z.string(),
2737
+ force: z.boolean().optional(),
2738
+ });
2292
2739
  export const DiagnosticsRequestSchema = z.object({
2293
2740
  type: z.literal("diagnostics.request"),
2294
2741
  requestId: z.string(),
@@ -2302,11 +2749,11 @@ export const SetDaemonConfigRequestMessageSchema = z.object({
2302
2749
  requestId: z.string(),
2303
2750
  config: MutableDaemonConfigPatchSchema,
2304
2751
  });
2305
- // Connectors MCP servers surfaced as named, toggle-able integrations. The
2752
+ // Connectors - MCP servers surfaced as named, toggle-able integrations. The
2306
2753
  // registry itself (add/remove/enable/disable a connector or an individual tool)
2307
2754
  // lives in daemon config and is edited via set_daemon_config's `connectors`
2308
2755
  // patch. The one thing config can't answer is what tools a connector actually
2309
- // exposes, which needs a live connect + listTools that is this RPC. Gated by
2756
+ // exposes, which needs a live connect + listTools - that is this RPC. Gated by
2310
2757
  // features.connectors.
2311
2758
  export const ConnectorsListToolsRequestSchema = z.object({
2312
2759
  type: z.literal("connectors.list_tools.request"),
@@ -2329,6 +2776,61 @@ export const ConnectorsListToolsResponseSchema = z.object({
2329
2776
  requestId: z.string(),
2330
2777
  }),
2331
2778
  });
2779
+ // Connector OAuth - the "sign in" path for connectors whose MCP server
2780
+ // authenticates by login rather than by a pasted token. Three steps, because a
2781
+ // login is not a request/response: the client asks to authorize, the daemon
2782
+ // answers with a URL to open (or "already authorized"), and the actual result
2783
+ // arrives later as a pushed status once the user finishes in their browser.
2784
+ // Gated by features.connectorOauth.
2785
+ export const ConnectorsOauthAuthorizeRequestSchema = z.object({
2786
+ type: z.literal("connectors.oauth.authorize.request"),
2787
+ requestId: z.string(),
2788
+ connectorId: z.string(),
2789
+ /** Scopes to request, when the catalog entry names them. */
2790
+ scope: z.string().optional(),
2791
+ });
2792
+ export const ConnectorsOauthAuthorizeResponseSchema = z.object({
2793
+ type: z.literal("connectors.oauth.authorize.response"),
2794
+ payload: z.object({
2795
+ connectorId: z.string(),
2796
+ /**
2797
+ * Present when the user must sign in: the client opens this URL. Absent when
2798
+ * the daemon already held a usable authorization, in which case status is
2799
+ * "authorized" and there is nothing to open.
2800
+ */
2801
+ authorizationUrl: z.string().nullable().default(null),
2802
+ status: z.enum(["redirect", "authorized", "error"]).default("error"),
2803
+ error: z.string().nullable().default(null),
2804
+ requestId: z.string(),
2805
+ }),
2806
+ });
2807
+ export const ConnectorsOauthDisconnectRequestSchema = z.object({
2808
+ type: z.literal("connectors.oauth.disconnect.request"),
2809
+ requestId: z.string(),
2810
+ connectorId: z.string(),
2811
+ });
2812
+ export const ConnectorsOauthDisconnectResponseSchema = z.object({
2813
+ type: z.literal("connectors.oauth.disconnect.response"),
2814
+ payload: z.object({
2815
+ connectorId: z.string(),
2816
+ requestId: z.string(),
2817
+ }),
2818
+ });
2819
+ /**
2820
+ * Pushed when an in-flight authorization settles. Not correlated to a requestId:
2821
+ * the browser round-trip outlives the request that started it, and more than one
2822
+ * client may be watching the same connector.
2823
+ */
2824
+ export const ConnectorsOauthStatusMessageSchema = z.object({
2825
+ type: z.literal("connectors.oauth.status"),
2826
+ payload: z.object({
2827
+ connectorId: z.string(),
2828
+ status: z.enum(["connected", "failed"]),
2829
+ /** Account label to show once connected, when the server reveals one. */
2830
+ account: z.string().nullable().default(null),
2831
+ error: z.string().nullable().default(null),
2832
+ }),
2833
+ });
2332
2834
  export const SpeechSettingsGetOptionsRequestSchema = z.object({
2333
2835
  type: z.literal("speech.settings.get_options.request"),
2334
2836
  requestId: z.string(),
@@ -2352,8 +2854,8 @@ export const SpeechTtsPreviewRequestSchema = z.object({
2352
2854
  .optional(),
2353
2855
  });
2354
2856
  // Read a full assistant message aloud on demand (the per-message playback
2355
- // button). Unlike the preview RPC which truncates to a short sample and
2356
- // returns one buffered clip this synthesizes the ENTIRE text and streams it
2857
+ // button). Unlike the preview RPC - which truncates to a short sample and
2858
+ // returns one buffered clip - this synthesizes the ENTIRE text and streams it
2357
2859
  // back as `audio_output` chunks (isVoiceMode: false), one group per sentence, so
2358
2860
  // playback starts after the first sentence instead of the whole message.
2359
2861
  // `voice` (optional) is the speaking agent's personality voice, resolved on the
@@ -2382,8 +2884,8 @@ export const SpeechTtsSpeakCancelRequestSchema = z.object({
2382
2884
  });
2383
2885
  // COMPAT(visualizerVoiceCues): added in v0.6.3; gate lives in
2384
2886
  // features.visualizerVoiceCues. Author short spoken "cue" lines for a
2385
- // personality a handful of variations each for three Visualizer moments
2386
- // (join / thinking / done) via the Writer mini-task chain, flavored by the
2887
+ // personality - a handful of variations each for three Visualizer moments
2888
+ // (join / thinking / done) - via the Writer mini-task chain, flavored by the
2387
2889
  // persona's `name` + `prompt`. The persona is passed inline (not a stored id)
2388
2890
  // so the personality editor can generate for an unsaved draft too; the result
2389
2891
  // is stored on the personality (`voiceCues`) and edited there, so this is an
@@ -2477,6 +2979,9 @@ const GitSetupOptionsSchema = z.object({
2477
2979
  worktreeSlug: z.string().optional(),
2478
2980
  refName: z.string().min(1).optional(),
2479
2981
  action: z.enum(["branch-off", "checkout"]).optional(),
2982
+ checkoutSource: ChangeRequestCheckoutSourceSchema.optional(),
2983
+ // COMPAT(githubPrNumber): added in v0.1.106, remove after 2026-12-28 once
2984
+ // clients send checkoutSource.
2480
2985
  githubPrNumber: z.number().int().positive().optional(),
2481
2986
  });
2482
2987
  export const CreateAgentWorktreeTargetSchema = z.discriminatedUnion("mode", [
@@ -2499,12 +3004,15 @@ export const CreateAgentRequestMessageSchema = z.object({
2499
3004
  config: AgentSessionConfigSchema,
2500
3005
  // Optional personality id. When present the daemon resolves the personality
2501
3006
  // against this cwd's provider snapshot and snapshots its identity (spinner,
2502
- // voice, prompt) onto the agent the brain (provider/model/mode/effort) still
3007
+ // voice, prompt) onto the agent - the brain (provider/model/mode/effort) still
2503
3008
  // comes from `config`, so hand-deviations in the picker keep the identity.
2504
3009
  // COMPAT(agentPersonalities): added in v0.5.0; gate lives in features.agentPersonalities.
2505
3010
  personality: z.string().optional(),
2506
3011
  env: z.record(z.string(), z.string()).optional(),
2507
3012
  workspaceId: z.string().optional(),
3013
+ // Optional caller context lets managed CLI invocations use the same daemon-owned
3014
+ // workspace and parentage policy as agent-scoped MCP creation.
3015
+ callerAgentId: z.string().optional(),
2508
3016
  worktreeName: z.string().optional(),
2509
3017
  initialPrompt: z.string().optional(),
2510
3018
  clientMessageId: z.string().optional(),
@@ -2553,7 +3061,7 @@ export const ProviderUsageListRequestMessageSchema = z.object({
2553
3061
  type: z.literal("provider.usage.list.request"),
2554
3062
  requestId: z.string(),
2555
3063
  });
2556
- // Daemon-wide "fun stats" counters see docs/data-model.md ActivityStatsStore.
3064
+ // Daemon-wide "fun stats" counters - see docs/data-model.md ActivityStatsStore.
2557
3065
  // Every field defaults to 0 so old and new daemons/clients stay compatible as
2558
3066
  // counters are added later.
2559
3067
  export const ActivityCountersSchema = z.object({
@@ -2572,7 +3080,7 @@ export const ActivityCountersSchema = z.object({
2572
3080
  // Usage & cost accounting (WP-G). Additive/defaulted like every counter above,
2573
3081
  // so old daemons emit 0 and old clients drop the unknown leaves. "In"/"Out"
2574
3082
  // are token totals; *CostMicroUsd are integer micro-USD (usd*1e6) to stay
2575
- // summable populated only for turns reporting a real provider cost (Claude).
3083
+ // summable - populated only for turns reporting a real provider cost (Claude).
2576
3084
  // The client detects whether the daemon actually populates these via
2577
3085
  // features.usageCostCategories (see below).
2578
3086
  costMicroUsd: z.number().default(0),
@@ -2605,10 +3113,10 @@ export const StatsActivityGetResponseMessageSchema = z.object({
2605
3113
  allTime: ActivityCountersSchema,
2606
3114
  }),
2607
3115
  });
2608
- // Daemon-wide "activity counters moved" ping broadcast to every client,
3116
+ // Daemon-wide "activity counters moved" ping - broadcast to every client,
2609
3117
  // coalesced at the daemon (at most once every few seconds) so bursts of
2610
3118
  // increments don't get chatty. Carries no payload: clients re-fetch the
2611
- // rollups via stats.activity.get. Purely additive old clients drop the
3119
+ // rollups via stats.activity.get. Purely additive - old clients drop the
2612
3120
  // unknown type with a warning, and against old daemons (which never send it)
2613
3121
  // the stats screen degrades to today's focus/manual refresh. Rides the
2614
3122
  // existing activityStats capability; no new feature flag needed because no
@@ -2616,12 +3124,12 @@ export const StatsActivityGetResponseMessageSchema = z.object({
2616
3124
  export const ActivityStatsChangedSchema = z.object({
2617
3125
  type: z.literal("activity_stats_changed"),
2618
3126
  });
2619
- // One itemized row of the usage ledger a single token/cost-bearing activity
3127
+ // One itemized row of the usage ledger - a single token/cost-bearing activity
2620
3128
  // (a chat turn, a sub-agent turn, or a background generation). The aggregate
2621
3129
  // ActivityCounters above are the rollup of this same event stream; the ledger is
2622
3130
  // the scrollable detail behind the tiles (usage-ledger project). `kind` and
2623
3131
  // `provider` are plain strings (not enums) so an OLD client still parses a NEW
2624
- // daemon that emits a kind it hasn't heard of it renders it generically rather
3132
+ // daemon that emits a kind it hasn't heard of - it renders it generically rather
2625
3133
  // than failing the whole message. All token/cost leaves default to 0.
2626
3134
  export const UsageEventSchema = z.object({
2627
3135
  /** Stable unique id for the row (daemon-generated). */
@@ -2657,20 +3165,20 @@ export const UsageEventSchema = z.object({
2657
3165
  /**
2658
3166
  * How many model round-trips this row aggregates. A chat row is one query, but
2659
3167
  * a sub-agent row covers a whole delegated task that internally ran many
2660
- * rounds and each round re-reads the growing context, so `cachedTokensIn` is
3168
+ * rounds - and each round re-reads the growing context, so `cachedTokensIn` is
2661
3169
  * cumulative cache-READS, not a cache size. Surfacing the count is what makes a
2662
3170
  * large cached figure legible instead of looking like a bug. Absent when the
2663
3171
  * provider doesn't report it.
2664
3172
  */
2665
3173
  rounds: z.number().optional(),
2666
3174
  /**
2667
- * Sub-agent rows only the spawn-tree identity that lets the Log group rows
3175
+ * Sub-agent rows only - the spawn-tree identity that lets the Log group rows
2668
3176
  * the way a human reads the run (chat turn → its sub-agents → their
2669
3177
  * sub-agents) instead of by settle time, which async sub-agents crossing turn
2670
3178
  * boundaries makes wrong. `startedAt` is when the sub-agent was first
2671
3179
  * observed (epoch ms; a row belongs to the turn that spawned it, not the turn
2672
3180
  * it happened to settle in), `subagentKey` is its stable observed key, and
2673
- * `parentSubagentKey` is the spawning sub-agent's key absent for depth-1
3181
+ * `parentSubagentKey` is the spawning sub-agent's key - absent for depth-1
2674
3182
  * sub-agents spawned by the chat itself.
2675
3183
  */
2676
3184
  startedAt: z.number().optional(),
@@ -2696,7 +3204,7 @@ export const UsageLogGetResponseMessageSchema = z.object({
2696
3204
  }),
2697
3205
  });
2698
3206
  // Wipe every daemon-wide usage counter AND the itemized usage ledger back to
2699
- // zero the "Reset" action on the Metrics screen. One RPC clears both sinks
3207
+ // zero - the "Reset" action on the Metrics screen. One RPC clears both sinks
2700
3208
  // (the day-bucketed ActivityStatsStore and the UsageLogStore) so the tiles and
2701
3209
  // the Log tab start fresh together. Gated behind features.statsReset so an old
2702
3210
  // daemon (no handler) never receives a request the client thinks it can send.
@@ -2776,9 +3284,29 @@ export const FetchAgentTimelineRequestMessageSchema = z.object({
2776
3284
  // Default should be projected for app timeline loading.
2777
3285
  projection: z.enum(["projected", "canonical"]).optional(),
2778
3286
  });
3287
+ export const ProviderSubagentListRequestMessageSchema = z.object({
3288
+ type: z.literal("agent.provider_subagents.list.request"),
3289
+ parentAgentId: z.string(),
3290
+ requestId: z.string(),
3291
+ });
3292
+ export const ProviderSubagentTimelineRequestMessageSchema = z.object({
3293
+ type: z.literal("agent.provider_subagents.timeline.get.request"),
3294
+ parentAgentId: z.string(),
3295
+ subagentId: z.string(),
3296
+ requestId: z.string(),
3297
+ direction: z.enum(["tail", "before", "after"]).optional(),
3298
+ cursor: AgentTimelineCursorSchema.optional(),
3299
+ limit: z.number().int().nonnegative().optional(),
3300
+ });
3301
+ export const SetAgentTimelineSubscriptionRequestMessageSchema = z.object({
3302
+ type: z.literal("agent.timeline.set_subscription.request"),
3303
+ agentIds: z.array(z.string()),
3304
+ requestId: z.string(),
3305
+ });
2779
3306
  export const AgentForkContextRequestMessageSchema = z.object({
2780
3307
  type: z.literal("agent.fork_context.request"),
2781
3308
  agentId: z.string(),
3309
+ boundaryCursor: AgentTimelineCursorSchema.optional(),
2782
3310
  boundaryMessageId: z.string().optional(),
2783
3311
  requestId: z.string(),
2784
3312
  });
@@ -2874,7 +3402,7 @@ export const AgentSubagentStopResponseMessageSchema = z.object({
2874
3402
  payload: AgentActionResponsePayloadSchema,
2875
3403
  });
2876
3404
  // A background shell task launched by a provider's own Bash tool (Claude:
2877
- // run_in_background). Not an agent, not a subagent a plain shell process
3405
+ // run_in_background). Not an agent, not a subagent - a plain shell process
2878
3406
  // the daemon tracks for the parent agent's Background Tasks track.
2879
3407
  // COMPAT(backgroundShellTasks): added in v0.5.3, drop the gate when daemon floor >= v0.5.3.
2880
3408
  export const BackgroundShellTaskInfoSchema = z.object({
@@ -2890,7 +3418,7 @@ export const BackgroundShellTaskInfoSchema = z.object({
2890
3418
  archivedAt: z.string().optional(),
2891
3419
  });
2892
3420
  // Pushed with the full current set of background shell tasks for a parent
2893
- // agent whenever any of them changes (start/progress/settle/clear) same
3421
+ // agent whenever any of them changes (start/progress/settle/clear) - same
2894
3422
  // full-list reconciliation shape as TerminalsChangedSchema.
2895
3423
  export const BackgroundShellTasksChangedSchema = z.object({
2896
3424
  type: z.literal("background_shell_tasks_changed"),
@@ -2926,7 +3454,7 @@ export const AgentBackgroundTaskClearResponseMessageSchema = z.object({
2926
3454
  // A suggested task an agent surfaced via the `spawn_task` tool (Claude Desktop
2927
3455
  // parity). Renders as a chip in the parent agent's session; the user starts it
2928
3456
  // (new worktree / local / this session) or dismisses it. The `prompt` is
2929
- // deliberately NOT part of this wire shape it stays server-side and is only
3457
+ // deliberately NOT part of this wire shape - it stays server-side and is only
2930
3458
  // used when the task is started ("not shown directly" in Claude Desktop).
2931
3459
  // COMPAT(suggestedTasks): added in v0.5.6, drop the gate when daemon floor >= v0.5.6.
2932
3460
  export const SuggestedTaskStateSchema = z.enum(["pending", "started", "dismissed"]);
@@ -2941,7 +3469,7 @@ export const SuggestedTaskInfoSchema = z.object({
2941
3469
  updatedAt: z.string(),
2942
3470
  });
2943
3471
  // Pushed with the full current set of pending suggested tasks for a parent
2944
- // agent whenever any of them changes (spawn/start/dismiss) same full-list
3472
+ // agent whenever any of them changes (spawn/start/dismiss) - same full-list
2945
3473
  // reconciliation shape as BackgroundShellTasksChangedSchema.
2946
3474
  export const SuggestedTasksChangedSchema = z.object({
2947
3475
  type: z.literal("suggested_tasks_changed"),
@@ -2950,7 +3478,7 @@ export const SuggestedTasksChangedSchema = z.object({
2950
3478
  tasks: z.array(SuggestedTaskInfoSchema),
2951
3479
  }),
2952
3480
  });
2953
- // Context Management the daemon's accounting of everything a provider sends
3481
+ // Context Management - the daemon's accounting of everything a provider sends
2954
3482
  // before the user types (see docs/context-management.md).
2955
3483
  //
2956
3484
  // Two distinctions carry the whole feature and must not be collapsed on the
@@ -3011,7 +3539,7 @@ export const ContextFindingSchema = z.object({
3011
3539
  range: ContextRangeSchema.optional(),
3012
3540
  relatedNodeIds: z.array(z.string()).optional(),
3013
3541
  // The node this finding is about. Redundant while the finding sits on its
3014
- // node, load-bearing once the report flattens them all into one list that
3542
+ // node, load-bearing once the report flattens them all into one list - that
3015
3543
  // list is the "Issues" tab, and without this a row cannot say where it came
3016
3544
  // from or take you there.
3017
3545
  nodeId: z.string().optional(),
@@ -3022,9 +3550,9 @@ export const ContextFindingSchema = z.object({
3022
3550
  // rather than dropping a cursor at the top of it.
3023
3551
  lineEnd: z.number().optional(),
3024
3552
  // True for kinds a mechanical delete can resolve on its own (dead links, a
3025
- // duplicate block) false/absent for kinds that need judgment (which side
3553
+ // duplicate block) - false/absent for kinds that need judgment (which side
3026
3554
  // of an import cycle to cut, how to split an oversized entry). Computed
3027
- // server-side, once, in `locateFinding` the only place that knows the kind
3555
+ // server-side, once, in `locateFinding` - the only place that knows the kind
3028
3556
  // vocabulary, so the fix-all button never has to guess.
3029
3557
  fixable: z.boolean().optional(),
3030
3558
  // The exact text at `range` when the file was scanned. `context.findings.fix`
@@ -3048,7 +3576,7 @@ export const ContextNodeSchema = z.object({
3048
3576
  });
3049
3577
  export const ContextEdgeSchema = z.object({
3050
3578
  fromNodeId: z.string(),
3051
- // Null when the target could not be resolved pairs with a dead_* finding.
3579
+ // Null when the target could not be resolved - pairs with a dead_* finding.
3052
3580
  toNodeId: z.string().nullable(),
3053
3581
  kind: z.enum(["import", "reference"]),
3054
3582
  rawTarget: z.string(),
@@ -3069,7 +3597,7 @@ export const ContextCategoryTotalSchema = z.object({
3069
3597
  export const ContextReportSchema = z.object({
3070
3598
  workspaceId: z.string(),
3071
3599
  provider: z.string(),
3072
- // The window the report was evaluated against from the active model, or
3600
+ // The window the report was evaluated against - from the active model, or
3073
3601
  // the client's what-if picker. Severity is meaningless without it.
3074
3602
  windowTokens: z.number(),
3075
3603
  scannedAt: z.string(),
@@ -3126,7 +3654,7 @@ export const ContextReportGetResponseMessageSchema = z.object({
3126
3654
  }),
3127
3655
  });
3128
3656
  // One readable block of the assembled prompt. `text` is absent exactly when
3129
- // `visibility` is "not_visible" the provider composes that part internally and
3657
+ // `visibility` is "not_visible" - the provider composes that part internally and
3130
3658
  // Otto has nothing to show, which the section states rather than hides.
3131
3659
  export const ContextPromptSectionSchema = z.object({
3132
3660
  category: ContextCategorySchema,
@@ -3149,8 +3677,8 @@ export const ContextPromptPreviewGetRequestMessageSchema = z.object({
3149
3677
  provider: z.string().optional(),
3150
3678
  windowTokens: z.number().optional(),
3151
3679
  personalityId: z.string().optional(),
3152
- // Assemble only this category. The tab reads one section at a time the user
3153
- // clicked a row in the tree and assembling the rest would re-read every
3680
+ // Assemble only this category. The tab reads one section at a time - the user
3681
+ // clicked a row in the tree - and assembling the rest would re-read every
3154
3682
  // context file on disk to build text nobody asked to see. Omitted means all,
3155
3683
  // which is what an older client sends.
3156
3684
  category: ContextCategorySchema.optional(),
@@ -3168,7 +3696,7 @@ export const ContextEdgeConvertRequestMessageSchema = z.object({
3168
3696
  type: z.literal("context.edge.convert.request"),
3169
3697
  requestId: z.string(),
3170
3698
  workspaceId: z.string(),
3171
- // The parent file holding the reference its `ContextNode.path`, not its
3699
+ // The parent file holding the reference - its `ContextNode.path`, not its
3172
3700
  // id: ids are case-folded on Windows and are not safe to write through.
3173
3701
  filePath: z.string(),
3174
3702
  rawTarget: z.string(),
@@ -3183,7 +3711,7 @@ export const ContextEdgeConvertResponseMessageSchema = z.object({
3183
3711
  error: z.string().optional(),
3184
3712
  }),
3185
3713
  });
3186
- // Deletes every mechanically-fixable finding's range in one pass the
3714
+ // Deletes every mechanically-fixable finding's range in one pass - the
3187
3715
  // "Fix all" button in the Issues tab. Each item names the file, the range the
3188
3716
  // scan flagged, and the snippet expected there; a file that changed since the
3189
3717
  // scan is skipped rather than corrupted (charter §7.5).
@@ -3207,7 +3735,7 @@ export const ContextFindingsFixResponseMessageSchema = z.object({
3207
3735
  }),
3208
3736
  });
3209
3737
  // ---------------------------------------------------------------------------
3210
- // Personality memory the lessons a named personality accrues across sessions.
3738
+ // Personality memory - the lessons a named personality accrues across sessions.
3211
3739
  // See docs/agent-personalities.md § Memory.
3212
3740
  // ---------------------------------------------------------------------------
3213
3741
  // Plain strings on the wire, like personality roles and effort levels, so the
@@ -3250,7 +3778,7 @@ export const PersonalityMemoryListResponseMessageSchema = z.object({
3250
3778
  personalityName: z.string(),
3251
3779
  /** Whether this personality is accruing (the `memoryEnabled` switch). */
3252
3780
  enabled: z.boolean(),
3253
- /** Every stored entry, including other projects' the UI shows them all. */
3781
+ /** Every stored entry, including other projects' - the UI shows them all. */
3254
3782
  entries: z.array(PersonalityMemoryEntrySchema),
3255
3783
  // The EXACT text the daemon would inject for `projectRoot`, not a
3256
3784
  // reconstruction. Memory is only trustworthy if it is inspectable, and the
@@ -3318,7 +3846,7 @@ export const PersonalityMemoryTransferResponseMessageSchema = z.object({
3318
3846
  }),
3319
3847
  });
3320
3848
  // Per-personality lesson counts. Its own RPC over its own file, mirroring
3321
- // agentPersonalities.get_stats counts must not ride the daemon-config
3849
+ // agentPersonalities.get_stats - counts must not ride the daemon-config
3322
3850
  // broadcast, or every recorded lesson would fan a config change to every client.
3323
3851
  export const PersonalityMemoryStatsRequestMessageSchema = z.object({
3324
3852
  type: z.literal("personality.memory.stats.request"),
@@ -3342,14 +3870,14 @@ const SuggestedTaskActionResponsePayloadSchema = z.object({
3342
3870
  failed: z.number(),
3343
3871
  error: z.string().nullable(),
3344
3872
  });
3345
- // Start one or more suggested tasks, applying the SAME mode to each no
3873
+ // Start one or more suggested tasks, applying the SAME mode to each - no
3346
3874
  // combining. Four modes, only `subagent` links the new agent to the parent:
3347
3875
  // - `new_chat`: a fresh independent agent in its own tab, same repo/cwd, NO
3348
- // parent link survives the parent's cancel/archive.
3876
+ // parent link - survives the parent's cancel/archive.
3349
3877
  // - `subagent`: a bound child agent that shows in the parent's Subagents
3350
3878
  // track and archive-cascades with it.
3351
3879
  // - `worktree`: an independent agent on a new git worktree (auto branch-off),
3352
- // isolated workspace also unlinked from the parent.
3880
+ // isolated workspace - also unlinked from the parent.
3353
3881
  // - `in_session`: steers the parent agent with the task prompt (no new agent).
3354
3882
  // The daemon resolves the parent agent's brain (provider/model/personality) so a
3355
3883
  // started task continues the suggesting agent.
@@ -3382,8 +3910,8 @@ export const TasksSuggestedDismissResponseMessageSchema = z.object({
3382
3910
  });
3383
3911
  // Switch a running agent to an Agent Personality (or clear with null). The
3384
3912
  // daemon re-resolves the id against the roster + the agent's cwd provider
3385
- // snapshot and applies the full personality live system prompt, identity
3386
- // (name/spinner), and brain (model/mode/effort) restarting the provider query
3913
+ // snapshot and applies the full personality live - system prompt, identity
3914
+ // (name/spinner), and brain (model/mode/effort) - restarting the provider query
3387
3915
  // so the new prompt takes effect on the next turn. Providers that cannot apply
3388
3916
  // a prompt mid-session reject. COMPAT(setAgentPersonality): added in v0.5.0;
3389
3917
  // gate lives in features.setAgentPersonality.
@@ -3498,6 +4026,48 @@ export const AgentWorkspaceTransferResponseMessageSchema = z.object({
3498
4026
  type: z.literal("agent.workspace.transfer.response"),
3499
4027
  payload: AgentWorkspaceTransferResponsePayloadSchema,
3500
4028
  });
4029
+ export const WorkspacePinSetResponsePayloadSchema = z.object({
4030
+ requestId: z.string(),
4031
+ workspaceId: z.string(),
4032
+ accepted: z.boolean(),
4033
+ pinnedAt: z.string().nullable(),
4034
+ error: z.string().nullable(),
4035
+ });
4036
+ export const WorkspacePinSetResponseSchema = z.object({
4037
+ type: z.literal("workspace.pin.set.response"),
4038
+ payload: WorkspacePinSetResponsePayloadSchema,
4039
+ });
4040
+ export const WorkspaceRecoveryStateSchema = z.discriminatedUnion("kind", [
4041
+ z.object({
4042
+ kind: z.literal("recoverable"),
4043
+ workspaceId: z.string(),
4044
+ workspaceName: z.string(),
4045
+ action: z.string(),
4046
+ branch: z.string().nullable(),
4047
+ }),
4048
+ z.object({
4049
+ kind: z.literal("unavailable"),
4050
+ workspaceId: z.string(),
4051
+ reason: z.string(),
4052
+ message: z.string(),
4053
+ }),
4054
+ ]);
4055
+ export const WorkspaceRecoveryInspectResponseSchema = z.object({
4056
+ type: z.literal("workspace.recovery.inspect.response"),
4057
+ payload: z.object({
4058
+ requestId: z.string(),
4059
+ state: WorkspaceRecoveryStateSchema,
4060
+ }),
4061
+ });
4062
+ export const WorkspaceRecoveryRestoreResponseSchema = z.object({
4063
+ type: z.literal("workspace.recovery.restore.response"),
4064
+ payload: z.object({
4065
+ requestId: z.string(),
4066
+ workspaceId: z.string(),
4067
+ accepted: z.boolean(),
4068
+ error: z.string().nullable(),
4069
+ }),
4070
+ });
3501
4071
  export const SetVoiceModeResponseMessageSchema = z.object({
3502
4072
  type: z.literal("set_voice_mode_response"),
3503
4073
  payload: z.object({
@@ -3659,7 +4229,7 @@ export const RunsClearResponseSchema = z.object({
3659
4229
  requestId: z.string(),
3660
4230
  }),
3661
4231
  });
3662
- // Delete one run by id. Terminal (done/failed/canceled) and draft runs only
4232
+ // Delete one run by id. Terminal (done/failed/canceled) and draft runs only -
3663
4233
  // deleting an active run is refused so a cleanup click can't silently orphan
3664
4234
  // running agents; cancel it first. Gated by server_info.features.runsDelete.
3665
4235
  export const RunsDeleteRequestSchema = z.object({
@@ -3671,7 +4241,7 @@ export const RunsDeleteResponseSchema = z.object({
3671
4241
  type: z.literal("runs.delete.response"),
3672
4242
  payload: z.object({
3673
4243
  // The deleted id, or absent when nothing was deleted (unknown or still
3674
- // active) `error` then carries why.
4244
+ // active) - `error` then carries why.
3675
4245
  runId: z.string().optional(),
3676
4246
  error: z.string().optional(),
3677
4247
  requestId: z.string(),
@@ -3786,11 +4356,11 @@ export const RunsTemplatesChangedNotificationSchema = z.object({
3786
4356
  }),
3787
4357
  });
3788
4358
  // Start (or draft) a user-initiated orchestration from the New Orchestration
3789
- // dialog. `flavor` is an open vocabulary: "ai" (prompt-and-go the daemon
4359
+ // dialog. `flavor` is an open vocabulary: "ai" (prompt-and-go - the daemon
3790
4360
  // spawns an orchestrator agent that declares its own plan via start_run) or
3791
- // "graph" (deterministic the daemon executes `graphId` with `graphInputs`).
4361
+ // "graph" (deterministic - the daemon executes `graphId` with `graphInputs`).
3792
4362
  // `draft: true` creates the record without executing (the designer flow);
3793
- // `runId` executes an existing draft in place or, with `draft: true`, re-saves
4363
+ // `runId` executes an existing draft in place - or, with `draft: true`, re-saves
3794
4364
  // that draft in place (Edit Orchestration).
3795
4365
  export const RunsStartRequestSchema = z.object({
3796
4366
  type: z.literal("runs.start.request"),
@@ -3842,7 +4412,7 @@ export const CheckoutGitCommitRequestSchema = z.object({
3842
4412
  });
3843
4413
  // Resolve which agent the daemon would use to author a commit message for this
3844
4414
  // checkout (the "writer" role) so the client can name it in a confirmation
3845
- // before running the AI-authored commit. A pure query it never commits. Gated
4415
+ // before running the AI-authored commit. A pure query - it never commits. Gated
3846
4416
  // by server_info.features.checkoutGitCommitAgent.
3847
4417
  export const CheckoutGitCommitAgentRequestSchema = z.object({
3848
4418
  type: z.literal("checkout.git.commit_agent.request"),
@@ -3894,8 +4464,8 @@ export const CheckoutGitFileCommitDiffRequestSchema = z.object({
3894
4464
  ignoreWhitespace: z.boolean().optional(),
3895
4465
  requestId: z.string(),
3896
4466
  });
3897
- // One page of blame. Always paged blaming a large file whole would block the
3898
- // daemon so the client walks the file a page at a time.
4467
+ // One page of blame. Always paged - blaming a large file whole would block the
4468
+ // daemon - so the client walks the file a page at a time.
3899
4469
  export const CheckoutGitFileBlameRequestSchema = z.object({
3900
4470
  type: z.literal("checkout.git.get_file_blame.request"),
3901
4471
  cwd: z.string(),
@@ -3958,6 +4528,15 @@ export const CheckoutPrMergeRequestSchema = z.object({
3958
4528
  mergeMethod: z.enum(["merge", "squash", "rebase"]),
3959
4529
  requestId: z.string(),
3960
4530
  });
4531
+ export const CheckoutForgeSetAutoMergeRequestSchema = z.object({
4532
+ type: z.literal("checkout.forge.set_auto_merge.request"),
4533
+ cwd: z.string(),
4534
+ enabled: z.boolean(),
4535
+ mergeMethod: z.enum(["merge", "squash", "rebase"]).optional(),
4536
+ requestId: z.string(),
4537
+ });
4538
+ // COMPAT(githubAutoMergeRpc): added in v0.1.106, remove after 2026-12-28 once
4539
+ // all supported clients use checkout.forge.set_auto_merge.*.
3961
4540
  export const CheckoutGithubSetAutoMergeRequestSchema = z.object({
3962
4541
  type: z.literal("checkout.github.set_auto_merge.request"),
3963
4542
  cwd: z.string(),
@@ -3965,16 +4544,63 @@ export const CheckoutGithubSetAutoMergeRequestSchema = z.object({
3965
4544
  mergeMethod: z.enum(["merge", "squash", "rebase"]).optional(),
3966
4545
  requestId: z.string(),
3967
4546
  });
4547
+ const CheckoutCommitFileSchema = z.object({
4548
+ path: z.string(),
4549
+ additions: z.number(),
4550
+ deletions: z.number(),
4551
+ status: z.enum(["added", "modified", "deleted", "renamed"]).optional(),
4552
+ });
4553
+ const CheckoutCommitSchema = z.object({
4554
+ sha: z.string(),
4555
+ shortSha: z.string(),
4556
+ subject: z.string(),
4557
+ authorName: z.string(),
4558
+ authorDate: z.string(), // ISO 8601
4559
+ isOnRemote: z.boolean(), // false = local-only (unpushed)
4560
+ // COMPAT(commitBaseClassification): added in v0.2.0, remove optional after 2027-01-23.
4561
+ isOnBase: z.boolean().optional(),
4562
+ files: z.array(CheckoutCommitFileSchema),
4563
+ });
4564
+ export const CheckoutCommitsListRequestSchema = z.object({
4565
+ type: z.literal("checkout.commits.list.request"),
4566
+ cwd: z.string(),
4567
+ requestId: z.string(),
4568
+ });
4569
+ export const CheckoutCommitFileDiffRequestSchema = z.object({
4570
+ type: z.literal("checkout.commits.file_diff.request"),
4571
+ cwd: z.string(),
4572
+ sha: z.string(),
4573
+ path: z.string(),
4574
+ requestId: z.string(),
4575
+ });
3968
4576
  const GitHubRepoSegmentSchema = z.string().regex(/^[A-Za-z0-9._-]+$/);
3969
- export const CheckoutGithubGetCheckDetailsRequestSchema = z.object({
3970
- type: z.literal("checkout.github.get_check_details.request"),
4577
+ const CheckoutCheckDetailsRequestPayloadSchema = z.object({
3971
4578
  cwd: z.string(),
3972
- repoOwner: GitHubRepoSegmentSchema,
3973
- repoName: GitHubRepoSegmentSchema,
3974
- checkRunId: z.number().int().positive(),
4579
+ // GitHub addresses check runs by owner/name. GitLab resolves the project from
4580
+ // cwd and omits these GitHub-only single-segment fields.
4581
+ repoOwner: GitHubRepoSegmentSchema.optional(),
4582
+ repoName: GitHubRepoSegmentSchema.optional(),
4583
+ // Permanently optional: a check addressed only by workflowRunId (Gitea
4584
+ // Actions runs carry no check-run id) is fetchable. Callers send at least one
4585
+ // of checkRunId/workflowRunId; the gated forge RPC only reaches daemons that
4586
+ // understand this.
4587
+ checkRunId: z.number().int().positive().optional(),
3975
4588
  workflowRunId: z.number().int().positive().optional(),
4589
+ // Permanent forge-routing field, optional because only some forges need it:
4590
+ // GitLab routes check details to the MR's head pipeline; Gitea-family adapters
4591
+ // resolve the PR head SHA by number, including after merge/close. GitHub
4592
+ // ignores it.
4593
+ changeRequestNumber: z.number().int().positive().optional(),
3976
4594
  requestId: z.string(),
3977
4595
  });
4596
+ export const CheckoutForgeGetCheckDetailsRequestSchema = CheckoutCheckDetailsRequestPayloadSchema.extend({
4597
+ type: z.literal("checkout.forge.get_check_details.request"),
4598
+ });
4599
+ // COMPAT(githubCheckDetailsRpc): added in v0.1.106, remove after 2026-12-28 once
4600
+ // all supported clients use checkout.forge.get_check_details.*.
4601
+ export const CheckoutGithubGetCheckDetailsRequestSchema = CheckoutCheckDetailsRequestPayloadSchema.extend({
4602
+ type: z.literal("checkout.github.get_check_details.request"),
4603
+ });
3978
4604
  export const CheckoutPrStatusRequestSchema = z.object({
3979
4605
  type: z.literal("checkout_pr_status_request"),
3980
4606
  cwd: z.string(),
@@ -4063,17 +4689,41 @@ export const BranchSuggestionsRequestSchema = z.object({
4063
4689
  });
4064
4690
  export const GitHubSearchItemSchema = z.object({
4065
4691
  kind: z.enum(["issue", "pr"]),
4692
+ forge: z.string().optional(),
4066
4693
  number: z.number(),
4067
4694
  title: z.string(),
4068
4695
  url: z.string(),
4069
4696
  state: z.string(),
4070
4697
  body: z.string().nullable(),
4071
4698
  labels: z.array(z.string()),
4699
+ projectPath: z.string().optional(),
4072
4700
  baseRefName: z.string().nullable().optional(),
4073
4701
  headRefName: z.string().nullable().optional(),
4074
4702
  updatedAt: z.string().optional(),
4075
4703
  });
4076
- export const GitHubSearchKindSchema = z.enum(["github-issue", "github-pr"]);
4704
+ export const ForgeSearchItemSchema = GitHubSearchItemSchema.extend({
4705
+ kind: z.enum(["issue", "change_request"]),
4706
+ });
4707
+ // COMPAT(githubSearchKind): added in v0.1.106, remove with the legacy
4708
+ // github_search_request RPC after 2026-12-28.
4709
+ export const ForgeSearchKindSchema = z.enum([
4710
+ "issue",
4711
+ "change_request",
4712
+ "github-issue",
4713
+ "github-pr",
4714
+ "pr",
4715
+ ]);
4716
+ export const GitHubSearchKindSchema = ForgeSearchKindSchema;
4717
+ export const ForgeSearchRequestSchema = z.object({
4718
+ type: z.literal("forge.search.request"),
4719
+ cwd: z.string(),
4720
+ query: z.string(),
4721
+ limit: z.number().int().min(1).max(50).optional(),
4722
+ kinds: z.array(ForgeSearchKindSchema).optional(),
4723
+ requestId: z.string(),
4724
+ });
4725
+ // COMPAT(githubSearchRpc): added in v0.1.106, remove after 2026-12-28 once
4726
+ // clients use forge.search.*.
4077
4727
  export const GitHubSearchRequestSchema = z.object({
4078
4728
  type: z.literal("github_search_request"),
4079
4729
  cwd: z.string(),
@@ -4094,7 +4744,7 @@ export const HostingSearchRequestSchema = z.object({
4094
4744
  kinds: z.array(HostingSearchKindSchema).optional(),
4095
4745
  requestId: z.string(),
4096
4746
  });
4097
- // Reports whether a host-level provider's credentials are valid drives the
4747
+ // Reports whether a host-level provider's credentials are valid - drives the
4098
4748
  // connection-status row in the host Git providers settings section.
4099
4749
  export const HostingAuthStatusRequestSchema = z.object({
4100
4750
  type: z.literal("hosting.auth_status.request"),
@@ -4154,6 +4804,9 @@ export const CreateOttoWorktreeRequestSchema = z.object({
4154
4804
  firstAgentContext: FirstAgentContextSchema.optional(),
4155
4805
  refName: z.string().min(1).optional(),
4156
4806
  action: z.enum(["branch-off", "checkout"]).optional(),
4807
+ checkoutSource: ChangeRequestCheckoutSourceSchema.optional(),
4808
+ // COMPAT(githubPrNumber): added in v0.1.106, remove after 2026-12-28 once
4809
+ // clients send checkoutSource: { kind: "change_request", forge, number }.
4157
4810
  githubPrNumber: z.number().int().positive().optional(),
4158
4811
  requestId: z.string(),
4159
4812
  });
@@ -4181,6 +4834,8 @@ export const OpenProjectRequestSchema = z.object({
4181
4834
  cwd: z.string(),
4182
4835
  requestId: z.string(),
4183
4836
  });
4837
+ // Smallest shorthand repo path is "a/b": owner, slash, repository.
4838
+ const MIN_REPOSITORY_PATH_LENGTH = 3;
4184
4839
  export const ProjectAddRequestSchema = z.object({
4185
4840
  type: z.literal("project.add.request"),
4186
4841
  cwd: z.string(),
@@ -4192,10 +4847,10 @@ export const ProjectAddRequestSchema = z.object({
4192
4847
  // from a remote), optionally create that remote on a connected hosting
4193
4848
  // provider, then register the result as a project. One RPC rather than a
4194
4849
  // client-driven sequence so a half-finished project can never be left behind by
4195
- // a dropped socket the daemon owns the whole transaction.
4850
+ // a dropped socket - the daemon owns the whole transaction.
4196
4851
  // COMPAT(projectScaffold): added in v0.6.9. Gated by server_info.features.projectScaffold.
4197
4852
  // Built-in .gitignore starters. Deliberately a short list of the ecosystems Otto
4198
- // itself works in this is a convenience, not a mirror of github/gitignore. The
4853
+ // itself works in - this is a convenience, not a mirror of github/gitignore. The
4199
4854
  // wire field stays an open string so a newer daemon can add one without an older
4200
4855
  // client's validator rejecting the message.
4201
4856
  export const PROJECT_SCAFFOLD_GITIGNORE_TEMPLATE_IDS = [
@@ -4276,12 +4931,41 @@ export const HostingListOwnersRequestSchema = z.object({
4276
4931
  provider: GitHostingProviderIdWireSchema,
4277
4932
  requestId: z.string(),
4278
4933
  });
4934
+ export const ProjectCreateDirectoryRequestSchema = z.object({
4935
+ type: z.literal("project.create_directory.request"),
4936
+ parentPath: z.string(),
4937
+ name: z.string(),
4938
+ requestId: z.string(),
4939
+ });
4940
+ export const GithubRepositorySchema = z.object({
4941
+ id: z.string().min(1),
4942
+ name: z.string().min(1),
4943
+ nameWithOwner: z.string().min(MIN_REPOSITORY_PATH_LENGTH),
4944
+ description: z.string().nullable(),
4945
+ visibility: z.enum(["public", "private", "internal"]),
4946
+ updatedAt: z.string(),
4947
+ cloneUrl: z.string().min(MIN_REPOSITORY_PATH_LENGTH),
4948
+ });
4949
+ export const WorkspaceGithubSearchRepositoriesRequestSchema = z.object({
4950
+ type: z.literal("workspace.github.search_repositories.request"),
4951
+ query: z.string(),
4952
+ limit: z.number().int().min(1).max(50).optional(),
4953
+ requestId: z.string(),
4954
+ });
4955
+ export const ProjectGithubCloneProtocolSchema = z.enum(["https", "ssh"]);
4956
+ export const ProjectGithubCloneRequestSchema = z.object({
4957
+ type: z.literal("project.github.clone.request"),
4958
+ repo: z.string().trim().min(MIN_REPOSITORY_PATH_LENGTH),
4959
+ cloneProtocol: ProjectGithubCloneProtocolSchema.optional(),
4960
+ targetDirectory: z.string().trim().min(1),
4961
+ requestId: z.string(),
4962
+ });
4279
4963
  export const ArchiveWorkspaceRequestSchema = z.object({
4280
4964
  type: z.literal("archive_workspace_request"),
4281
4965
  workspaceId: z.string(),
4282
4966
  requestId: z.string(),
4283
4967
  // COMPAT(worktreeArchiveBranchCleanup): added in v0.6.7, drop the optional
4284
- // gate when daemon floor >= v0.6.7. Absent means "keep" the leftover local
4968
+ // gate when daemon floor >= v0.6.7. Absent means "keep" - the leftover local
4285
4969
  // branch is never touched (old-client behavior). "delete" asks the daemon to
4286
4970
  // remove the worktree's local branch after the backing directory is torn down
4287
4971
  // (only when this was the last reference to it and the branch is not checked
@@ -4303,7 +4987,7 @@ export const WorkspaceArchivePreflightRequestSchema = z.object({
4303
4987
  // record the answer, and it has to look like one or a wrong guess reads as a bug in the diff.
4304
4988
  // COMPAT(checkoutDiffBaseAnyRepo): added in v0.7.4.
4305
4989
  export const CheckoutBaseSourceSchema = z.enum(["user", "inferred", "worktree", "default"]);
4306
- // Repoint a worktree-backed workspace's base branch what the Changes view diffs
4990
+ // Repoint a worktree-backed workspace's base branch - what the Changes view diffs
4307
4991
  // against, and what merge-into-base and PR creation target. On a stacked branch the
4308
4992
  // useful base is the parent branch, not the repo default, the same way a forge PR
4309
4993
  // carries an explicit base. A null baseRef resets to the repository default branch.
@@ -4313,7 +4997,7 @@ export const WorktreeBaseRefSetRequestSchema = z.object({
4313
4997
  requestId: z.string(),
4314
4998
  workspaceId: z.string(),
4315
4999
  // Branch name; null resets to the default branch. An `origin/` prefix is meaningful and is
4316
- // kept `main` and `origin/main` are different comparisons whenever the two have drifted.
5000
+ // kept - `main` and `origin/main` are different comparisons whenever the two have drifted.
4317
5001
  baseRef: z.string().nullable(),
4318
5002
  // Forget the remembered base and detect the branch's parent again, ignoring `baseRef`.
4319
5003
  // The escape hatch for a wrong guess: parent detection is a heuristic over a graph that does
@@ -4350,6 +5034,11 @@ export const WorkspaceCreateRequestSchema = z.object({
4350
5034
  // the repository default.
4351
5035
  refName: z.string().min(1).optional(),
4352
5036
  baseBranch: z.string().optional(),
5037
+ // New branch name for branch-off. The worktree path may use a different slug.
5038
+ branchName: z.string().min(1).optional(),
5039
+ checkoutSource: ChangeRequestCheckoutSourceSchema.optional(),
5040
+ // COMPAT(githubPrNumber): added in v0.1.106, remove after 2026-12-28 once
5041
+ // clients send checkoutSource.
4353
5042
  githubPrNumber: z.number().int().positive().optional(),
4354
5043
  worktreeSlug: z.string().optional(),
4355
5044
  }),
@@ -4439,6 +5128,7 @@ const FileExplorerFileSchema = z.object({
4439
5128
  // inline JSON read path only); old daemons omit both fields.
4440
5129
  eol: FileEolSchema.optional(),
4441
5130
  hash: z.string().optional(),
5131
+ revision: z.string().optional(),
4442
5132
  });
4443
5133
  const FileExplorerDirectorySchema = z.object({
4444
5134
  path: z.string(),
@@ -4452,6 +5142,103 @@ export const FileExplorerRequestSchema = z.object({
4452
5142
  requestId: z.string(),
4453
5143
  acceptBinary: z.boolean().optional(),
4454
5144
  });
5145
+ export const FileVersionSchema = z.discriminatedUnion("status", [
5146
+ z.object({
5147
+ status: z.literal("ready"),
5148
+ cwd: z.string(),
5149
+ path: z.string(),
5150
+ size: z.number().int().nonnegative(),
5151
+ modifiedAt: z.string(),
5152
+ revision: z.string().optional(),
5153
+ }),
5154
+ z.object({
5155
+ status: z.literal("missing"),
5156
+ cwd: z.string(),
5157
+ path: z.string(),
5158
+ }),
5159
+ z.object({
5160
+ status: z.literal("error"),
5161
+ cwd: z.string(),
5162
+ path: z.string(),
5163
+ error: z.string(),
5164
+ }),
5165
+ ]);
5166
+ export const FileSubscribeRequestSchema = z.object({
5167
+ type: z.literal("fs.file.subscribe.request"),
5168
+ cwd: z.string(),
5169
+ path: z.string(),
5170
+ subscriptionId: z.string(),
5171
+ requestId: z.string(),
5172
+ });
5173
+ export const FileUnsubscribeRequestSchema = z.object({
5174
+ type: z.literal("fs.file.unsubscribe.request"),
5175
+ subscriptionId: z.string(),
5176
+ requestId: z.string(),
5177
+ });
5178
+ export const FsFileWriteRequestSchema = z.object({
5179
+ type: z.literal("fs.file.write.request"),
5180
+ cwd: z.string(),
5181
+ path: z.string(),
5182
+ content: z.string(),
5183
+ expectedModifiedAt: z.string(),
5184
+ expectedRevision: z.string().optional(),
5185
+ requestId: z.string(),
5186
+ });
5187
+ /**
5188
+ * Write bytes to a workspace file.
5189
+ *
5190
+ * The counterpart to `fs.file.write`, which is text only: it LF-normalizes,
5191
+ * re-applies the file's detected EOL, and outright refuses to overwrite a file
5192
+ * whose current bytes look binary. None of that can carry a PDF, an image or
5193
+ * any other generated artifact, so those go through here instead - the bytes
5194
+ * land verbatim.
5195
+ *
5196
+ * Deliberately not a conditional write. Callers are producing a generated file
5197
+ * from a source they already hold, so there is no "the file changed under you"
5198
+ * to reconcile: either the caller means to replace what is there or it does
5199
+ * not, and `overwrite` says which. Keeping that explicit is what stops this
5200
+ * from being a clobber-any-path primitive.
5201
+ *
5202
+ * Workspace-bounded, like the create/delete/rename surface and unlike
5203
+ * `file.write`. `file.write` is unbounded because a tab may edit a file the
5204
+ * user opened from anywhere; putting new bytes at an arbitrary path on the host
5205
+ * is a different power and does not need to be that wide.
5206
+ *
5207
+ * The bytes themselves do not ride in this message. They follow it as
5208
+ * `FileTransfer` binary frames correlated on `requestId` - FileBegin, then
5209
+ * FileChunk, then FileEnd - the same transport `file.upload` uses. This request
5210
+ * is the metadata half: where the bytes go and how many of them to expect.
5211
+ * Everything here writes multi-megabyte files (a printed PDF, a dropped image),
5212
+ * and base64 in a JSON message costs a third again on the wire plus the whole
5213
+ * encoded string allocated on both sides and walked by the validator.
5214
+ */
5215
+ export const FsFileWriteBinaryRequestSchema = z.object({
5216
+ type: z.literal("fs.file.write_binary.request"),
5217
+ cwd: z.string(),
5218
+ path: z.string(),
5219
+ /**
5220
+ * Byte length of the payload to follow. The daemon refuses a transfer that
5221
+ * overruns it and refuses one that ends short, so a truncated stream fails
5222
+ * loudly instead of landing a half file. Optional only because the base64
5223
+ * form below predates it and carries its own length.
5224
+ */
5225
+ size: z.number().int().nonnegative().optional(),
5226
+ /**
5227
+ * base64. Decoded and written as-is: no EOL translation, no re-encoding.
5228
+ *
5229
+ * COMPAT(binaryWriteBase64): added in v0.7.6, drop this field and its daemon
5230
+ * branch on 2027-02-02. Superseded by `size` plus file-transfer frames. The
5231
+ * daemon still reads it - a field we stopped sending is not a field we stop
5232
+ * accepting - and picks the branch from which of the two is present.
5233
+ */
5234
+ contentBase64: z.string().optional(),
5235
+ /**
5236
+ * Replace an existing file. Absent (the default) an existing target comes
5237
+ * back as `exists` and nothing is written.
5238
+ */
5239
+ overwrite: z.boolean().optional(),
5240
+ requestId: z.string(),
5241
+ });
4455
5242
  export const ProjectIconRequestSchema = z.object({
4456
5243
  type: z.literal("project_icon_request"),
4457
5244
  cwd: z.string(),
@@ -4474,7 +5261,7 @@ export const FileUploadRequestSchema = z.object({
4474
5261
  /**
4475
5262
  * Text-editor save. A conditional write: the request carries the client's
4476
5263
  * last-known file identity and the daemon refuses to clobber content it did
4477
- * not hand out a mismatch comes back as a typed conflict, never a write.
5264
+ * not hand out - a mismatch comes back as a typed conflict, never a write.
4478
5265
  */
4479
5266
  export const FileWriteRequestSchema = z.object({
4480
5267
  type: z.literal("file.write.request"),
@@ -4512,7 +5299,7 @@ export const FileCreateRequestSchema = z.object({
4512
5299
  requestId: z.string(),
4513
5300
  });
4514
5301
  /**
4515
- * Permanent delete an unlink, not a move to the OS trash. The daemon may be
5302
+ * Permanent delete - an unlink, not a move to the OS trash. The daemon may be
4516
5303
  * headless, remote, or inside WSL, where there is no reliable trash to move to;
4517
5304
  * a "deleted" file that silently stayed on disk in one environment and vanished
4518
5305
  * in another would be worse than either. The client's confirmation says so.
@@ -4528,7 +5315,7 @@ export const FileDeleteRequestSchema = z.object({
4528
5315
  requestId: z.string(),
4529
5316
  });
4530
5317
  /**
4531
- * Rename and move are the same operation a move is a rename whose new path
5318
+ * Rename and move are the same operation - a move is a rename whose new path
4532
5319
  * has a different parent. Never clobbers: an occupied destination comes back as
4533
5320
  * `exists` and nothing moves. There is no overwrite flag on purpose, so this
4534
5321
  * RPC cannot destroy a file the user did not name.
@@ -4541,7 +5328,7 @@ export const FileRenameRequestSchema = z.object({
4541
5328
  requestId: z.string(),
4542
5329
  });
4543
5330
  /**
4544
- * Refine an AI rewrite the user reviews as a diff before anything is written.
5331
+ * Refine - an AI rewrite the user reviews as a diff before anything is written.
4545
5332
  * This RPC only *proposes*: it reads nothing from disk and writes nothing. The
4546
5333
  * accepted result goes back through `file.write.request` like any other save,
4547
5334
  * so the conditional-write precondition still guards it.
@@ -4569,7 +5356,7 @@ export const FileRefineReferenceSchema = z.object({
4569
5356
  });
4570
5357
  export const FileRefineRequestSchema = z.object({
4571
5358
  type: z.literal("file.refine.request"),
4572
- // Provider resolution only which workspace's mini-task chain runs this.
5359
+ // Provider resolution only - which workspace's mini-task chain runs this.
4573
5360
  // Documents are NOT read from disk here; they travel on the wire.
4574
5361
  cwd: z.string(),
4575
5362
  // What the model may rewrite. The blast radius of the whole request: a file
@@ -4690,7 +5477,7 @@ export const CodeRenamePreviewRequestSchema = z.object({
4690
5477
  *
4691
5478
  * The client sends back only the `planId` it was shown; the daemon recomputes the plan and
4692
5479
  * refuses unless the identity matches. A request that carried its own edit list would be a
4693
- * remote arbitrary-write primitive wearing a rename's name any client could post any text
5480
+ * remote arbitrary-write primitive wearing a rename's name - any client could post any text
4694
5481
  * at any path. This shape makes the daemon's own language server the sole author of what
4695
5482
  * gets written, and the plan id the proof that the user saw it.
4696
5483
  */
@@ -4706,7 +5493,7 @@ export const CodeRenameApplyRequestSchema = z.object({
4706
5493
  requestId: z.string(),
4707
5494
  });
4708
5495
  /**
4709
- * Undo a run. Carries only the run's id the daemon holds the before-images.
5496
+ * Undo a run. Carries only the run's id - the daemon holds the before-images.
4710
5497
  *
4711
5498
  * Declared here, with the other inbound rename schemas, rather than beside its response
4712
5499
  * further down: `SessionInboundMessageSchema` is a top-level const, so a schema it names
@@ -4750,8 +5537,8 @@ export const LspServerStopRequestSchema = z.object({
4750
5537
  * as the build system sees it rather than as the filesystem lays it out.
4751
5538
  *
4752
5539
  * **Independent of the LSP family above, despite sharing the `code.` domain.** There is no
4753
- * project-structure request in the Language Server Protocol not one Otto has yet to wire, one
4754
- * that does not exist so this subsystem builds its own model through Microsoft's solution
5540
+ * project-structure request in the Language Server Protocol - not one Otto has yet to wire, one
5541
+ * that does not exist - so this subsystem builds its own model through Microsoft's solution
4755
5542
  * libraries. Turning C# code intelligence off does not turn this off, and vice versa.
4756
5543
  *
4757
5544
  * Discovery is separate from loading on purpose: `list` decides whether the switcher appears at
@@ -4767,7 +5554,7 @@ export const CodeSolutionListRequestSchema = z.object({
4767
5554
  });
4768
5555
  /**
4769
5556
  * One solution's organisation: folders, the projects inside them, and the configurations. No file
4770
- * membership that is `load_project`, paid per project on expand, because evaluating fifty
5557
+ * membership - that is `load_project`, paid per project on expand, because evaluating fifty
4771
5558
  * projects to render a collapsed tree is the cost this design exists to avoid.
4772
5559
  */
4773
5560
  export const CodeSolutionGetTreeRequestSchema = z.object({
@@ -4779,7 +5566,7 @@ export const CodeSolutionGetTreeRequestSchema = z.object({
4779
5566
  });
4780
5567
  /**
4781
5568
  * One project's evaluated file membership. `solutionPath` scopes the sidecar instance so two
4782
- * solutions in one repo never share a warm `ProjectCollection` and so Phase 4 has the selection
5569
+ * solutions in one repo never share a warm `ProjectCollection` - and so Phase 4 has the selection
4783
5570
  * it needs for `--solution`.
4784
5571
  */
4785
5572
  export const CodeSolutionLoadProjectRequestSchema = z.object({
@@ -4817,7 +5604,7 @@ const FileReplaceMatchSchema = z.object({
4817
5604
  });
4818
5605
  /**
4819
5606
  * Preview-first project replace. Each file carries the hash the preview was
4820
- * built against files changed since are skipped and reported, never
5607
+ * built against - files changed since are skipped and reported, never
4821
5608
  * corrupted. The replacement string is literal (no capture references in v1).
4822
5609
  */
4823
5610
  export const FileReplaceRequestSchema = z.object({
@@ -4901,6 +5688,16 @@ export const CreateTerminalRequestSchema = z.object({
4901
5688
  agentId: z.string().optional(),
4902
5689
  command: z.string().optional(),
4903
5690
  args: z.array(z.string()).optional(),
5691
+ // Initial PTY size. Added in v0.1.107; the app no longer sends it (the estimate cache that fed
5692
+ // it was removed - the pane-focus resize claim sizes the PTY instead). Kept and honored
5693
+ // permanently: released v0.1.107 clients still send it, and programmatic callers may pass an
5694
+ // exact size. Daemons without it start at 80x24 and the first resize corrects that.
5695
+ size: z
5696
+ .object({
5697
+ rows: z.number().int().positive(),
5698
+ cols: z.number().int().positive(),
5699
+ })
5700
+ .optional(),
4904
5701
  requestId: z.string(),
4905
5702
  });
4906
5703
  export const RenameTerminalRequestSchema = z.object({
@@ -4915,6 +5712,31 @@ export const StartWorkspaceScriptRequestSchema = z.object({
4915
5712
  scriptName: z.string(),
4916
5713
  requestId: z.string(),
4917
5714
  });
5715
+ export const WorkspaceScriptListRequestSchema = z.object({
5716
+ type: z.literal("workspace.script.list.request"),
5717
+ workspaceId: z.string(),
5718
+ requestId: z.string(),
5719
+ /**
5720
+ * Also return the Scripts the workspace's own project files declare
5721
+ * (`package.json` scripts, and later Makefile targets, .NET launch profiles),
5722
+ * each tagged with the `source` it came from. Off by default so a client that
5723
+ * predates discovery gets exactly the otto.json list it asked for.
5724
+ * COMPAT(workspaceScriptDiscovery): added in v0.7.6.
5725
+ */
5726
+ includeDiscovered: z.boolean().optional().default(false),
5727
+ });
5728
+ export const WorkspaceScriptStartRequestSchema = z.object({
5729
+ type: z.literal("workspace.script.start.request"),
5730
+ workspaceId: z.string(),
5731
+ scriptName: z.string(),
5732
+ requestId: z.string(),
5733
+ });
5734
+ export const WorkspaceScriptStopRequestSchema = z.object({
5735
+ type: z.literal("workspace.script.stop.request"),
5736
+ workspaceId: z.string(),
5737
+ scriptName: z.string(),
5738
+ requestId: z.string(),
5739
+ });
4918
5740
  export const SubscribeTerminalRequestSchema = z.object({
4919
5741
  type: z.literal("subscribe_terminal_request"),
4920
5742
  terminalId: z.string(),
@@ -4957,15 +5779,39 @@ export const KillTerminalRequestSchema = z.object({
4957
5779
  terminalId: z.string(),
4958
5780
  requestId: z.string(),
4959
5781
  });
4960
- export const CaptureTerminalRequestSchema = z.object({
4961
- type: z.literal("capture_terminal_request"),
4962
- terminalId: z.string(),
4963
- start: z.number().int().optional(),
4964
- end: z.number().int().optional(),
4965
- stripAnsi: z.boolean().default(true),
5782
+ export const CaptureTerminalRequestSchema = z.object({
5783
+ type: z.literal("capture_terminal_request"),
5784
+ terminalId: z.string(),
5785
+ start: z.number().int().optional(),
5786
+ end: z.number().int().optional(),
5787
+ stripAnsi: z.boolean().default(true),
5788
+ requestId: z.string(),
5789
+ });
5790
+ export const HubExecutionAgentCreateRequestSchema = z.object({
5791
+ type: z.literal("hub.execution.agent.create.request"),
5792
+ requestId: z.string(),
5793
+ executionId: z.string(),
5794
+ provider: z.string(),
5795
+ cwd: z.string(),
5796
+ prompt: z.string(),
5797
+ workspaceId: z.string().optional(),
5798
+ model: z.string().optional(),
5799
+ modeId: z.string().optional(),
5800
+ thinkingOptionId: z.string().optional(),
5801
+ featureValues: z.record(z.string(), z.unknown()).optional(),
5802
+ env: z.record(z.string(), z.string()).optional(),
5803
+ worktree: CreateAgentWorktreeTargetSchema.optional(),
5804
+ });
5805
+ export const HubExecutionControlActionSchema = z.enum(["interrupt", "archive"]);
5806
+ export const HubExecutionControlRequestSchema = z.object({
5807
+ type: z.literal("hub.execution.control.request"),
4966
5808
  requestId: z.string(),
5809
+ executionId: z.string(),
5810
+ action: HubExecutionControlActionSchema,
4967
5811
  });
4968
5812
  export const SessionInboundMessageSchema = z.discriminatedUnion("type", [
5813
+ HubExecutionAgentCreateRequestSchema,
5814
+ HubExecutionControlRequestSchema,
4969
5815
  BrowserAutomationExecuteResponseSchema,
4970
5816
  VoiceAudioChunkMessageSchema,
4971
5817
  AbortRequestMessageSchema,
@@ -4974,6 +5820,7 @@ export const SessionInboundMessageSchema = z.discriminatedUnion("type", [
4974
5820
  FetchAgentHistoryRequestMessageSchema,
4975
5821
  FetchRecentProviderSessionsRequestMessageSchema,
4976
5822
  FetchWorkspacesRequestMessageSchema,
5823
+ ProjectListRequestMessageSchema,
4977
5824
  FetchAgentRequestMessageSchema,
4978
5825
  DeleteAgentRequestMessageSchema,
4979
5826
  ArchiveAgentRequestMessageSchema,
@@ -5003,6 +5850,14 @@ export const SessionInboundMessageSchema = z.discriminatedUnion("type", [
5003
5850
  BrainHfSearchRequestSchema,
5004
5851
  BrainHfQuantsRequestSchema,
5005
5852
  BrainModelsAddRequestSchema,
5853
+ BrainModelsInventoryRequestSchema,
5854
+ BrainModelProfileGetRequestSchema,
5855
+ BrainModelProfileSetRequestSchema,
5856
+ BrainModelBudgetGetRequestSchema,
5857
+ BrainModelLoadRequestSchema,
5858
+ BrainModelUnloadRequestSchema,
5859
+ BrainModelDeleteRequestSchema,
5860
+ BrainLogsTailRequestSchema,
5006
5861
  UpdateAgentRequestMessageSchema,
5007
5862
  ProjectRenameRequestSchema,
5008
5863
  ProjectRemoveRequestSchema,
@@ -5010,15 +5865,23 @@ export const SessionInboundMessageSchema = z.discriminatedUnion("type", [
5010
5865
  ProjectLinksSetRequestSchema,
5011
5866
  ProjectLinksUnsetRequestSchema,
5012
5867
  WorkspaceTitleSetRequestSchema,
5868
+ WorkspacePinSetRequestSchema,
5869
+ WorkspaceRecoveryInspectRequestSchema,
5870
+ WorkspaceRecoveryRestoreRequestSchema,
5013
5871
  SetVoiceModeMessageSchema,
5014
5872
  SendAgentMessageRequestSchema,
5015
5873
  WaitForFinishRequestSchema,
5016
5874
  DaemonGetStatusRequestSchema,
5017
5875
  DaemonGetPairingOfferRequestSchema,
5876
+ HubManagementDaemonConnectRequestSchema,
5877
+ HubManagementDaemonGetStatusRequestSchema,
5878
+ HubManagementDaemonDisconnectRequestSchema,
5018
5879
  DiagnosticsRequestSchema,
5019
5880
  GetDaemonConfigRequestMessageSchema,
5020
5881
  SetDaemonConfigRequestMessageSchema,
5021
5882
  ConnectorsListToolsRequestSchema,
5883
+ ConnectorsOauthAuthorizeRequestSchema,
5884
+ ConnectorsOauthDisconnectRequestSchema,
5022
5885
  SpeechSettingsGetOptionsRequestSchema,
5023
5886
  SpeechTtsPreviewRequestSchema,
5024
5887
  SpeechTtsSpeakRequestSchema,
@@ -5061,6 +5924,9 @@ export const SessionInboundMessageSchema = z.discriminatedUnion("type", [
5061
5924
  RestartServerRequestMessageSchema,
5062
5925
  DaemonUpdateRequestMessageSchema,
5063
5926
  FetchAgentTimelineRequestMessageSchema,
5927
+ ProviderSubagentListRequestMessageSchema,
5928
+ ProviderSubagentTimelineRequestMessageSchema,
5929
+ SetAgentTimelineSubscriptionRequestMessageSchema,
5064
5930
  AgentForkContextRequestMessageSchema,
5065
5931
  SetAgentModeRequestMessageSchema,
5066
5932
  SetAgentModelRequestMessageSchema,
@@ -5110,7 +5976,11 @@ export const SessionInboundMessageSchema = z.discriminatedUnion("type", [
5110
5976
  CheckoutRefreshRequestSchema,
5111
5977
  CheckoutPrCreateRequestSchema,
5112
5978
  CheckoutPrMergeRequestSchema,
5979
+ CheckoutForgeSetAutoMergeRequestSchema,
5113
5980
  CheckoutGithubSetAutoMergeRequestSchema,
5981
+ CheckoutCommitsListRequestSchema,
5982
+ CheckoutCommitFileDiffRequestSchema,
5983
+ CheckoutForgeGetCheckDetailsRequestSchema,
5114
5984
  CheckoutGithubGetCheckDetailsRequestSchema,
5115
5985
  PreviewListConfigRequestSchema,
5116
5986
  PreviewStartRequestSchema,
@@ -5125,6 +5995,7 @@ export const SessionInboundMessageSchema = z.discriminatedUnion("type", [
5125
5995
  StashListRequestSchema,
5126
5996
  ValidateBranchRequestSchema,
5127
5997
  BranchSuggestionsRequestSchema,
5998
+ ForgeSearchRequestSchema,
5128
5999
  GitHubSearchRequestSchema,
5129
6000
  HostingSearchRequestSchema,
5130
6001
  HostingAuthStatusRequestSchema,
@@ -5140,6 +6011,9 @@ export const SessionInboundMessageSchema = z.discriminatedUnion("type", [
5140
6011
  ProjectScaffoldRequestSchema,
5141
6012
  HostingListRepositoriesRequestSchema,
5142
6013
  HostingListOwnersRequestSchema,
6014
+ ProjectCreateDirectoryRequestSchema,
6015
+ WorkspaceGithubSearchRepositoriesRequestSchema,
6016
+ ProjectGithubCloneRequestSchema,
5143
6017
  ArchiveWorkspaceRequestSchema,
5144
6018
  WorkspaceArchivePreflightRequestSchema,
5145
6019
  WorktreeBaseRefSetRequestSchema,
@@ -5148,6 +6022,10 @@ export const SessionInboundMessageSchema = z.discriminatedUnion("type", [
5148
6022
  WorkspaceCreateRequestSchema,
5149
6023
  WorkspaceClearAttentionRequestSchema,
5150
6024
  FileExplorerRequestSchema,
6025
+ FileSubscribeRequestSchema,
6026
+ FileUnsubscribeRequestSchema,
6027
+ FsFileWriteRequestSchema,
6028
+ FsFileWriteBinaryRequestSchema,
5151
6029
  ProjectIconRequestSchema,
5152
6030
  FileDownloadTokenRequestSchema,
5153
6031
  FileUploadRequestSchema,
@@ -5187,6 +6065,9 @@ export const SessionInboundMessageSchema = z.discriminatedUnion("type", [
5187
6065
  CreateTerminalRequestSchema,
5188
6066
  RenameTerminalRequestSchema,
5189
6067
  StartWorkspaceScriptRequestSchema,
6068
+ WorkspaceScriptListRequestSchema,
6069
+ WorkspaceScriptStartRequestSchema,
6070
+ WorkspaceScriptStopRequestSchema,
5190
6071
  SubscribeTerminalRequestSchema,
5191
6072
  UnsubscribeTerminalRequestSchema,
5192
6073
  TerminalInputSchema,
@@ -5361,14 +6242,23 @@ export const ServerInfoStatusPayloadSchema = z
5361
6242
  serverId: z.string().trim().min(1),
5362
6243
  hostname: ServerInfoHostnameSchema.optional(),
5363
6244
  version: ServerInfoVersionSchema.optional(),
6245
+ // COMPAT(desktopManaged): added in v0.1.X, remove optional parsing after 2027-01-16.
6246
+ desktopManaged: z.boolean().optional(),
5364
6247
  capabilities: ServerCapabilitiesFromUnknownSchema.optional(),
5365
6248
  // COMPAT(providersSnapshot): added in v0.1.48, remove gating when all clients use snapshot
5366
6249
  features: z
5367
6250
  .object({
5368
6251
  providersSnapshot: z.boolean().optional(),
6252
+ // COMPAT(checkoutForgeSetAutoMerge): added in v0.1.106, remove old
6253
+ // checkoutGithubSetAutoMerge fallback after 2026-12-28.
6254
+ checkoutForgeSetAutoMerge: z.boolean().optional(),
5369
6255
  checkoutGithubSetAutoMerge: z.boolean().optional(),
5370
6256
  // COMPAT(githubCheckDetails): added in v0.1.92, remove gate after 2026-12-08.
5371
6257
  githubCheckDetails: z.boolean().optional(),
6258
+ // COMPAT(forgeCheckDetails): added in v0.1.106, remove githubCheckDetails fallback after 2026-12-28.
6259
+ forgeCheckDetails: z.boolean().optional(),
6260
+ // COMPAT(forgeSearch): added in v0.1.106, remove github_search fallback after 2026-12-28.
6261
+ forgeSearch: z.boolean().optional(),
5372
6262
  // COMPAT(daemonStatusRpc): added in v0.1.76, remove gate after 2026-11-18.
5373
6263
  daemonStatusRpc: z.boolean().optional(),
5374
6264
  // COMPAT(terminalRestoreModes): added in v0.1.81, remove gate after 2026-11-23.
@@ -5391,10 +6281,16 @@ export const ServerInfoStatusPayloadSchema = z
5391
6281
  projectScaffold: z.boolean().optional(),
5392
6282
  // COMPAT(worktreeRestore): added in v0.1.97, drop the gate when floor >= v0.1.97
5393
6283
  worktreeRestore: z.boolean().optional(),
6284
+ // COMPAT(workspaceRecovery): added in v0.1.105, remove after 2027-01-11 once daemon floor >= v0.1.105.
6285
+ workspaceRecovery: z.boolean().optional(),
6286
+ // COMPAT(workspaceFileEditing): added in v0.2.0, remove after 2027-01-18 once daemon floor >= v0.2.0.
6287
+ workspaceFileEditing: z.boolean().optional(),
5394
6288
  // COMPAT(providerUsageList): added in v0.1.98, drop the gate when daemon floor >= v0.1.98.
5395
6289
  providerUsageList: z.boolean().optional(),
5396
6290
  // COMPAT(agentDetach): added in v0.1.98, remove gate after 2026-12-19 once daemon floor >= v0.1.98.
5397
6291
  agentDetach: z.boolean().optional(),
6292
+ // COMPAT(agentThinkingUpdate): added in v0.2.4, remove gate after 2027-01-28.
6293
+ agentThinkingUpdate: z.boolean().optional(),
5398
6294
  // COMPAT(daemonDiagnostics): added in v0.1.100, remove gate after 2026-12-25 once daemon floor >= v0.1.100.
5399
6295
  daemonDiagnostics: z.boolean().optional(),
5400
6296
  // COMPAT(daemonSelfUpdate): added in v0.1.93, remove gate after 2026-12-13.
@@ -5434,6 +6330,16 @@ export const ServerInfoStatusPayloadSchema = z
5434
6330
  // Without it the Brain "Models" section hides the Hugging Face search box.
5435
6331
  // COMPAT(brainHfSearch): added in v0.7.5, remove gate after 2026-07-30 once daemon floor >= v0.7.5.
5436
6332
  brainHfSearch: z.boolean().optional(),
6333
+ // Daemon proxies the brain's own /__host/* management API: the joined
6334
+ // model inventory, per-model profiles, the VRAM budget, load/unload,
6335
+ // delete, and the log tail. Unlike brainManage (which shells out to the
6336
+ // CLI and is therefore local-only) these work against a remote brain too.
6337
+ // Without it the Brain page is not offered at all. Which of those the
6338
+ // brain on the far side actually serves is a separate question, answered
6339
+ // by `capabilities` on brain.host.status, because the daemon and the
6340
+ // brain version independently.
6341
+ // COMPAT(brainConsole): added in v0.7.7, drop the gate when daemon floor >= v0.7.7.
6342
+ brainConsole: z.boolean().optional(),
5437
6343
  // COMPAT(agentForkContext): added in v0.1.102, remove gate after 2026-12-28.
5438
6344
  agentForkContext: z.boolean().optional(),
5439
6345
  // COMPAT(providerRemove): added in v0.1.105, drop the gate when daemon floor >= v0.1.105.
@@ -5456,7 +6362,7 @@ export const ServerInfoStatusPayloadSchema = z
5456
6362
  // Daemon owns a per-agent queue of steering messages, accepts
5457
6363
  // `delivery: "queue"` on send, reports `queuedMessages` on the agent
5458
6364
  // snapshot, and serves agent.queue.remove/clear. Without it the
5459
- // composer keeps its own local queue that is the pre-existing
6365
+ // composer keeps its own local queue - that is the pre-existing
5460
6366
  // behavior, not a degraded build of this feature.
5461
6367
  steerQueue: z.boolean().optional(),
5462
6368
  // Daemon serves agent.queue.reorder. Separate from `steerQueue`
@@ -5467,33 +6373,43 @@ export const ServerInfoStatusPayloadSchema = z
5467
6373
  // Daemon reports `cumulativeUsage` (the lifetime in/cached/out split
5468
6374
  // plus its own booked cost) on every agent snapshot, so a chat's total
5469
6375
  // spend can be summed and priced honestly. Without it the client shows
5470
- // token totals only and no cost NOT an estimated cost, which is the
6376
+ // token totals only and no cost - NOT an estimated cost, which is the
5471
6377
  // behavior this feature exists to remove.
5472
6378
  // COMPAT(cumulativeUsage): added in v0.7.0, drop the gate when daemon floor >= v0.7.0.
5473
6379
  cumulativeUsage: z.boolean().optional(),
5474
6380
  // Daemon can resolve and evaluate the provider's context graph, serve
5475
6381
  // context.report.* and push context_report_changed. Without it the
5476
6382
  // client hides both the Context Management tab and the composer
5477
- // warning entirely there is no degraded client-side fallback, since
6383
+ // warning entirely - there is no degraded client-side fallback, since
5478
6384
  // only the daemon can see the files a provider loads.
5479
6385
  // COMPAT(contextManagement): added in v0.6.5, drop the gate when daemon floor >= v0.6.5.
5480
6386
  contextManagement: z.boolean().optional(),
5481
6387
  // COMPAT(textEditor): added in v0.4.4, drop the gate when daemon floor >= v0.4.4.
5482
6388
  textEditor: z.boolean().optional(),
5483
- // Refine the daemon can turn a pinned document plus an instruction
6389
+ // Refine - the daemon can turn a pinned document plus an instruction
5484
6390
  // into a proposed rewrite (`file.refine.*`). Without it the Refine
5485
6391
  // entry is absent: there is no client-side substitute for a model, and
5486
6392
  // a degraded "open a chat instead" path is exactly the unreviewed edit
5487
6393
  // Refine exists to replace.
5488
6394
  // COMPAT(refine): added in v0.6.9, drop the gate when daemon floor >= v0.6.9.
5489
6395
  refine: z.boolean().optional(),
5490
- // Personality memory the daemon stores per-personality lessons, injects
6396
+ // Personality memory - the daemon stores per-personality lessons, injects
5491
6397
  // them at spawn, and serves personality.memory.*. Without it the client
5492
6398
  // hides the Memory tab, the accrual indicator and the transfer-on-delete
5493
6399
  // choice: storage is daemon-side by definition, so there is nothing a
5494
6400
  // client-side fallback could read.
5495
6401
  // COMPAT(personalityMemory): added in v0.7.0, drop the gate when daemon floor >= v0.7.0.
5496
6402
  personalityMemory: z.boolean().optional(),
6403
+ // Script discovery - the daemon scans a workspace for the Scripts its
6404
+ // project files already declare (package.json scripts, and later
6405
+ // Makefile targets, .NET launch profiles) and serves them from
6406
+ // `workspace.script.list` with `includeDiscovered`. Without it the
6407
+ // Scripts dropdown shows only what otto.json declares, which is the
6408
+ // pre-existing behavior and not a degraded build of this feature: only
6409
+ // the daemon can read the workspace's files, so there is no client-side
6410
+ // scan to fall back to.
6411
+ // COMPAT(workspaceScriptDiscovery): added in v0.7.6, drop the gate when daemon floor >= v0.7.6.
6412
+ workspaceScriptDiscovery: z.boolean().optional(),
5497
6413
  // COMPAT(projectSearch): added in v0.4.4, drop the gate when daemon floor >= v0.4.4.
5498
6414
  projectSearch: z.boolean().optional(),
5499
6415
  // COMPAT(codeIndex): added in v0.4.4, drop the gate when daemon floor >= v0.4.4.
@@ -5510,11 +6426,11 @@ export const ServerInfoStatusPayloadSchema = z
5510
6426
  // flag the client says to update the host rather than showing an empty screen.
5511
6427
  // COMPAT(lspHostServers): added in v0.7.3, drop the gate when daemon floor >= v0.7.3.
5512
6428
  lspHostServers: z.boolean().optional(),
5513
- // The Solution view the daemon can discover solutions and serve
6429
+ // The Solution view - the daemon can discover solutions and serve
5514
6430
  // `code.solution.*`. Deliberately NOT implied by `lsp`: there is no
5515
6431
  // project-structure request in LSP, so this subsystem is independent of
5516
6432
  // language servers and of the C# row's on/off state. Without the flag the
5517
- // client never shows the view switcher and never asks there is no
6433
+ // client never shows the view switcher and never asks - there is no
5518
6434
  // client-side substitute for reading a solution, and a hand-parsed
5519
6435
  // half-tree is exactly the mistake this design exists to avoid.
5520
6436
  // COMPAT(solutionView): added in v0.6.8, drop the gate when daemon floor >= v0.6.8.
@@ -5549,7 +6465,7 @@ export const ServerInfoStatusPayloadSchema = z
5549
6465
  // COMPAT(checkoutGitLog): added in v0.5.1, drop the gate when daemon floor >= v0.5.1.
5550
6466
  checkoutGitLog: z.boolean().optional(),
5551
6467
  // Local-git file investigation: history, per-commit diff, blame, origin
5552
- // commit for a whole file or a line range. No forge connection needed
6468
+ // commit - for a whole file or a line range. No forge connection needed
5553
6469
  // and no per-provider rollout; it is git, so every provider gets it at
5554
6470
  // once.
5555
6471
  // COMPAT(checkoutGitFileHistory): added in v0.6.6, drop the gate when daemon floor >= v0.6.6.
@@ -5558,19 +6474,19 @@ export const ServerInfoStatusPayloadSchema = z
5558
6474
  // archiving (workspace.archive.preflight.*) and delete it as part of the
5559
6475
  // archive (archive_workspace_request.branchDisposition). Without it the
5560
6476
  // client archives the worktree exactly as before and never offers to
5561
- // remove the branch no degraded client-side branch detection exists.
6477
+ // remove the branch - no degraded client-side branch detection exists.
5562
6478
  // COMPAT(worktreeArchiveBranchCleanup): added in v0.6.7, drop the gate when daemon floor >= v0.6.7.
5563
6479
  worktreeArchiveBranchCleanup: z.boolean().optional(),
5564
6480
  // COMPAT(worktreeReattach): added in v0.6.7, drop the gate when daemon floor >= v0.6.7.
5565
6481
  worktreeReattach: z.boolean().optional(),
5566
6482
  // Set when the daemon can repoint a worktree's stored base branch
5567
6483
  // (worktree.baseRef.set.*). Without it the client renders the base as a
5568
- // read-only "vs <base>" label there is no client-side override, since only
6484
+ // read-only "vs <base>" label - there is no client-side override, since only
5569
6485
  // the daemon can write the worktree's metadata.
5570
6486
  // COMPAT(worktreeDiffBase): added in v0.6.8, drop the gate when daemon floor >= v0.6.8.
5571
6487
  worktreeDiffBase: z.boolean().optional(),
5572
6488
  // Set when the daemon stores the diff base *per branch*, which is what lets any git
5573
- // checkout repoint it rather than only an Otto worktree a plain checkout's gitdir is
6489
+ // checkout repoint it rather than only an Otto worktree - a plain checkout's gitdir is
5574
6490
  // shared by every branch in it, so a single stored base would bleed across branch
5575
6491
  // switches. Also gates parent-branch detection, the `origin/`-qualified pin, and the
5576
6492
  // re-detect action. Without it the client keeps the worktree-only picker.
@@ -5626,24 +6542,30 @@ export const ServerInfoStatusPayloadSchema = z
5626
6542
  // cap) shows "Update the host" instead of a knob that does nothing.
5627
6543
  openaiCompatMaxToolRounds: z.boolean().optional(),
5628
6544
  // COMPAT(mcpToolGroups): added in v0.6.4, drop the gate when daemon floor >= v0.6.4.
5629
- // Set when the daemon honors `mcp.toolGroups` per-group gating of the
6545
+ // Set when the daemon honors `mcp.toolGroups` - per-group gating of the
5630
6546
  // Otto tool catalog on the MCP (Claude) path. Old daemons register every
5631
6547
  // group regardless, so the client hides the categorized section instead
5632
6548
  // of showing category switches that do nothing.
5633
6549
  mcpToolGroups: z.boolean().optional(),
5634
6550
  // COMPAT(connectors): added in v0.7.5, drop the gate when daemon floor >= v0.7.5.
5635
- // Set when the daemon persists and honors `connectors` MCP servers
6551
+ // Set when the daemon persists and honors `connectors` - MCP servers
5636
6552
  // surfaced as named, toggle-able integrations with per-tool disable,
5637
6553
  // enforced today on the openai-compat path. Old daemons ignore the
5638
6554
  // section, so the client hides the Connectors settings entirely.
5639
6555
  connectors: z.boolean().optional(),
6556
+ // COMPAT(connectorOauth): added in v0.7.7, drop the gate when daemon floor >= v0.7.7.
6557
+ // Set when the daemon can run the OAuth authorization-code flow for a
6558
+ // connector and attach the resulting token to its MCP transport. Old
6559
+ // daemons have no broker and no way to hold a token, so the client hides
6560
+ // Connect / Disconnect and offers only the paste-a-token connectors.
6561
+ connectorOauth: z.boolean().optional(),
5640
6562
  // COMPAT(agentBehaviorToggles): added in v0.6.4, drop the gate when daemon floor >= v0.6.4.
5641
6563
  // Set when the daemon persists `agentBehaviors.*` (promptSuggestions,
5642
6564
  // agentProgressSummaries, notifyOnFinishDefault). The reads are wired by
5643
6565
  // Claude-tier providers (WP-E); the client gates the toggle cards on this.
5644
6566
  agentBehaviorToggles: z.boolean().optional(),
5645
6567
  // COMPAT(todoReminders): added in v0.7.5, drop the gate when daemon floor >= v0.7.5.
5646
- // Set when the daemon acts on `agentBehaviors.{todoNudge,todoReconcileOnIdle}`
6568
+ // Set when the daemon acts on `agentBehaviors.{todoNudge,todoReconcileOnIdle}` -
5647
6569
  // the provider-agnostic stale-todo nudge (next turn) and idle reconcile pass.
5648
6570
  // The client gates the task-list toggle cards on this so an old daemon never
5649
6571
  // shows switches that do nothing.
@@ -5674,20 +6596,27 @@ export const ServerInfoStatusPayloadSchema = z
5674
6596
  // the existing `delete_agent_request`, and in bulk via
5675
6597
  // `history.agents.clear_archived`. Archive has always been a soft delete
5676
6598
  // with no counterpart; this is the counterpart. Deleting removes Otto's
5677
- // record only provider transcripts are never touched (see the
6599
+ // record only - provider transcripts are never touched (see the
5678
6600
  // clear_archived schema). The client hides both affordances when this is
5679
6601
  // absent rather than shipping a degraded path.
5680
6602
  historyDelete: z.boolean().optional(),
5681
6603
  // COMPAT(fileMutations): added in v0.7.0, drop the gate when daemon floor >= v0.7.0.
5682
6604
  // Set when the daemon serves `file.create`, `file.delete` and
5683
- // `file.rename` creating, removing and moving entries, as opposed to
6605
+ // `file.rename` - creating, removing and moving entries, as opposed to
5684
6606
  // `file.write`, which only changes what is inside an existing file.
5685
6607
  // There is no client-side substitute (the client never touches the
5686
6608
  // filesystem), so an old daemon simply does not get the menu items.
5687
6609
  fileMutations: z.boolean().optional(),
6610
+ // COMPAT(binaryFileWrite): added in v0.7.6, drop the gate when daemon
6611
+ // floor >= v0.7.6. Set when the daemon serves
6612
+ // `fs.file.write_binary` - bytes to a workspace path, as opposed to
6613
+ // `fs.file.write`, which is text and refuses binary targets outright.
6614
+ // The client cannot write a workspace file itself on any platform, so
6615
+ // an old daemon simply does not offer the exports that produce bytes.
6616
+ binaryFileWrite: z.boolean().optional(),
5688
6617
  // COMPAT(attachmentStorage): added in v0.7.1, drop the gate when daemon floor >= v0.7.1.
5689
6618
  // Set when the daemon serves `attachments.images.get_stats` and
5690
- // `attachments.images.clear` the readout and reclaim for the images it
6619
+ // `attachments.images.clear` - the readout and reclaim for the images it
5691
6620
  // materializes on the agent's behalf. The client has no way to size or
5692
6621
  // clear a directory on the host, so an old daemon simply does not get
5693
6622
  // the daemon half of the Storage section; the app-side preview cache row
@@ -5695,10 +6624,44 @@ export const ServerInfoStatusPayloadSchema = z
5695
6624
  attachmentStorage: z.boolean().optional(),
5696
6625
  // COMPAT(agentWorkspaceTransfer): added in v0.7.4, drop the gate when
5697
6626
  // daemon floor >= v0.7.4. Set when the daemon serves
5698
- // `agent.workspace.transfer` moving a chat to another workspace over
6627
+ // `agent.workspace.transfer` - moving a chat to another workspace over
5699
6628
  // the same directory. The client cannot restamp ownership itself (it is
5700
6629
  // daemon state), so an old daemon simply does not get the menu item.
5701
6630
  agentWorkspaceTransfer: z.boolean().optional(),
6631
+ // COMPAT(agentForkContextCursor): added in v0.1.108, remove gate after 2027-01-14.
6632
+ agentForkContextCursor: z.boolean().optional(),
6633
+ // COMPAT(providerSubagents): added in v0.1.107, remove gate after 2027-01-12.
6634
+ providerSubagents: z.boolean().optional(),
6635
+ // COMPAT(workspacePinning): added in v0.1.107, remove gate after 2027-01-12.
6636
+ workspacePinning: z.boolean().optional(),
6637
+ // COMPAT(hubRelationship): added in v0.1.X, drop the gate when floor >= v0.1.X.
6638
+ hubRelationship: z.boolean().optional(),
6639
+ // COMPAT(projectGithubClone): added in v0.1.108, remove gate after 2027-01-15.
6640
+ projectGithubClone: z.boolean().optional(),
6641
+ // COMPAT(workspaceGithubRepositorySearch): added in v0.1.108, remove gate after 2027-01-15.
6642
+ workspaceGithubRepositorySearch: z.boolean().optional(),
6643
+ // COMPAT(projectCreateDirectory): added in v0.1.108, remove gate after 2027-01-15.
6644
+ projectCreateDirectory: z.boolean().optional(),
6645
+ // COMPAT(projectList): added in v0.2.4, drop the gate when floor >= v0.2.4.
6646
+ projectList: z.boolean().optional(),
6647
+ // COMPAT(commitsList): added in v0.1.110, remove gate after 2027-01-16.
6648
+ commitsList: z.boolean().optional(),
6649
+ // COMPAT(commitBaseClassification): added in v0.2.0, remove gate after 2027-01-23.
6650
+ commitBaseClassification: z.boolean().optional(),
6651
+ // COMPAT(providerRemoval): added in v0.1.105, drop the gate when floor >= v0.1.105.
6652
+ providerRemoval: z.boolean().optional(),
6653
+ // COMPAT(importSessionWorkspaceTarget): added in v0.1.110, remove gate after 2027-01-16.
6654
+ importSessionWorkspaceTarget: z.boolean().optional(),
6655
+ // COMPAT(forgeProviders): added in v0.1.106, drop the gate when daemon floor >= v0.1.106.
6656
+ // Daemon advertises pluggable non-GitHub forge support (the forge registry);
6657
+ // the client gates non-GitHub setup UI on it.
6658
+ forgeProviders: z.boolean().optional(),
6659
+ // COMPAT(selectiveAgentTimeline): added in v0.1.106, remove after 2027-01-12.
6660
+ selectiveAgentTimeline: z.boolean().optional(),
6661
+ // COMPAT(stableProjectIdentity): added in v0.1.109, remove gate after 2027-01-15.
6662
+ stableProjectIdentity: z.boolean().optional(),
6663
+ // COMPAT(workspaceScriptManagement): added in v0.1.105, remove gate after 2027-01-10.
6664
+ workspaceScriptManagement: z.boolean().optional(),
5702
6665
  })
5703
6666
  .optional(),
5704
6667
  })
@@ -5787,7 +6750,7 @@ export const DaemonConfigChangedStatusPayloadSchema = z
5787
6750
  * spinner, so an idempotent snapshot cannot drift out of sync the way a missed
5788
6751
  * transition would.
5789
6752
  *
5790
- * Separate from the workspace status bucket on purpose indexing is not the workspace
6753
+ * Separate from the workspace status bucket on purpose - indexing is not the workspace
5791
6754
  * "working", and folding it in would mislabel a quiet workspace as busy with agent work.
5792
6755
  */
5793
6756
  export const LspActivityChangedStatusPayloadSchema = z
@@ -5810,11 +6773,11 @@ export const CodeDiagnosticSchema = z.object({
5810
6773
  endColumn: z.number().int().positive(),
5811
6774
  severity: CodeDiagnosticSeveritySchema,
5812
6775
  message: z.string(),
5813
- /** Who says so `ts`, `pyright`, a linter behind the server. */
6776
+ /** Who says so - `ts`, `pyright`, a linter behind the server. */
5814
6777
  source: z.string().optional(),
5815
6778
  /** The server's own code for the rule or error, e.g. TypeScript's `2345`. */
5816
6779
  code: z.string().optional(),
5817
- /** Documentation for that rule, when the server offers one oxlint does. */
6780
+ /** Documentation for that rule, when the server offers one - oxlint does. */
5818
6781
  codeHref: z.string().optional(),
5819
6782
  /** Which registry row published it, so two servers on one file stay attributable. */
5820
6783
  serverId: z.string().optional(),
@@ -5825,7 +6788,7 @@ export const CodeDiagnosticSchema = z.object({
5825
6788
  * This is the one part of code intelligence that is not request/response:
5826
6789
  * `textDocument/publishDiagnostics` arrives whenever the server has recomputed, which is
5827
6790
  * whenever it feels like it. So it is a status broadcast, and the payload is the document's
5828
- * **whole** current set never a delta. A missed delta would leave a stale squiggle on a
6791
+ * **whole** current set - never a delta. A missed delta would leave a stale squiggle on a
5829
6792
  * line the user already fixed, and an idempotent snapshot cannot drift.
5830
6793
  *
5831
6794
  * Only documents a client has synced produce these. A server may know about every file in
@@ -5918,8 +6881,50 @@ export const ProjectPlacementPayloadSchema = z.object({
5918
6881
  });
5919
6882
  export const WorkspaceScriptLifecycleSchema = z.enum(["running", "stopped"]);
5920
6883
  export const WorkspaceScriptHealthSchema = z.enum(["healthy", "unhealthy"]);
6884
+ export const WorkspaceScriptSourcePayloadSchema = z.object({
6885
+ /** Stable provider id; also the prefix of every `scriptName` it produces. */
6886
+ id: z.string(),
6887
+ /** The tool half of the dropdown's group header, e.g. "npm" or "pnpm". */
6888
+ label: z.string(),
6889
+ /**
6890
+ * Repo-relative file the script was read from, e.g. "package.json".
6891
+ *
6892
+ * Plain-optional rather than defaulted, matching `label` and `command` on the
6893
+ * payload: absent means unknown. A discovery run knows the file, but the
6894
+ * descriptor's orphan path recovers a source from a qualified runtime name
6895
+ * alone (`npm:dev`) and has no discovery behind it to ask. A `.default(null)`
6896
+ * here would make the field required on the output type and force that path
6897
+ * to invent an answer it does not have.
6898
+ */
6899
+ file: z.string().nullable().optional(),
6900
+ });
5921
6901
  export const WorkspaceScriptPayloadSchema = z.object({
6902
+ /**
6903
+ * The launch/stop key, unique within a workspace. Otto's own otto.json
6904
+ * scripts use their bare name; a discovered one is qualified by its source
6905
+ * ("npm:build") so two sources offering "build" cannot collide in the
6906
+ * runtime store or the service-proxy hostname.
6907
+ */
5922
6908
  scriptName: z.string(),
6909
+ /**
6910
+ * What to show instead of `scriptName` - the name the project itself uses.
6911
+ * COMPAT(workspaceScriptDiscovery): added in v0.7.6; absent ⇒ show `scriptName`.
6912
+ */
6913
+ label: z.string().optional(),
6914
+ /**
6915
+ * Where this Script came from. Absent ⇒ declared in otto.json, which is the
6916
+ * only kind that existed before discovery and the only kind that may own a
6917
+ * service-proxy route.
6918
+ * COMPAT(workspaceScriptDiscovery): added in v0.7.6.
6919
+ */
6920
+ source: WorkspaceScriptSourcePayloadSchema.optional(),
6921
+ /**
6922
+ * The command the Script runs, for the row's subtitle. Left plain-optional
6923
+ * rather than defaulted so an existing payload that never carried a command
6924
+ * stays valid without gaining a field it has no answer for.
6925
+ * COMPAT(workspaceScriptDiscovery): added in v0.7.6; absent ⇒ no subtitle.
6926
+ */
6927
+ command: z.string().nullable().optional(),
5923
6928
  type: z.enum(["script", "service"]).optional().default("service"),
5924
6929
  hostname: z.string(),
5925
6930
  port: z.number().int().positive().nullable(),
@@ -6003,7 +7008,9 @@ export const WorkspaceDescriptorPayloadSchema = z
6003
7008
  projectRootPath: z.string(),
6004
7009
  workspaceDirectory: z.string().optional(),
6005
7010
  projectKind: z.enum(["git", "non_git", "directory"]),
6006
- // COMPAT(workspaces): keep legacy directory workspace kind parseable.
7011
+ // COMPAT(workspaces): keep the legacy "directory" workspace kind parseable.
7012
+ // Persisted registries still carry it and there is no migration, so this
7013
+ // stays until a migration retires the kind rather than expiring on a date.
6007
7014
  workspaceKind: z.enum(["directory", "local_checkout", "checkout", "worktree"]),
6008
7015
  name: z.string(),
6009
7016
  // COMPAT(workspaceTitles): added in v0.1.97, drop the optional gate when floor >= v0.1.97.
@@ -6012,6 +7019,8 @@ export const WorkspaceDescriptorPayloadSchema = z
6012
7019
  // its input and offer a "reset to branch name" action. Null means the name
6013
7020
  // is derived from the branch/directory.
6014
7021
  title: z.string().nullable().optional(),
7022
+ // COMPAT(workspacePinning): added in v0.1.107, remove optional after 2027-01-12.
7023
+ pinnedAt: z.string().nullable().optional(),
6015
7024
  archivingAt: z.string().nullable().optional().default(null),
6016
7025
  status: WorkspaceStateBucketSchema,
6017
7026
  // Best-effort workspace status entry timestamp. Old daemons omit the
@@ -6033,6 +7042,10 @@ export const WorkspaceDescriptorPayloadSchema = z
6033
7042
  scripts: z.array(WorkspaceScriptPayloadSchema).default([]),
6034
7043
  gitRuntime: WorkspaceGitRuntimePayloadSchema,
6035
7044
  githubRuntime: WorkspaceGitHubRuntimePayloadSchema,
7045
+ // COMPAT(forge): added in v0.1.106, remove after 2026-12-27. The forge resolved
7046
+ // for this workspace, so the sidebar/hover-card render the right brand mark.
7047
+ // Old daemons omit it; absent means the client falls back to GitHub.
7048
+ forge: z.string().optional(),
6036
7049
  project: ProjectPlacementPayloadSchema.optional(),
6037
7050
  })
6038
7051
  .transform((workspace) => ({
@@ -6124,6 +7137,8 @@ export const FetchRecentProviderSessionsResponseMessageSchema = z.object({
6124
7137
  // workspace is archived.
6125
7138
  export const WorkspaceProjectDescriptorPayloadSchema = z.object({
6126
7139
  projectId: z.string(),
7140
+ // COMPAT(projectKey): added in v0.2.4 on 2026-07-28; remove optional after 2027-01-28.
7141
+ projectKey: z.string().optional(),
6127
7142
  projectDisplayName: z.string(),
6128
7143
  projectCustomName: z.string().nullable().optional(),
6129
7144
  projectRootPath: z.string(),
@@ -6171,7 +7186,7 @@ export const WorkspaceUpdateMessageSchema = z.object({
6171
7186
  });
6172
7187
  // A project's own metadata changed (today: the user renamed it). The workspace
6173
7188
  // channel can only carry a project's name inside its workspaces' descriptors, so
6174
- // a project with no active workspaces had no live channel at all its name only
7189
+ // a project with no active workspaces had no live channel at all - its name only
6175
7190
  // refreshed on the next full workspace fetch. This is the project-level channel:
6176
7191
  // it fires whether or not the project currently has workspaces, and the daemon
6177
7192
  // fans it out to every connected session because project metadata is host-global.
@@ -6185,6 +7200,20 @@ export const ProjectUpdatedNotificationSchema = z.object({
6185
7200
  hasActiveWorkspaces: z.boolean(),
6186
7201
  }),
6187
7202
  });
7203
+ export const ProjectUpdateMessageSchema = z.object({
7204
+ type: z.literal("project.update"),
7205
+ payload: z.discriminatedUnion("kind", [
7206
+ z.object({ kind: z.literal("upsert"), project: WorkspaceProjectDescriptorPayloadSchema }),
7207
+ z.object({ kind: z.literal("remove"), projectId: z.string() }),
7208
+ ]),
7209
+ });
7210
+ export const ProjectListResponseMessageSchema = z.object({
7211
+ type: z.literal("project.list.response"),
7212
+ payload: z.object({
7213
+ requestId: z.string(),
7214
+ projects: z.array(WorkspaceProjectDescriptorPayloadSchema),
7215
+ }),
7216
+ });
6188
7217
  export const ScriptStatusUpdateMessageSchema = z.object({
6189
7218
  type: z.literal("script_status_update"),
6190
7219
  payload: z.object({
@@ -6238,7 +7267,7 @@ export const ProjectScaffoldResponseSchema = z.object({
6238
7267
  type: z.literal("project.scaffold.response"),
6239
7268
  payload: z.object({
6240
7269
  requestId: z.string(),
6241
- // Registered project on success. Null whenever any step failed the
7270
+ // Registered project on success. Null whenever any step failed - the
6242
7271
  // daemon does not register a half-built directory.
6243
7272
  project: WorkspaceProjectDescriptorPayloadSchema.nullable(),
6244
7273
  // Absolute path of the created directory. Non-null even on a late failure
@@ -6316,6 +7345,70 @@ export const HostingListOwnersResponseSchema = z.object({
6316
7345
  error: z.string().nullable(),
6317
7346
  }),
6318
7347
  });
7348
+ export const ProjectCreateDirectoryErrorCodeSchema = z.enum([
7349
+ "invalid_name",
7350
+ "parent_directory_not_found",
7351
+ "directory_exists",
7352
+ "permission_denied",
7353
+ "registration_failed",
7354
+ "filesystem_error",
7355
+ ]);
7356
+ export const ProjectCreateDirectoryResponseSchema = z.object({
7357
+ type: z.literal("project.create_directory.response"),
7358
+ payload: z.object({
7359
+ requestId: z.string(),
7360
+ directoryPath: z.string().nullable(),
7361
+ project: WorkspaceProjectDescriptorPayloadSchema.nullable(),
7362
+ error: z.string().nullable(),
7363
+ // Error codes are open-ended on the wire so older clients can still parse
7364
+ // responses after a newer daemon learns another failure reason.
7365
+ errorCode: z.string().nullable(),
7366
+ }),
7367
+ });
7368
+ export const WorkspaceGithubSearchRepositoriesResponseSchema = z.object({
7369
+ type: z.literal("workspace.github.search_repositories.response"),
7370
+ payload: z.discriminatedUnion("status", [
7371
+ z.object({
7372
+ status: z.literal("success"),
7373
+ requestId: z.string(),
7374
+ repositories: z.array(GithubRepositorySchema),
7375
+ available: z.literal(true),
7376
+ error: z.null(),
7377
+ }),
7378
+ z.object({
7379
+ status: z.literal("unavailable"),
7380
+ requestId: z.string(),
7381
+ repositories: z.array(GithubRepositorySchema),
7382
+ reason: z.literal("gh_missing"),
7383
+ available: z.literal(false),
7384
+ error: z.string(),
7385
+ }),
7386
+ z.object({
7387
+ status: z.literal("unauthenticated"),
7388
+ requestId: z.string(),
7389
+ repositories: z.array(GithubRepositorySchema),
7390
+ available: z.literal(false),
7391
+ error: z.string(),
7392
+ }),
7393
+ z.object({
7394
+ status: z.literal("error"),
7395
+ requestId: z.string(),
7396
+ repositories: z.array(GithubRepositorySchema),
7397
+ available: z.literal(true),
7398
+ error: z.string(),
7399
+ }),
7400
+ ]),
7401
+ });
7402
+ export const ProjectGithubCloneResponseSchema = z.object({
7403
+ type: z.literal("project.github.clone.response"),
7404
+ payload: z.object({
7405
+ requestId: z.string(),
7406
+ repo: z.string().trim().min(MIN_REPOSITORY_PATH_LENGTH),
7407
+ checkoutPath: z.string().nullable(),
7408
+ project: WorkspaceProjectDescriptorPayloadSchema.nullable(),
7409
+ error: z.string().nullable(),
7410
+ }),
7411
+ });
6319
7412
  export const StartWorkspaceScriptResponseMessageSchema = z.object({
6320
7413
  type: z.literal("start_workspace_script_response"),
6321
7414
  payload: z.object({
@@ -6326,6 +7419,26 @@ export const StartWorkspaceScriptResponseMessageSchema = z.object({
6326
7419
  error: z.string().nullable(),
6327
7420
  }),
6328
7421
  });
7422
+ const WorkspaceScriptOperationPayloadSchema = z.object({
7423
+ requestId: z.string(),
7424
+ workspaceId: z.string(),
7425
+ scriptName: z.string().optional(),
7426
+ script: WorkspaceScriptPayloadSchema.nullable().optional(),
7427
+ scripts: z.array(WorkspaceScriptPayloadSchema).optional(),
7428
+ error: z.string().nullable(),
7429
+ });
7430
+ export const WorkspaceScriptListResponseMessageSchema = z.object({
7431
+ type: z.literal("workspace.script.list.response"),
7432
+ payload: WorkspaceScriptOperationPayloadSchema,
7433
+ });
7434
+ export const WorkspaceScriptStartResponseMessageSchema = z.object({
7435
+ type: z.literal("workspace.script.start.response"),
7436
+ payload: WorkspaceScriptOperationPayloadSchema,
7437
+ });
7438
+ export const WorkspaceScriptStopResponseMessageSchema = z.object({
7439
+ type: z.literal("workspace.script.stop.response"),
7440
+ payload: WorkspaceScriptOperationPayloadSchema,
7441
+ });
6329
7442
  // COMPAT(desktopEditorBridge): added in v0.1.88, remove after 2026-12-03 once old clients no longer parse daemon editor RPC responses.
6330
7443
  export const LegacyListAvailableEditorsResponseMessageSchema = z.object({
6331
7444
  type: z.literal("list_available_editors_response"),
@@ -6363,7 +7476,7 @@ export const ArchiveWorkspaceResponseMessageSchema = z.object({
6363
7476
  // the workspace is archived. See WorkspaceArchivePreflightRequestSchema.
6364
7477
  export const WorktreeArchiveBranchDetectionSchema = z.object({
6365
7478
  // True only for Otto-owned worktrees whose branch we can offer to delete.
6366
- // False for local checkouts, plain directories, and non-owned worktrees the
7479
+ // False for local checkouts, plain directories, and non-owned worktrees - the
6367
7480
  // client then skips the branch-cleanup UI entirely.
6368
7481
  isOttoWorktree: z.boolean(),
6369
7482
  // The local branch checked out in the worktree, or null when detached/unknown.
@@ -6373,7 +7486,7 @@ export const WorktreeArchiveBranchDetectionSchema = z.object({
6373
7486
  mergeState: z.enum(["merged", "unmerged", "unknown"]),
6374
7487
  // Commits on the branch not contained in the base ref; null when unknown.
6375
7488
  unmergedCommitCount: z.number().int().nonnegative().nullable(),
6376
- // A matching origin/<branch> exists deleting the local branch keeps the
7489
+ // A matching origin/<branch> exists - deleting the local branch keeps the
6377
7490
  // remote copy. Purely informational for the confirmation copy.
6378
7491
  hasRemoteBranch: z.boolean(),
6379
7492
  // The branch is checked out in another worktree too, so git will refuse to
@@ -6490,6 +7603,106 @@ export const FetchAgentTimelineResponseMessageSchema = z.object({
6490
7603
  error: z.string().nullable(),
6491
7604
  }),
6492
7605
  });
7606
+ export const ProviderSubagentDescriptorPayloadSchema = z.object({
7607
+ id: z.string(),
7608
+ parentAgentId: z.string(),
7609
+ provider: AgentProviderSchema,
7610
+ title: z.string().nullable(),
7611
+ description: z.string().nullable(),
7612
+ status: z.enum(["running", "completed", "failed", "canceled"]),
7613
+ createdAt: z.string(),
7614
+ updatedAt: z.string(),
7615
+ toolCallId: z.string().nullable(),
7616
+ cwd: z.string().nullable().optional(),
7617
+ });
7618
+ export const ProviderSubagentListResponseMessageSchema = z.object({
7619
+ type: z.literal("agent.provider_subagents.list.response"),
7620
+ payload: z.object({
7621
+ requestId: z.string(),
7622
+ parentAgentId: z.string(),
7623
+ subagents: z.array(ProviderSubagentDescriptorPayloadSchema),
7624
+ error: z.string().nullable(),
7625
+ }),
7626
+ });
7627
+ export const ProviderSubagentTimelineResponseMessageSchema = z.object({
7628
+ type: z.literal("agent.provider_subagents.timeline.get.response"),
7629
+ payload: z.object({
7630
+ requestId: z.string(),
7631
+ parentAgentId: z.string(),
7632
+ subagentId: z.string(),
7633
+ provider: AgentProviderSchema.nullable(),
7634
+ direction: z.enum(["tail", "before", "after"]),
7635
+ epoch: z.string(),
7636
+ reset: z.boolean(),
7637
+ staleCursor: z.boolean(),
7638
+ gap: z.boolean(),
7639
+ window: z.object({
7640
+ minSeq: z.number().int().nonnegative(),
7641
+ maxSeq: z.number().int().nonnegative(),
7642
+ nextSeq: z.number().int().nonnegative(),
7643
+ }),
7644
+ hasOlder: z.boolean(),
7645
+ hasNewer: z.boolean(),
7646
+ rows: z.array(z.object({
7647
+ item: AgentTimelineItemPayloadSchema,
7648
+ timestamp: z.string(),
7649
+ seq: z.number().int().nonnegative(),
7650
+ })),
7651
+ error: z.string().nullable(),
7652
+ }),
7653
+ });
7654
+ export const ProviderSubagentUpdateMessageSchema = z.object({
7655
+ type: z.literal("agent.provider_subagents.update"),
7656
+ payload: z.discriminatedUnion("kind", [
7657
+ z.object({
7658
+ kind: z.literal("upsert"),
7659
+ subagent: ProviderSubagentDescriptorPayloadSchema,
7660
+ }),
7661
+ z.object({
7662
+ kind: z.literal("timeline"),
7663
+ parentAgentId: z.string(),
7664
+ subagentId: z.string(),
7665
+ provider: AgentProviderSchema,
7666
+ item: AgentTimelineItemPayloadSchema,
7667
+ timestamp: z.string(),
7668
+ seq: z.number().int().nonnegative(),
7669
+ epoch: z.string(),
7670
+ }),
7671
+ z.object({
7672
+ kind: z.literal("remove"),
7673
+ parentAgentId: z.string(),
7674
+ subagentId: z.string(),
7675
+ }),
7676
+ ]),
7677
+ });
7678
+ export const SetAgentTimelineSubscriptionResponseMessageSchema = z.object({
7679
+ type: z.literal("agent.timeline.set_subscription.response"),
7680
+ payload: z.object({
7681
+ agentIds: z.array(z.string()),
7682
+ requestId: z.string(),
7683
+ }),
7684
+ });
7685
+ export const AgentAttentionRequiredMessageSchema = z.object({
7686
+ type: z.literal("agent_attention_required"),
7687
+ payload: z.object({
7688
+ agentId: z.string(),
7689
+ reason: z.enum(["finished", "error", "permission"]),
7690
+ timestamp: z.string(),
7691
+ shouldNotify: z.boolean(),
7692
+ notification: z
7693
+ .object({
7694
+ title: z.string(),
7695
+ body: z.string(),
7696
+ data: z.object({
7697
+ serverId: z.string(),
7698
+ workspaceId: z.string().optional(),
7699
+ agentId: z.string(),
7700
+ reason: z.enum(["finished", "error", "permission"]),
7701
+ }),
7702
+ })
7703
+ .optional(),
7704
+ }),
7705
+ });
6493
7706
  export const AgentForkContextResponseMessageSchema = z.object({
6494
7707
  type: z.literal("agent.fork_context.response"),
6495
7708
  payload: z.object({
@@ -6498,6 +7711,7 @@ export const AgentForkContextResponseMessageSchema = z.object({
6498
7711
  attachment: TextAttachmentSchema.nullable(),
6499
7712
  itemCount: z.number().int().nonnegative(),
6500
7713
  boundaryMessageId: z.string().nullable(),
7714
+ boundaryCursor: AgentTimelineCursorSchema.nullable().optional(),
6501
7715
  error: z.string().nullable(),
6502
7716
  }),
6503
7717
  });
@@ -6509,7 +7723,7 @@ export const AgentQueueRemoveResponseMessageSchema = z.object({
6509
7723
  /**
6510
7724
  * The removed message's text, handed back so the composer can put it back
6511
7725
  * in the box for editing or re-send it right away. Null when the id was
6512
- * already gone the turn drained it while the tap was in flight.
7726
+ * already gone - the turn drained it while the tap was in flight.
6513
7727
  * Attachments are not echoed: the client that queued the message keeps its
6514
7728
  * own local copy keyed by `id` (see the composer's queued-attachment
6515
7729
  * sidecar), and a client that never queued it has nothing to restore.
@@ -6558,6 +7772,7 @@ export const CancelAgentResponseMessageSchema = z.object({
6558
7772
  // can say "nothing to stop" instead of silently no-oping. Purely additive;
6559
7773
  // absent ⇒ unknown (old daemon). See docs/agent-lifecycle.md (Item 2).
6560
7774
  cancelled: z.boolean().optional(),
7775
+ error: z.string().nullable().optional(),
6561
7776
  }),
6562
7777
  });
6563
7778
  export const ClearAgentAttentionResponseMessageSchema = z.object({
@@ -6603,7 +7818,7 @@ export const SendAgentMessageResponseMessageSchema = z.object({
6603
7818
  error: z.string().nullable(),
6604
7819
  // Set when the message was parked for the next turn rather than dispatched
6605
7820
  // (`delivery: "queue"` against a busy agent). `queuedMessageId` is the
6606
- // entry's id in `AgentSnapshotPayload.queuedMessages` the key the sender
7821
+ // entry's id in `AgentSnapshotPayload.queuedMessages` - the key the sender
6607
7822
  // uses to find its own entry again. Absent ⇒ dispatched now (or old daemon).
6608
7823
  // COMPAT(steerQueue): added in v0.6.8, drop the gate when floor >= v0.6.8.
6609
7824
  queued: z.boolean().optional(),
@@ -6766,6 +7981,37 @@ export const DaemonGetStatusResponseSchema = z.object({
6766
7981
  })
6767
7982
  .passthrough(),
6768
7983
  });
7984
+ export const HubRelationshipStatusSchema = z.object({
7985
+ state: z.enum([
7986
+ "not_connected",
7987
+ "connecting",
7988
+ "connected",
7989
+ "reconnecting",
7990
+ "disconnecting",
7991
+ "revoked",
7992
+ ]),
7993
+ daemonId: z.string().nullable(),
7994
+ hubOrigin: z.string().nullable(),
7995
+ scopes: z.array(z.string()),
7996
+ connectedAt: z.string().nullable(),
7997
+ lastError: z.string().nullable(),
7998
+ });
7999
+ export const HubManagementDaemonConnectResponseSchema = z.object({
8000
+ type: z.literal("hub.management.daemon.connect.response"),
8001
+ payload: z.object({ requestId: z.string(), status: HubRelationshipStatusSchema }),
8002
+ });
8003
+ export const HubManagementDaemonGetStatusResponseSchema = z.object({
8004
+ type: z.literal("hub.management.daemon.get_status.response"),
8005
+ payload: z.object({ requestId: z.string(), status: HubRelationshipStatusSchema }),
8006
+ });
8007
+ export const HubManagementDaemonDisconnectResponseSchema = z.object({
8008
+ type: z.literal("hub.management.daemon.disconnect.response"),
8009
+ payload: z.object({
8010
+ requestId: z.string(),
8011
+ status: HubRelationshipStatusSchema,
8012
+ warning: z.string().optional(),
8013
+ }),
8014
+ });
6769
8015
  export const DaemonGetPairingOfferResponseSchema = z.object({
6770
8016
  type: z.literal("daemon.get_pairing_offer.response"),
6771
8017
  payload: z
@@ -6979,8 +8225,7 @@ const CheckoutPrGithubRepositoryPolicySchema = z
6979
8225
  rebaseMergeAllowed: false,
6980
8226
  viewerDefaultMergeMethod: null,
6981
8227
  });
6982
- const CheckoutPrGithubStatusSchema = z
6983
- .object({
8228
+ const CheckoutPrGithubStatusObjectSchema = z.object({
6984
8229
  mergeStateStatus: z.string().nullable().optional().default(null),
6985
8230
  autoMergeRequest: CheckoutPrGithubAutoMergeRequestSchema,
6986
8231
  viewerCanEnableAutoMerge: z.boolean().optional().default(false),
@@ -6990,9 +8235,23 @@ const CheckoutPrGithubStatusSchema = z
6990
8235
  repository: CheckoutPrGithubRepositoryPolicySchema,
6991
8236
  isMergeQueueEnabled: z.boolean().optional().default(false),
6992
8237
  isInMergeQueue: z.boolean().optional().default(false),
6993
- })
6994
- .optional();
8238
+ });
8239
+ const CheckoutPrGithubStatusSchema = CheckoutPrGithubStatusObjectSchema.optional();
8240
+ // The open facts envelope for forge-specific PR facts. Permanent - non-GitHub
8241
+ // forges deliver their native facts through it. The transitional piece is the
8242
+ // `github` mirror above, which stays populated for clients predating this
8243
+ // envelope; see COMPAT(forgeSpecific) in status-projection.ts for the shim.
8244
+ //
8245
+ // NOTE: `forgeSpecific.forge` is a FACTS-FAMILY tag, not the workspace brand id.
8246
+ // The whole Gitea family (gitea, forgejo, codeberg) emits `forge: "gitea"` here
8247
+ // because they share one facts shape, while the top-level `forge` above carries
8248
+ // the specific brand. Validation of family-specific payloads happens at runtime
8249
+ // in the consumer that knows that forge family.
8250
+ const CheckoutPrForgeSpecificSchema = z.unknown().optional();
6995
8251
  export const CheckoutPrStatusSchema = z.object({
8252
+ // COMPAT(forge): added in v0.1.106, remove the default after 2026-12-27 once daemon floor >= v0.1.106.
8253
+ forge: z.string().optional().default("github"),
8254
+ projectPath: z.string().optional(),
6996
8255
  number: z.number().optional(),
6997
8256
  url: z.string(),
6998
8257
  title: z.string(),
@@ -7025,6 +8284,14 @@ export const CheckoutPrStatusSchema = z.object({
7025
8284
  github: CheckoutPrGithubStatusSchema,
7026
8285
  // Provider-neutral per-PR hosting facts. Absent from old daemons; for
7027
8286
  // GitHub projects both this and the legacy `github` field are populated.
8287
+ //
8288
+ // NOT a shim, and deliberately untagged: this does not collapse into `forge`.
8289
+ // Otto registers every hosting provider into the forge registry under the
8290
+ // forge id `github`, which is the provider-routing facade rather than the gh
8291
+ // CLI (bootstrap.ts, `createGitHostingResolver`). So `forge` reads "github"
8292
+ // for a Bitbucket workspace and only `hosting.provider` carries the truth.
8293
+ // The two disagree by design; a reader wanting the real provider must use
8294
+ // this field.
7028
8295
  hosting: z
7029
8296
  .object({
7030
8297
  provider: GitHostingProviderIdWireSchema,
@@ -7038,7 +8305,9 @@ export const CheckoutPrStatusSchema = z.object({
7038
8305
  .optional(),
7039
8306
  })
7040
8307
  .optional(),
8308
+ forgeSpecific: CheckoutPrForgeSpecificSchema,
7041
8309
  });
8310
+ export const ForgeAuthStateSchema = z.unknown().optional();
7042
8311
  const CheckoutPrStatusPayloadSchema = z.object({
7043
8312
  cwd: z.string(),
7044
8313
  status: CheckoutPrStatusSchema.nullable(),
@@ -7048,6 +8317,9 @@ const CheckoutPrStatusPayloadSchema = z.object({
7048
8317
  githubFeaturesEnabled: z.boolean(),
7049
8318
  // Provider-neutral enablement. Present even when status is null so clients
7050
8319
  // can drive search/create-PR affordances for the workspace's provider.
8320
+ // Permanent for the same reason as the `hosting` block on the PR status
8321
+ // schema above: `forge` is a routing-facade id and cannot answer "which
8322
+ // provider is this really".
7051
8323
  hosting: z
7052
8324
  .object({
7053
8325
  provider: GitHostingProviderIdWireSchema,
@@ -7055,6 +8327,13 @@ const CheckoutPrStatusPayloadSchema = z.object({
7055
8327
  capabilities: GitHostingCapabilitiesSchema.optional(),
7056
8328
  })
7057
8329
  .optional(),
8330
+ // COMPAT(forgeAuthState): added in v0.1.106, remove after 2026-12-27. Optional richer
8331
+ // signal that supersedes githubFeaturesEnabled. The legacy boolean stays for old clients
8332
+ // and may remain true for non-auth error payloads so old clients still show the error.
8333
+ // Drop the boolean once the daemon floor >= v0.1.106.
8334
+ authState: ForgeAuthStateSchema,
8335
+ // COMPAT(forge): added in v0.1.106, remove the default after 2026-12-27 once daemon floor >= v0.1.106.
8336
+ forge: z.string().optional().default("github"),
7058
8337
  error: CheckoutErrorSchema.nullable(),
7059
8338
  requestId: z.string(),
7060
8339
  });
@@ -7082,6 +8361,8 @@ const CheckoutDiffSubscriptionPayloadSchema = z.object({
7082
8361
  cwd: z.string(),
7083
8362
  files: z.array(ParsedDiffFileSchema),
7084
8363
  error: CheckoutErrorSchema.nullable(),
8364
+ // COMPAT(diffTooLarge): added in v0.2.4, keep optional until the daemon floor is v0.2.4.
8365
+ diffTooLarge: z.boolean().optional(),
7085
8366
  });
7086
8367
  export const SubscribeCheckoutDiffResponseSchema = z.object({
7087
8368
  type: z.literal("subscribe_checkout_diff_response"),
@@ -7232,7 +8513,7 @@ export const GitFileHistoryEntrySchema = z.object({
7232
8513
  authoredAt: z.number(),
7233
8514
  committerName: z.string(),
7234
8515
  committedAt: z.number(),
7235
- // The file's name at this commit differs from the requested path across a
8516
+ // The file's name at this commit - differs from the requested path across a
7236
8517
  // rename. Diff requests must echo this one back, not the current name.
7237
8518
  path: z.string(),
7238
8519
  previousPath: z.string().optional(),
@@ -7263,7 +8544,7 @@ export const CheckoutGitFileCommitDiffResponseSchema = z.object({
7263
8544
  diff: z.string(),
7264
8545
  // Highlighted/parsed form of the same diff, when it parsed cleanly.
7265
8546
  structured: z.array(ParsedDiffFileSchema).optional(),
7266
- // The file's previous revision the diff's left-hand side, and the honest
8547
+ // The file's previous revision - the diff's left-hand side, and the honest
7267
8548
  // label for it. Absent when this revision created the file.
7268
8549
  previousSha: z.string().optional(),
7269
8550
  previousPath: z.string().optional(),
@@ -7378,6 +8659,18 @@ export const CheckoutPrMergeResponseSchema = z.object({
7378
8659
  requestId: z.string(),
7379
8660
  }),
7380
8661
  });
8662
+ export const CheckoutForgeSetAutoMergeResponseSchema = z.object({
8663
+ type: z.literal("checkout.forge.set_auto_merge.response"),
8664
+ payload: z.object({
8665
+ cwd: z.string(),
8666
+ enabled: z.boolean(),
8667
+ success: z.boolean(),
8668
+ error: CheckoutErrorSchema.nullable(),
8669
+ requestId: z.string(),
8670
+ }),
8671
+ });
8672
+ // COMPAT(githubAutoMergeRpc): added in v0.1.106, remove after 2026-12-28 once
8673
+ // all supported clients use checkout.forge.set_auto_merge.*.
7381
8674
  export const CheckoutGithubSetAutoMergeResponseSchema = z.object({
7382
8675
  type: z.literal("checkout.github.set_auto_merge.response"),
7383
8676
  payload: z.object({
@@ -7454,6 +8747,29 @@ export const PreviewStopResponseSchema = z.object({
7454
8747
  requestId: z.string(),
7455
8748
  }),
7456
8749
  });
8750
+ export const CheckoutCommitsListResponseSchema = z.object({
8751
+ type: z.literal("checkout.commits.list.response"),
8752
+ payload: z.object({
8753
+ cwd: z.string(),
8754
+ baseRef: z.string().nullable(),
8755
+ commits: z.array(CheckoutCommitSchema),
8756
+ error: CheckoutErrorSchema.nullable(),
8757
+ requestId: z.string(),
8758
+ }),
8759
+ });
8760
+ export const CheckoutCommitFileDiffResponseSchema = z.object({
8761
+ type: z.literal("checkout.commits.file_diff.response"),
8762
+ payload: z.object({
8763
+ cwd: z.string(),
8764
+ sha: z.string(),
8765
+ path: z.string(),
8766
+ // null when the file is absent from the commit or carries no textual diff
8767
+ // (e.g. binary-only changes).
8768
+ file: ParsedDiffFileSchema.nullable(),
8769
+ error: CheckoutErrorSchema.nullable(),
8770
+ requestId: z.string(),
8771
+ }),
8772
+ });
7457
8773
  const CheckoutGithubCheckAnnotationSchema = z.object({
7458
8774
  path: z.string().optional(),
7459
8775
  startLine: z.number().optional(),
@@ -7472,6 +8788,31 @@ const CheckoutGithubCheckJobSchema = z.object({
7472
8788
  logTail: z.string().optional(),
7473
8789
  logTruncated: z.boolean().optional(),
7474
8790
  });
8791
+ // Statuses stay open strings so future forge values cannot break parsing.
8792
+ const CheckoutPipelineJobSchema = z.object({
8793
+ id: z.number(),
8794
+ name: z.string(),
8795
+ stage: z.string(),
8796
+ status: z.string(),
8797
+ rawStatus: z.string(),
8798
+ url: z.string().nullable().optional().default(null),
8799
+ allowFailure: z.boolean().optional().default(false),
8800
+ durationSeconds: z.number().nullable().optional().default(null),
8801
+ });
8802
+ const CheckoutPipelineStageSchema = z.object({
8803
+ name: z.string(),
8804
+ status: z.string(),
8805
+ jobs: z.array(CheckoutPipelineJobSchema).optional().default([]),
8806
+ });
8807
+ const CheckoutPipelineSchema = z.object({
8808
+ id: z.number(),
8809
+ status: z.string(),
8810
+ rawStatus: z.string(),
8811
+ url: z.string().nullable().optional().default(null),
8812
+ ref: z.string().nullable().optional().default(null),
8813
+ sha: z.string().nullable().optional().default(null),
8814
+ stages: z.array(CheckoutPipelineStageSchema).optional().default([]),
8815
+ });
7475
8816
  export const CheckoutGithubCheckDetailsSchema = z.object({
7476
8817
  checkRunId: z.number(),
7477
8818
  workflowRunId: z.number().nullable().optional(),
@@ -7491,13 +8832,28 @@ export const CheckoutGithubCheckDetailsSchema = z.object({
7491
8832
  annotations: z.array(CheckoutGithubCheckAnnotationSchema).optional().default([]),
7492
8833
  failedJobs: z.array(CheckoutGithubCheckJobSchema).optional().default([]),
7493
8834
  truncated: z.boolean().optional().default(false),
8835
+ // No default: server CheckDetails keeps this optional and GitHub leaves it absent.
8836
+ pipeline: CheckoutPipelineSchema.nullable().optional(),
7494
8837
  });
8838
+ export const CheckoutCheckDetailsSchema = CheckoutGithubCheckDetailsSchema;
8839
+ export const CheckoutForgeGetCheckDetailsResponseSchema = z.object({
8840
+ type: z.literal("checkout.forge.get_check_details.response"),
8841
+ payload: z.object({
8842
+ cwd: z.string(),
8843
+ success: z.boolean(),
8844
+ details: CheckoutCheckDetailsSchema.nullable().optional().default(null),
8845
+ error: CheckoutErrorSchema.nullable(),
8846
+ requestId: z.string(),
8847
+ }),
8848
+ });
8849
+ // COMPAT(githubCheckDetailsRpc): added in v0.1.106, remove after 2026-12-28 once
8850
+ // all supported clients use checkout.forge.get_check_details.*.
7495
8851
  export const CheckoutGithubGetCheckDetailsResponseSchema = z.object({
7496
8852
  type: z.literal("checkout.github.get_check_details.response"),
7497
8853
  payload: z.object({
7498
8854
  cwd: z.string(),
7499
8855
  success: z.boolean(),
7500
- details: CheckoutGithubCheckDetailsSchema.nullable().optional().default(null),
8856
+ details: CheckoutCheckDetailsSchema.nullable().optional().default(null),
7501
8857
  error: CheckoutErrorSchema.nullable(),
7502
8858
  requestId: z.string(),
7503
8859
  }),
@@ -7557,6 +8913,16 @@ const PullRequestTimelineCommentItemSchema = z.object({
7557
8913
  // threads under their parent review. Absent on issue comments and on
7558
8914
  // timelines from daemons that predate the field.
7559
8915
  reviewId: z.string().optional(),
8916
+ // Forge-neutral discussion/thread id this comment belongs to, independent of a
8917
+ // file position. GitLab maps its discussion id here so general (non-file)
8918
+ // reply chains group into one thread; file-position threads also carry it.
8919
+ // Absent on standalone comments and on timelines from daemons that predate it.
8920
+ threadId: z.string().optional(),
8921
+ // Forge-neutral resolution state for a thread that has no file position, e.g. a
8922
+ // GitLab general (non-file) discussion that is resolvable. File-position threads
8923
+ // carry their resolution under `location.isResolved` instead. Absent on ordinary
8924
+ // comments, on forges that expose no thread resolution, and on older timelines.
8925
+ threadIsResolved: z.boolean().optional(),
7560
8926
  location: z
7561
8927
  .object({
7562
8928
  path: z.string(),
@@ -7592,6 +8958,10 @@ export const PullRequestTimelineResponseSchema = z.object({
7592
8958
  error: PullRequestTimelineErrorSchema.nullable().optional().default(null),
7593
8959
  requestId: z.string().optional().default(""),
7594
8960
  githubFeaturesEnabled: z.boolean().optional().default(true),
8961
+ // COMPAT(forgeAuthState): added in v0.1.106, remove after 2026-12-27. Optional richer
8962
+ // signal that supersedes githubFeaturesEnabled, mirroring CheckoutPrStatusPayloadSchema.
8963
+ // Drop the boolean once the daemon floor >= v0.1.106.
8964
+ authState: ForgeAuthStateSchema,
7595
8965
  })
7596
8966
  .optional()
7597
8967
  .prefault({}),
@@ -7681,14 +9051,29 @@ export const BranchSuggestionsResponseSchema = z.object({
7681
9051
  requestId: z.string(),
7682
9052
  }),
7683
9053
  });
9054
+ const ForgeSearchResponsePayloadSchema = z.object({
9055
+ items: z.array(z.unknown()),
9056
+ authState: z.unknown().optional(),
9057
+ error: z.string().nullable(),
9058
+ requestId: z.string(),
9059
+ });
9060
+ const GitHubSearchResponsePayloadSchema = z.object({
9061
+ items: z.array(z.unknown()),
9062
+ featuresEnabled: z.boolean().optional(),
9063
+ authState: z.unknown().optional(),
9064
+ githubFeaturesEnabled: z.boolean().optional(),
9065
+ error: z.string().nullable(),
9066
+ requestId: z.string(),
9067
+ });
9068
+ export const ForgeSearchResponseSchema = z.object({
9069
+ type: z.literal("forge.search.response"),
9070
+ payload: ForgeSearchResponsePayloadSchema,
9071
+ });
9072
+ // COMPAT(githubSearchRpc): added in v0.1.106, remove after 2026-12-28 once
9073
+ // clients use forge.search.*.
7684
9074
  export const GitHubSearchResponseSchema = z.object({
7685
9075
  type: z.literal("github_search_response"),
7686
- payload: z.object({
7687
- items: z.array(GitHubSearchItemSchema),
7688
- githubFeaturesEnabled: z.boolean(),
7689
- error: z.string().nullable(),
7690
- requestId: z.string(),
7691
- }),
9076
+ payload: GitHubSearchResponsePayloadSchema,
7692
9077
  });
7693
9078
  export const HostingSearchResponseSchema = z.object({
7694
9079
  type: z.literal("hosting.search.response"),
@@ -7769,6 +9154,64 @@ export const FileExplorerResponseSchema = z.object({
7769
9154
  requestId: z.string(),
7770
9155
  }),
7771
9156
  });
9157
+ export const FileSubscribeResponseSchema = z.object({
9158
+ type: z.literal("fs.file.subscribe.response"),
9159
+ payload: z.object({
9160
+ subscriptionId: z.string(),
9161
+ initial: FileVersionSchema,
9162
+ requestId: z.string(),
9163
+ }),
9164
+ });
9165
+ export const FileUnsubscribeResponseSchema = z.object({
9166
+ type: z.literal("fs.file.unsubscribe.response"),
9167
+ payload: z.object({
9168
+ subscriptionId: z.string(),
9169
+ requestId: z.string(),
9170
+ }),
9171
+ });
9172
+ export const FsFileWriteResultSchema = z.discriminatedUnion("status", [
9173
+ z.object({
9174
+ status: z.literal("written"),
9175
+ modifiedAt: z.string(),
9176
+ size: z.number(),
9177
+ revision: z.string().optional(),
9178
+ }),
9179
+ z.object({ status: z.literal("conflict"), version: FileVersionSchema }),
9180
+ z.object({ status: z.literal("error"), error: z.string() }),
9181
+ ]);
9182
+ export const FsFileWriteResponseSchema = z.object({
9183
+ type: z.literal("fs.file.write.response"),
9184
+ payload: z.object({
9185
+ result: FsFileWriteResultSchema,
9186
+ requestId: z.string(),
9187
+ }),
9188
+ });
9189
+ export const FsFileWriteBinaryResultSchema = z.discriminatedUnion("status", [
9190
+ z.object({
9191
+ status: z.literal("written"),
9192
+ modifiedAt: z.string(),
9193
+ size: z.number(),
9194
+ }),
9195
+ // The target is already there and the request did not ask to replace it.
9196
+ z.object({ status: z.literal("exists") }),
9197
+ z.object({ status: z.literal("error"), error: z.string() }),
9198
+ ]);
9199
+ export const FsFileWriteBinaryResponseSchema = z.object({
9200
+ type: z.literal("fs.file.write_binary.response"),
9201
+ payload: z.object({
9202
+ cwd: z.string(),
9203
+ path: z.string(),
9204
+ result: FsFileWriteBinaryResultSchema,
9205
+ requestId: z.string(),
9206
+ }),
9207
+ });
9208
+ export const FileUpdateSchema = z.object({
9209
+ type: z.literal("fs.file.update"),
9210
+ payload: z.object({
9211
+ subscriptionId: z.string(),
9212
+ version: FileVersionSchema,
9213
+ }),
9214
+ });
7772
9215
  const ProjectIconSchema = z.object({
7773
9216
  data: z.string(),
7774
9217
  mimeType: z.string(),
@@ -7865,7 +9308,7 @@ export const FileCreateResponseSchema = z.object({
7865
9308
  });
7866
9309
  /**
7867
9310
  * Delete outcome. `not_empty` means the target is a directory with children and
7868
- * the request did not set `recursive` nothing was removed, and the client can
9311
+ * the request did not set `recursive` - nothing was removed, and the client can
7869
9312
  * re-ask with the stronger confirmation.
7870
9313
  */
7871
9314
  export const FileDeleteResultSchema = z.discriminatedUnion("status", [
@@ -7998,7 +9441,7 @@ export const CodeDefinitionLocationSchema = z.object({
7998
9441
  /**
7999
9442
  * Which registry row answered (`typescript`, `csharp`, …). The multi-hit picker
8000
9443
  * shows it, so a user looking at two candidates can tell whether a language server
8001
- * resolved them or the name index guessed which changes how much to trust the
9444
+ * resolved them or the name index guessed - which changes how much to trust the
8002
9445
  * list. Absent from old daemons.
8003
9446
  */
8004
9447
  serverId: z.string().optional(),
@@ -8006,7 +9449,7 @@ export const CodeDefinitionLocationSchema = z.object({
8006
9449
  /**
8007
9450
  * Three-valued on purpose. `unavailable` (no server for this language on the host) and
8008
9451
  * `indexing` (the server is up but still building its project model) are different
8009
- * answers to the user, and neither is "not found" reporting either as an empty
9452
+ * answers to the user, and neither is "not found" - reporting either as an empty
8010
9453
  * result is how a working feature reads as broken.
8011
9454
  */
8012
9455
  export const CodeDefinitionStatusSchema = z.enum(["ok", "indexing", "unavailable"]);
@@ -8081,7 +9524,7 @@ export const CodeRenameEditSchema = z.object({
8081
9524
  newText: z.string(),
8082
9525
  /**
8083
9526
  * The text this edit expects to replace. Carried so the dry run can show what is being
8084
- * changed rather than only what it becomes and, on the daemon side, so the run can tell
9527
+ * changed rather than only what it becomes - and, on the daemon side, so the run can tell
8085
9528
  * that a file moved under the plan. For a rename this is always one identifier.
8086
9529
  */
8087
9530
  oldText: z.string().default(""),
@@ -8117,7 +9560,7 @@ export const CodeRenamePreviewResponseSchema = z.object({
8117
9560
  * workspace. Collapsing them into one failure is how "nothing happened" becomes unexplainable.
8118
9561
  */
8119
9562
  /**
8120
- * Whether the run HAPPENED deliberately not whether everything applied.
9563
+ * Whether the run HAPPENED - deliberately not whether everything applied.
8121
9564
  *
8122
9565
  * A run where two of fourteen edits no longer fit is still a run that took place, and the
8123
9566
  * twelve that landed are real. Collapsing that into a failure would hide them, and hiding a
@@ -8234,7 +9677,7 @@ export const LspServerStopResponseSchema = z.object({
8234
9677
  export const SolutionFormatSchema = z.enum(["sln", "slnx"]);
8235
9678
  /** One solution a workspace contains. Enough to populate the switcher's picker, nothing more. */
8236
9679
  export const SolutionRefSchema = z.object({
8237
- /** Workspace-relative, forward slashes the identity used by every later request. */
9680
+ /** Workspace-relative, forward slashes - the identity used by every later request. */
8238
9681
  path: z.string(),
8239
9682
  /** File name without the extension, which is what a .NET developer calls the solution. */
8240
9683
  name: z.string(),
@@ -8247,7 +9690,7 @@ export const CodeSolutionListResponseSchema = z.object({
8247
9690
  /**
8248
9691
  * Empty means the switcher never appears and the Files tab behaves exactly as it does today.
8249
9692
  * That is also what a disabled feature, a host with no .NET SDK, and a workspace with no
8250
- * solution all return the client has one silent case to handle, not four.
9693
+ * solution all return - the client has one silent case to handle, not four.
8251
9694
  */
8252
9695
  solutions: z.array(SolutionRefSchema),
8253
9696
  error: z.string().nullable(),
@@ -8277,8 +9720,8 @@ export const SolutionTreeProjectSchema = z.object({
8277
9720
  */
8278
9721
  path: z.string(),
8279
9722
  /**
8280
- * A project the solution names outside the workspace root. Shown and opened like any other
8281
- * the solution file is the authority naming it, so this is not free browsing but editing one
9723
+ * A project the solution names outside the workspace root. Shown and opened like any other -
9724
+ * the solution file is the authority naming it, so this is not free browsing - but editing one
8282
9725
  * warns, and it is absent from every git surface. See docs/solution-view.md.
8283
9726
  */
8284
9727
  outsideWorkspace: z.boolean(),
@@ -8296,7 +9739,7 @@ export const CodeSolutionGetTreeResponseSchema = z.object({
8296
9739
  format: SolutionFormatSchema.default("sln"),
8297
9740
  folders: z.array(SolutionTreeFolderSchema),
8298
9741
  projects: z.array(SolutionTreeProjectSchema),
8299
- /** Solution configurations and platforms first-class .NET concepts no CLI surfaces. */
9742
+ /** Solution configurations and platforms - first-class .NET concepts no CLI surfaces. */
8300
9743
  buildTypes: z.array(z.string()),
8301
9744
  platforms: z.array(z.string()),
8302
9745
  error: z.string().nullable(),
@@ -8333,7 +9776,7 @@ export const SolutionProjectNodeSchema = z.discriminatedUnion("kind", [
8333
9776
  name: z.string(),
8334
9777
  path: z.string(),
8335
9778
  outsideWorkspace: z.boolean(),
8336
- /** `Compile`, `Content`, `EmbeddedResource`, … MSBuild's own item type. */
9779
+ /** `Compile`, `Content`, `EmbeddedResource`, … - MSBuild's own item type. */
8337
9780
  itemType: z.string(),
8338
9781
  isImplicit: z.boolean(),
8339
9782
  }),
@@ -8389,7 +9832,7 @@ export const FileSearchResultEventSchema = z.object({
8389
9832
  cwd: z.string(),
8390
9833
  searchId: z.string(),
8391
9834
  path: z.string(),
8392
- /** File content hash at match time the replace precondition. */
9835
+ /** File content hash at match time - the replace precondition. */
8393
9836
  hash: z.string(),
8394
9837
  matches: z.array(FileSearchMatchSchema),
8395
9838
  }),
@@ -8759,7 +10202,72 @@ export const DaemonUpdateProgressMessageSchema = z.object({
8759
10202
  phase: z.enum(["starting", "downloading", "installing", "complete"]),
8760
10203
  }),
8761
10204
  });
10205
+ export const HubExecutionAgentCreateResponseSchema = z.object({
10206
+ type: z.literal("hub.execution.agent.create.response"),
10207
+ payload: z.object({
10208
+ requestId: z.string(),
10209
+ executionId: z.string(),
10210
+ agentId: z.string().nullable(),
10211
+ agent: AgentSnapshotPayloadSchema.nullable(),
10212
+ success: z.boolean(),
10213
+ error: z.string().nullable(),
10214
+ }),
10215
+ });
10216
+ export const HubExecutionControlResponseSchema = z.object({
10217
+ type: z.literal("hub.execution.control.response"),
10218
+ payload: z.object({
10219
+ requestId: z.string(),
10220
+ executionId: z.string(),
10221
+ action: HubExecutionControlActionSchema,
10222
+ success: z.boolean(),
10223
+ error: z.string().nullable(),
10224
+ }),
10225
+ });
10226
+ export const HubExecutionAgentUpdateSchema = z.object({
10227
+ type: z.literal("hub.execution.agent.update"),
10228
+ payload: z.object({
10229
+ executionId: z.string(),
10230
+ agentId: z.string(),
10231
+ agent: AgentSnapshotPayloadSchema,
10232
+ }),
10233
+ });
10234
+ export const HubExecutionAgentStreamSchema = z.object({
10235
+ type: z.literal("hub.execution.agent.stream"),
10236
+ payload: z.object({
10237
+ executionId: z.string(),
10238
+ agentId: z.string(),
10239
+ event: AgentStreamEventPayloadSchema,
10240
+ }),
10241
+ });
10242
+ export const HubExecutionOutboundMessageSchema = z.discriminatedUnion("type", [
10243
+ HubExecutionAgentCreateResponseSchema,
10244
+ HubExecutionControlResponseSchema,
10245
+ HubExecutionAgentUpdateSchema,
10246
+ HubExecutionAgentStreamSchema,
10247
+ ]);
10248
+ export class HubMessageCorrelationError extends Error {
10249
+ constructor(messageType) {
10250
+ super(`Hub message ${messageType} has mismatched agent correlation`);
10251
+ this.name = "HubMessageCorrelationError";
10252
+ }
10253
+ }
10254
+ export function parseHubExecutionOutboundMessage(value) {
10255
+ const message = HubExecutionOutboundMessageSchema.parse(value);
10256
+ const payload = message.payload;
10257
+ if ("agent" in payload &&
10258
+ payload.agent !== null &&
10259
+ "agentId" in payload &&
10260
+ payload.agentId !== null &&
10261
+ payload.agent.id !== payload.agentId) {
10262
+ throw new HubMessageCorrelationError(message.type);
10263
+ }
10264
+ return message;
10265
+ }
8762
10266
  export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [
10267
+ HubExecutionAgentCreateResponseSchema,
10268
+ HubExecutionControlResponseSchema,
10269
+ HubExecutionAgentUpdateSchema,
10270
+ HubExecutionAgentStreamSchema,
8763
10271
  BrowserAutomationExecuteRequestSchema,
8764
10272
  ActivityLogMessageSchema,
8765
10273
  AssistantChunkMessageSchema,
@@ -8778,6 +10286,8 @@ export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [
8778
10286
  ArtifactUpdateMessageSchema,
8779
10287
  AgentUpdateMessageSchema,
8780
10288
  WorkspaceUpdateMessageSchema,
10289
+ ProjectUpdateMessageSchema,
10290
+ ProjectListResponseMessageSchema,
8781
10291
  ScriptStatusUpdateMessageSchema,
8782
10292
  WorkspaceSetupProgressMessageSchema,
8783
10293
  WorkspaceSetupStatusResponseMessageSchema,
@@ -8790,8 +10300,14 @@ export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [
8790
10300
  ProjectAddResponseSchema,
8791
10301
  ProjectScaffoldResponseSchema,
8792
10302
  ProjectScaffoldProgressSchema,
10303
+ ProjectCreateDirectoryResponseSchema,
8793
10304
  OpenProjectResponseMessageSchema,
10305
+ WorkspaceGithubSearchRepositoriesResponseSchema,
10306
+ ProjectGithubCloneResponseSchema,
8794
10307
  StartWorkspaceScriptResponseMessageSchema,
10308
+ WorkspaceScriptListResponseMessageSchema,
10309
+ WorkspaceScriptStartResponseMessageSchema,
10310
+ WorkspaceScriptStopResponseMessageSchema,
8795
10311
  LegacyListAvailableEditorsResponseMessageSchema,
8796
10312
  LegacyOpenInEditorResponseMessageSchema,
8797
10313
  ArchiveWorkspaceResponseMessageSchema,
@@ -8801,6 +10317,11 @@ export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [
8801
10317
  WorktreeReattachResponseSchema,
8802
10318
  FetchAgentResponseMessageSchema,
8803
10319
  FetchAgentTimelineResponseMessageSchema,
10320
+ ProviderSubagentListResponseMessageSchema,
10321
+ ProviderSubagentTimelineResponseMessageSchema,
10322
+ ProviderSubagentUpdateMessageSchema,
10323
+ SetAgentTimelineSubscriptionResponseMessageSchema,
10324
+ AgentAttentionRequiredMessageSchema,
8804
10325
  AgentForkContextResponseMessageSchema,
8805
10326
  CancelAgentResponseMessageSchema,
8806
10327
  ClearAgentAttentionResponseMessageSchema,
@@ -8810,10 +10331,16 @@ export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [
8810
10331
  SetVoiceModeResponseMessageSchema,
8811
10332
  DaemonGetStatusResponseSchema,
8812
10333
  DaemonGetPairingOfferResponseSchema,
10334
+ HubManagementDaemonConnectResponseSchema,
10335
+ HubManagementDaemonGetStatusResponseSchema,
10336
+ HubManagementDaemonDisconnectResponseSchema,
8813
10337
  DiagnosticsResponseSchema,
8814
10338
  GetDaemonConfigResponseMessageSchema,
8815
10339
  SetDaemonConfigResponseMessageSchema,
8816
10340
  ConnectorsListToolsResponseSchema,
10341
+ ConnectorsOauthAuthorizeResponseSchema,
10342
+ ConnectorsOauthDisconnectResponseSchema,
10343
+ ConnectorsOauthStatusMessageSchema,
8817
10344
  SpeechSettingsGetOptionsResponseSchema,
8818
10345
  SpeechTtsPreviewResponseSchema,
8819
10346
  SpeechTtsSpeakResponseSchema,
@@ -8851,6 +10378,9 @@ export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [
8851
10378
  ProjectLinksUnsetResponseSchema,
8852
10379
  ProjectLinksChangedSchema,
8853
10380
  WorkspaceTitleSetResponseSchema,
10381
+ WorkspacePinSetResponseSchema,
10382
+ WorkspaceRecoveryInspectResponseSchema,
10383
+ WorkspaceRecoveryRestoreResponseSchema,
8854
10384
  WaitForFinishResponseMessageSchema,
8855
10385
  AgentPermissionRequestMessageSchema,
8856
10386
  AgentPermissionResolvedMessageSchema,
@@ -8880,6 +10410,14 @@ export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [
8880
10410
  BrainHfSearchResponseSchema,
8881
10411
  BrainHfQuantsResponseSchema,
8882
10412
  BrainModelsAddResponseSchema,
10413
+ BrainModelsInventoryResponseSchema,
10414
+ BrainModelProfileGetResponseSchema,
10415
+ BrainModelProfileSetResponseSchema,
10416
+ BrainModelBudgetGetResponseSchema,
10417
+ BrainModelLoadResponseSchema,
10418
+ BrainModelUnloadResponseSchema,
10419
+ BrainModelDeleteResponseSchema,
10420
+ BrainLogsTailResponseSchema,
8883
10421
  AgentArchivedMessageSchema,
8884
10422
  CloseItemsResponseSchema,
8885
10423
  CheckoutStatusResponseSchema,
@@ -8919,7 +10457,11 @@ export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [
8919
10457
  CheckoutRefreshResponseSchema,
8920
10458
  CheckoutPrCreateResponseSchema,
8921
10459
  CheckoutPrMergeResponseSchema,
10460
+ CheckoutForgeSetAutoMergeResponseSchema,
8922
10461
  CheckoutGithubSetAutoMergeResponseSchema,
10462
+ CheckoutCommitsListResponseSchema,
10463
+ CheckoutCommitFileDiffResponseSchema,
10464
+ CheckoutForgeGetCheckDetailsResponseSchema,
8923
10465
  CheckoutGithubGetCheckDetailsResponseSchema,
8924
10466
  PreviewListConfigResponseSchema,
8925
10467
  PreviewStartResponseSchema,
@@ -8934,6 +10476,7 @@ export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [
8934
10476
  StashListResponseSchema,
8935
10477
  ValidateBranchResponseSchema,
8936
10478
  BranchSuggestionsResponseSchema,
10479
+ ForgeSearchResponseSchema,
8937
10480
  GitHubSearchResponseSchema,
8938
10481
  HostingSearchResponseSchema,
8939
10482
  HostingAuthStatusResponseSchema,
@@ -8944,6 +10487,11 @@ export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [
8944
10487
  OttoWorktreeArchiveResponseSchema,
8945
10488
  CreateOttoWorktreeResponseSchema,
8946
10489
  FileExplorerResponseSchema,
10490
+ FileSubscribeResponseSchema,
10491
+ FileUnsubscribeResponseSchema,
10492
+ FsFileWriteResponseSchema,
10493
+ FsFileWriteBinaryResponseSchema,
10494
+ FileUpdateSchema,
8947
10495
  ProjectIconResponseSchema,
8948
10496
  FileDownloadTokenResponseSchema,
8949
10497
  FileUploadResponseSchema,
@@ -9063,8 +10611,11 @@ export const WSHelloMessageSchema = z.object({
9063
10611
  voice: z.boolean().optional(),
9064
10612
  pushNotifications: z.boolean().optional(),
9065
10613
  [CLIENT_CAPS.reasoningMergeEnum]: z.boolean().optional(),
10614
+ [CLIENT_CAPS.selectiveAgentTimeline]: z.boolean().optional(),
9066
10615
  [CLIENT_CAPS.customModeIcons]: z.boolean().optional(),
9067
10616
  [CLIENT_CAPS.terminalReflowableSnapshot]: z.boolean().optional(),
10617
+ [CLIENT_CAPS.providerSubagents]: z.boolean().optional(),
10618
+ [CLIENT_CAPS.projectUpdates]: z.boolean().optional(),
9068
10619
  [CLIENT_CAPS.browserHost]: BrowserAutomationHostCapabilitySchema.optional(),
9069
10620
  })
9070
10621
  .passthrough()