@maudecode/talaria-web-contracts 1.17.2 → 1.19.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 (92) hide show
  1. package/dist/gateway-session.d.ts +229 -0
  2. package/dist/gateway-session.js +46 -0
  3. package/dist/gateway-session.js.map +1 -0
  4. package/dist/index.d.ts +4 -0
  5. package/dist/index.js +4 -0
  6. package/dist/index.js.map +1 -1
  7. package/dist/openapi.js +9 -1
  8. package/dist/openapi.js.map +1 -1
  9. package/dist/router.d.ts +8256 -1763
  10. package/dist/routes/automation.d.ts +130 -25
  11. package/dist/routes/automation.js +8 -4
  12. package/dist/routes/automation.js.map +1 -1
  13. package/dist/routes/chat.d.ts +2484 -41
  14. package/dist/routes/chat.js +10 -4
  15. package/dist/routes/chat.js.map +1 -1
  16. package/dist/routes/core.d.ts +28 -6
  17. package/dist/routes/core.js +5 -2
  18. package/dist/routes/core.js.map +1 -1
  19. package/dist/routes/git.d.ts +76 -1
  20. package/dist/routes/git.js +13 -3
  21. package/dist/routes/git.js.map +1 -1
  22. package/dist/routes/raw.d.ts +3 -1
  23. package/dist/routes/raw.js +30 -6
  24. package/dist/routes/raw.js.map +1 -1
  25. package/dist/routes/sessions.d.ts +5181 -744
  26. package/dist/routes/sessions.js +76 -8
  27. package/dist/routes/sessions.js.map +1 -1
  28. package/dist/routes/settings.d.ts +457 -8
  29. package/dist/routes/settings.js +2 -2
  30. package/dist/routes/settings.js.map +1 -1
  31. package/dist/routes/tools.d.ts +1001 -33
  32. package/dist/routes/tools.js +82 -6
  33. package/dist/routes/tools.js.map +1 -1
  34. package/dist/routes/workspaces.d.ts +82 -0
  35. package/dist/routes/workspaces.js +18 -1
  36. package/dist/routes/workspaces.js.map +1 -1
  37. package/dist/run-status.d.ts +39 -0
  38. package/dist/run-status.js +13 -0
  39. package/dist/run-status.js.map +1 -0
  40. package/dist/sidecar/methods.d.ts +1299 -109
  41. package/dist/sidecar/methods.js +10 -2
  42. package/dist/sidecar/methods.js.map +1 -1
  43. package/dist/sidecar/namespaces.d.ts +1736 -123
  44. package/dist/sidecar/namespaces.js +332 -32
  45. package/dist/sidecar/namespaces.js.map +1 -1
  46. package/dist/sidecar/runtime.d.ts +61 -0
  47. package/dist/sidecar/runtime.js +20 -0
  48. package/dist/sidecar/runtime.js.map +1 -1
  49. package/dist/sidecar/version.d.ts +1 -1
  50. package/dist/sidecar/version.js +1 -1
  51. package/dist/speed.d.ts +14 -0
  52. package/dist/speed.js +6 -0
  53. package/dist/speed.js.map +1 -0
  54. package/dist/sse.d.ts +833 -14
  55. package/dist/sse.js +25 -9
  56. package/dist/sse.js.map +1 -1
  57. package/dist/theme.d.ts +10 -0
  58. package/dist/theme.js +10 -0
  59. package/dist/theme.js.map +1 -0
  60. package/dist/transcribe-stream.d.ts +34 -0
  61. package/dist/transcribe-stream.js +23 -0
  62. package/dist/transcribe-stream.js.map +1 -0
  63. package/dist/views.d.ts +2900 -159
  64. package/dist/views.js +380 -55
  65. package/dist/views.js.map +1 -1
  66. package/fixtures/sidecar/aux.json +134 -0
  67. package/fixtures/sidecar/chat.json +14 -0
  68. package/fixtures/sidecar/checkpoints.json +50 -0
  69. package/fixtures/sidecar/commands.json +258 -99
  70. package/fixtures/sidecar/config.json +15 -0
  71. package/fixtures/sidecar/connectors.json +218 -0
  72. package/fixtures/sidecar/cron.json +1008 -55
  73. package/fixtures/sidecar/gateway.json +39 -0
  74. package/fixtures/sidecar/goals.json +10 -8
  75. package/fixtures/sidecar/kanban.json +181 -90
  76. package/fixtures/sidecar/memory.json +50 -0
  77. package/fixtures/sidecar/models.json +122 -0
  78. package/fixtures/sidecar/personalities.json +92 -0
  79. package/fixtures/sidecar/plugins.json +262 -174
  80. package/fixtures/sidecar/process.json +31 -0
  81. package/fixtures/sidecar/profiles.json +12 -2
  82. package/fixtures/sidecar/providers.json +39 -13
  83. package/fixtures/sidecar/rpc.json +50 -1
  84. package/fixtures/sidecar/runtime.json +131 -22
  85. package/fixtures/sidecar/skills.json +256 -13
  86. package/fixtures/sidecar/state_db.json +34 -2
  87. package/fixtures/sidecar/stt.json +62 -1
  88. package/fixtures/sidecar/text.json +20 -2
  89. package/fixtures/sidecar/toolsets.json +468 -0
  90. package/fixtures/sidecar/usage.json +24 -4
  91. package/fixtures/sidecar/write_approval.json +98 -0
  92. package/package.json +1 -1
package/dist/views.js CHANGED
@@ -5,6 +5,12 @@
5
5
  * that must survive a round trip (documented passthrough).
6
6
  */
7
7
  import { z } from 'zod';
8
+ import { RunStatusSchema } from './run-status.js';
9
+ import { AgentRefreshConfirmationSchema } from './sidecar/runtime.js';
10
+ import { SpeedTierSchema } from './speed.js';
11
+ import { OwnerAttachmentSchema } from './gateway-session.js';
12
+ import { CanonicalBotChatSchema, GatewayModeSchema, PluginSettingFieldSchema, SkillCatalogIdentitySchema } from './sidecar/namespaces.js';
13
+ export { SpeedTierSchema } from './speed.js';
8
14
  const Json = z.unknown();
9
15
  export const NullableString = z.string().nullable();
10
16
  export const NullableNumber = z.number().nullable();
@@ -17,12 +23,26 @@ export const AttachmentSchema = z.looseObject({
17
23
  });
18
24
  /** The server's display class for a tool call: clients map it to an icon and localized verb, and never classify names themselves. */
19
25
  export const ToolKindSchema = z.enum(['shell', 'read', 'list', 'search', 'web', 'write', 'skill', 'memory', 'delegate', 'unknown']);
26
+ /**
27
+ * TAL-604: one row of a unified diff hunk as the server parsed it. `text` is the raw line, its `+` / `-` / space prefix kept;
28
+ * `old_line` / `new_line` number it on each side (null on the side it is absent from, and on a `\ No newline` marker).
29
+ */
30
+ export const DiffLineSchema = z.object({ kind: z.enum(['addition', 'deletion', 'context']), old_line: z.number().int().nullable(), new_line: z.number().int().nullable(), text: z.string() });
31
+ /**
32
+ * TAL-604: one hunk of a unified diff: its `@@` `header`, the first old and new line numbers, the last new line it covers
33
+ * (`new_end`, at least `new_start`), its added and removed line counts, and its lines. A diff without `@@` headers yields
34
+ * one hunk per file with an empty `header` and null numbers.
35
+ */
36
+ export const DiffHunkSchema = z.object({
37
+ header: z.string(), old_start: z.number().int().nullable(), new_start: z.number().int().nullable(), new_end: z.number().int().nullable(),
38
+ additions: z.number().int(), deletions: z.number().int(), lines: z.array(DiffLineSchema),
39
+ });
20
40
  /**
21
41
  * TAL-448: a completed file-edit call's change, from its result's unified diff: `added` / `removed` count the whole diff's
22
- * lines (file headers excluded); `diff` is the redacted diff, capped at 400 lines, with `truncated` when cut. Absent on a
23
- * call whose result has no diff.
42
+ * lines (file headers excluded); `diff` is the redacted diff, capped at 400 lines, with `truncated` when cut, and `hunks`
43
+ * that diff parsed (TAL-604). Absent on a call whose result has no diff.
24
44
  */
25
- export const ToolEditDiffSchema = z.object({ added: z.number().int(), removed: z.number().int(), diff: z.string(), truncated: z.boolean() });
45
+ export const ToolEditDiffSchema = z.object({ added: z.number().int(), removed: z.number().int(), diff: z.string(), truncated: z.boolean(), hunks: z.array(DiffHunkSchema) });
26
46
  /** Server-derived display fields every tool call carries: the kind, and the redacted first-line label of its main argument (`''` when none; omitted on a live frame with none, which keeps its start frame's target), and a file edit's `edit_diff`. */
27
47
  export const ToolDisplayFields = { kind: ToolKindSchema.optional(), target: z.string().optional(), edit_diff: ToolEditDiffSchema.optional() };
28
48
  /**
@@ -85,8 +105,8 @@ export const TurnFileChangeSchema = z.looseObject({ path: z.string(), action: z.
85
105
  export const TurnTerminalStateSchema = z.enum(['completed', 'no_response', 'cancelled', 'interrupted', 'tool_limit_reached', 'compression_exhausted', 'error']);
86
106
  export const ActivitySceneSchema = z.looseObject({
87
107
  version: z.literal('activity_scene_v1'), activity_rows: z.array(ActivitySceneRowSchema), final_answer: z.string().optional(),
88
- /** TAL-456: a settled final answer too long to lay out whole; clients render it collapsed, with a local "Show more". */
89
- final_answer_excerpt: z.string().optional(), turn_duration: z.number().nullable().optional(),
108
+ /** TAL-456: an older settled final answer too long to lay out whole. Absent on the session-global newest settled visible reply and throughout the next running turn. Clients render the excerpt with a local "Show more". */
109
+ final_answer_excerpt: z.string().optional().describe("Collapsed preview of an older settled final answer. Absent on the session-global newest visible reply; its exemption transfers only when a newer visible reply settles."), turn_duration: z.number().nullable().optional(),
90
110
  /** TAL-186: `final_answer` with its media references rewritten for display (see `_display_content`), and its media. */
91
111
  final_answer_display: z.string().optional(), final_answer_media: z.array(DisplayMediaSchema).optional(),
92
112
  terminal_state: z.string().optional().describe('The turn\'s outcome: a `TurnTerminalState` (`completed`, `no_response`, `error`, `cancelled`, `interrupted`, `tool_limit_reached`, ...), or `running` for the running turn of a run with no journal to replay (TAL-374). A running scene holds every persisted row (no final answer) and renders open, with no "Worked" fold or outcome; the live rows streamed after attach continue it, and the settled scene replaces both.'),
@@ -100,8 +120,13 @@ export const ActivitySceneSchema = z.looseObject({
100
120
  file_changes: z.array(TurnFileChangeSchema).optional(),
101
121
  activity_rows_total: z.number().int().optional(), activity_rows_offset: z.number().int().optional(), activity_rows_complete: z.boolean().optional(), activity_rows_omitted: z.number().int().optional(), activity_scene_ref: z.string().optional(),
102
122
  });
103
- /** TAL-460: one finished background item (see `_background_update.lines`). */
104
- export const BackgroundLineSchema = z.object({ kind: z.enum(['agent', 'command', 'other']), status: z.enum(['completed', 'failed', 'notice']), label: z.string(), exit_code: z.number().int().nullable().optional() });
123
+ /** The server-owned result-review lifecycle (TAL-1022). */
124
+ export const BackgroundReviewStateSchema = z.enum(['waiting', 'reviewing', 'reviewed', 'failed', 'unknown']);
125
+ /** One finished background item (see `_background_update.lines`). */
126
+ export const BackgroundLineSchema = z.object({ kind: z.enum(['agent', 'command', 'other']), status: z.enum(['completed', 'failed', 'notice']), label: z.string(), exit_code: z.number().int().nullable().optional(),
127
+ /** Server-composed worker and parent outcome. Failure/unknown worker outcomes take precedence. */
128
+ review_state: BackgroundReviewStateSchema.optional(),
129
+ });
105
130
  export const MessageSchema = z.looseObject({
106
131
  role: z.string(), content: MessageContentSchema.optional(), id: MessageIdSchema.optional(), message_id: MessageIdSchema.optional(), timestamp: z.number().nullable().optional(),
107
132
  attachments: z.array(AttachmentSchema).optional(), tool_calls: z.array(ToolCallSchema).optional(),
@@ -115,15 +140,23 @@ export const MessageSchema = z.looseObject({
115
140
  provider_details: Json.optional(), provider_details_label: z.string().optional(), recovery_control: Json.optional(), _anchor_activity_scene: ActivitySceneSchema.optional(),
116
141
  /** The turn this row belongs to; the server stamps every row it sends, so clients group turns by equality alone. */
117
142
  _turn_id: z.string().optional(),
143
+ /** On a turn's last assistant row: seconds the turn ran. */
144
+ _turnDuration: z.number().optional(),
145
+ /** On a turn's last assistant row: output tokens per second, when the turn reported usage. */
146
+ _turnTps: z.number().optional(),
147
+ /** Epoch seconds the row was written; `timestamp` when absent. */
148
+ _ts: z.number().optional(),
149
+ /** Reasoning titles an older Web stored on the row, in order. */
150
+ reasoning_titles: z.array(z.string()).optional(),
118
151
  /** The running turn's prompt, which the turn's settlement replaces with its persisted row. */
119
152
  _active_turn_user: z.boolean().optional(),
120
153
  /** A consumed steer at its causal place in the turn: display-only, never model history. A steer only the Agent recorded has no timing. */
121
154
  _steer: z.looseObject({ steer_id: z.string(), submitted_at: z.number().nullable().optional(), consumed_at: z.number().optional(), phase_duration: z.number().optional() }).optional(),
122
155
  /** On a steered turn's last reply: seconds from the last consumed steer to the turn's end. */
123
156
  _final_phase_duration: z.number().optional(),
124
- /** A settled body too long to lay out whole: clients render `_display_excerpt` collapsed, with a local "Show more" for `content`. */
125
- _display_truncated: z.boolean().optional(),
126
- _display_excerpt: z.string().optional(),
157
+ /** A settled body too long to lay out whole. The session-global newest settled visible assistant reply and running output carry no excerpt. Only a newer visible terminal answer transfers that exemption. Clients render `_display_excerpt` with a local "Show more" for full `content`. */
158
+ _display_truncated: z.boolean().optional().describe("Whether this settled body has a collapsed preview. The newest settled visible assistant reply and active-turn output remain full."),
159
+ _display_excerpt: z.string().optional().describe("Collapsed preview with full content retained. The newest settled visible assistant reply remains exempt throughout the next running turn, until a newer visible reply settles."),
127
160
  /**
128
161
  * TAL-452: a typed user message (never a steer, background update or marker) longer than 20 lines or 2,000
129
162
  * characters. Clients show it folded to a few lines with a local "Show more"; copy and edit use the whole `content`.
@@ -145,11 +178,13 @@ export const MessageSchema = z.looseObject({
145
178
  _background_update: z.object({
146
179
  kind: z.enum(['delegation', 'process', 'mixed', 'other']), attention: z.boolean(), count: z.number().int().positive(), summary: z.string(),
147
180
  /**
148
- * TAL-460: one completion line per finished item, shown in place of the row. Clients localize the wording per
149
- * `kind` and `status`: an agent by its goal (`completed` / `failed`), a command (`finished` / `failed` with
150
- * `exit_code`), or `label` as written for any other notice. `content` stays available in full on expansion.
181
+ * One completion line per finished item. Clients show its label and exactly one leading icon from `review_state`,
182
+ * with localized accessible/help wording. Parent review never overrides failed/unknown worker outcomes.
183
+ * `content` stays available in full on expansion.
151
184
  */
152
185
  lines: z.array(BackgroundLineSchema),
186
+ /** Parent review only, established from admission, the live run, and terminal evidence. */
187
+ review_state: BackgroundReviewStateSchema.optional(),
153
188
  }).optional(),
154
189
  /**
155
190
  * TAL-305: a marker the Agent wrote around context compaction, not a message anyone typed. Clients render a collapsed
@@ -158,10 +193,29 @@ export const MessageSchema = z.looseObject({
158
193
  _marker_kind: z.enum(['context_compaction', 'preserved_task_list']).optional(),
159
194
  /** TAL-305: a preserved task list's card body, without its marker line. */
160
195
  _marker_body: z.string().optional(),
196
+ /**
197
+ * TAL-815: an assistant row the Agent wrote as a notice, not the model's reply. `failed_turn`: the boundary closing a
198
+ * turn that failed, with its copy in `content`. Clients show it as a notice in its place, never as a reply bubble or
199
+ * part of a turn's reply; it is absent when Web's own error row already reports that turn. `session_finalize` is
200
+ * a plugin's closing text after clear, with role `local_notice`, display-only and excluded from model history.
201
+ */
202
+ _notice_kind: z.enum(['failed_turn', 'session_finalize']).optional(),
203
+ /**
204
+ * TAL-816: a user row of a messaging session that the Agent's gateway mirrored after a webhook route's or cron job's
205
+ * delivery to the chat, not a message the user sent. Clients show it as a delivery notice titled by a localized label
206
+ * per `kind` with `name`, and `body` (the delivered text, without its label line) beneath, never as a user bubble.
207
+ */
208
+ _delivery: z.object({ kind: z.enum(['webhook', 'cron']), name: z.string(), body: z.string() }).optional(),
161
209
  /** TAL-460: part of the Agent's reply to the background update just before it in the same turn. */
162
210
  _background_reply: z.boolean().optional(),
163
211
  /** TAL-460: that reply is only a silence marker; clients show the update's lines and nothing of this turn's reply. */
164
212
  _background_silent: z.boolean().optional(),
213
+ /**
214
+ * TAL-600: how clients show the row, on every row the server sends: `row` on its own, `in_scene` only inside its turn's
215
+ * activity scene (a tool result or a consumed steer), `hidden` not at all (a silent background reply, or a row with
216
+ * nothing to show).
217
+ */
218
+ _display: z.enum(['row', 'hidden', 'in_scene']).optional(),
165
219
  });
166
220
  export const ComposerDraftSchema = z.looseObject({ text: z.string().optional(), files: z.array(Json).optional() });
167
221
  /**
@@ -172,6 +226,23 @@ export const PendingSteerSchema = z.object({
172
226
  steer_id: z.string(), text: z.string(), submitted_at: z.number(), state: z.enum(['pending', 'sending_now']),
173
227
  actions: z.object({ edit: z.boolean(), cancel: z.boolean(), send_now: z.boolean() }),
174
228
  });
229
+ /**
230
+ * TAL-803: an out-of-band notice the Agent raised for the session (a memory provider that failed to load, credit and
231
+ * usage warnings), one per `key`. The server keeps it until the Agent clears that key, `expires_at` passes, or a new agent
232
+ * replaces the one that raised it.
233
+ */
234
+ export const SessionNoticeSchema = z.object({
235
+ key: z.string(), level: z.enum(['info', 'warn', 'error', 'success']), text: z.string(),
236
+ expires_at: NullableNumber.describe('Unix seconds when the server drops it; null when only the Agent clears it.'),
237
+ });
238
+ /**
239
+ * TAL-808: the session's standing `/goal` while it runs, from the Agent's goal state. `parked` is an active goal waiting on
240
+ * a process or a timer; `reason` is why it is paused or what it waits on. A done, cleared or absent goal is null.
241
+ */
242
+ export const SessionGoalSchema = z.object({
243
+ text: z.string(), status: z.enum(['active', 'paused', 'parked']), turns_used: z.number().int().nonnegative(), max_turns: z.number().int().nonnegative(),
244
+ reason: NullableString, parked_until: NullableNumber.describe('Unix seconds a timed park ends; null otherwise.'),
245
+ });
175
246
  /**
176
247
  * TAL-299: the context ring, computed by the server. `context_used_tokens` is the post-compression estimate, else the last
177
248
  * prompt (never the cumulative `input_tokens`); `context_window_tokens` is the model's window. The percents are rounded
@@ -186,12 +257,13 @@ export const ContextUsageFields = {
186
257
  const ActiveTurnOriginSchema = z.enum(['user', 'background']).nullable().optional().describe('Who started the running turn; null while idle.');
187
258
  const IsStreamingSchema = z.boolean().describe('True only while the session\'s run is a live stream on this server.');
188
259
  const ActiveStreamIdSchema = NullableString.optional().describe('The live stream id; non-null only while `is_streaming` is true.');
260
+ const ReadOnlyReasonSchema = NullableString.optional().describe('Server-owned reason this session is display-only or its ownership cannot be verified.');
189
261
  const ReadOnlySchema = z.boolean().describe('The session cannot be modified from Web: a read-only import, a view-only subagent child, or a foreign session whose owner refuses claiming.');
190
- const CanBranchSchema = z.boolean().describe('Web may branch this session: never a subagent child, and a read-only session only when it is a cron run.');
191
- const CanPinSchema = z.boolean().describe('Web may pin or unpin this session: never a subagent child.');
192
- const CanArchiveSchema = z.boolean().describe('Web may archive or unarchive this session: never a subagent child.');
262
+ const CanBranchSchema = z.boolean().describe('Web may branch this session: never a subagent child or an ownership-locked session; other read-only sessions only when they are cron runs.');
263
+ const CanPinSchema = z.boolean().describe('Web may pin or unpin this session: never a subagent child or a session locked by canonical ownership or unreadable ownership storage.');
264
+ const CanArchiveSchema = z.boolean().describe('Web may archive or unarchive this session: never a subagent child or a session locked by canonical ownership or unreadable ownership storage.');
193
265
  const CanDeleteSchema = z.boolean().describe('Web may delete this session: never a read-only session (a read-only import, a view-only subagent child, or a foreign session whose owner refuses claiming).');
194
- const CanDuplicateSchema = z.boolean().describe('Web may duplicate this session: it has a WebUI copy and is not a subagent child.');
266
+ const CanDuplicateSchema = z.boolean().describe('Web may duplicate this session: it has a WebUI copy, is not a subagent child, and is not locked by canonical ownership or unreadable ownership storage.');
195
267
  // TAL-310: the server classifies every session's source once; clients file rows by it and never scan source markers.
196
268
  export const SourceKindSchema = z.enum(['webui', 'cli', 'messaging', 'cron', 'webhook', 'subagent', 'claude_code', 'kanban', 'api', 'other'])
197
269
  .describe('The session\'s source family. `is_cli_session` is true only for `cli` and `claude_code`.');
@@ -212,17 +284,27 @@ export const CompressionReferenceSchema = z.object({
212
284
  text: z.string(),
213
285
  after_message_index: z.number().int().nonnegative().nullable().describe('The full-transcript index of the row the card follows; null puts it above the transcript.'),
214
286
  });
287
+ /** TAL-454: the chat a `/branch` copied, with its redacted title. */
288
+ export const BranchedFromSchema = z.object({ session_id: SessionIdSchema, title: z.string() });
215
289
  /** Full session record from `GET /api/session` and mutations returning `session`. */
216
290
  export const SessionSchema = z.looseObject({
291
+ composer_state_key: z.string().optional().describe('Opaque profile-home and owner identity for device-local composer state; never an API session id.'),
217
292
  session_id: SessionIdSchema, title: z.string(), workspace: z.string().optional(), created_workspace: z.string().nullable().optional(), model: NullableString.optional(), model_provider: NullableString.optional(),
218
293
  model_option_id: NullableString.optional().describe('TAL-301: the catalog entry id the stored model/provider selects; null when none.'),
219
294
  messages: z.array(MessageSchema).optional(), tool_calls: z.array(ToolCallSchema).optional(), created_at: UnixSeconds.optional(), updated_at: UnixSeconds.optional(), last_message_at: NullableNumber.optional(),
220
295
  message_count: z.number().optional(), user_message_count: z.number().optional(), pinned: z.boolean().optional(), archived: z.boolean().optional(), project_id: NullableString.optional(), profile: NullableString.optional(),
221
296
  personality: NullableString.optional(), input_tokens: z.number().optional(), output_tokens: z.number().optional(), cache_read_tokens: z.number().optional(), cache_write_tokens: z.number().optional(),
222
- cache_hit_percent: NullableNumber.optional(), estimated_cost: NullableNumber.optional(), active_stream_id: ActiveStreamIdSchema, is_streaming: IsStreamingSchema, active_turn_origin: ActiveTurnOriginSchema, pending_steers: z.array(PendingSteerSchema).optional().describe('TAL-424: the active stream\'s pending steers, oldest first.'), has_pending_user_message: z.boolean().optional(),
297
+ cache_hit_percent: NullableNumber.optional(), estimated_cost: NullableNumber.optional(), active_stream_id: ActiveStreamIdSchema, is_streaming: IsStreamingSchema, active_turn_origin: ActiveTurnOriginSchema, pending_steers: z.array(PendingSteerSchema).optional().describe('TAL-424: the active stream\'s pending steers, oldest first.'),
298
+ run_status: RunStatusSchema.nullable().optional().describe('Current server-selected run presentation; null when no turn runs. Older servers omit it.'),
299
+ compression_started_at: NullableNumber.optional().describe('TAL-1006: when the compression clients show started (epoch seconds): the running manual job\'s, else the live run\'s open span; null when none runs.'),
300
+ notices: z.array(SessionNoticeSchema).optional().describe('TAL-803: the Agent\'s active notices for the session, oldest first.'),
301
+ goal: SessionGoalSchema.nullable().optional().describe('TAL-808: the standing /goal; null when none runs.'),
302
+ automatic_turns_held: z.boolean().optional().describe('TAL-899: the user pressed Stop, so no automatic turn (background wakeup or goal continuation) starts until their next message.'), has_pending_user_message: z.boolean().optional(),
223
303
  pending_user_message: NullableString.optional(), pending_attachments: z.array(AttachmentSchema).optional(), pending_started_at: NullableNumber.optional(), pending_user_source: NullableString.optional(),
224
304
  context_length: NullableNumber.optional(), threshold_tokens: NullableNumber.optional(), last_prompt_tokens: NullableNumber.optional(), post_compression_context_tokens_estimate: NullableNumber.optional(), ...ContextUsageFields,
225
- enabled_toolsets: z.array(z.string()).nullable().optional(), composer_draft: ComposerDraftSchema.optional(), is_cli_session: z.boolean().optional(), source_kind: SourceKindSchema, is_messaging_session: IsMessagingSessionSchema, sort_ts: SortTsSchema, read_only: ReadOnlySchema, can_branch: CanBranchSchema, can_pin: CanPinSchema, can_archive: CanArchiveSchema, can_delete: CanDeleteSchema, can_duplicate: CanDuplicateSchema, source_tag: NullableString.optional(),
305
+ enabled_toolsets: z.array(z.string()).nullable().optional(), composer_draft: ComposerDraftSchema.optional(), is_cli_session: z.boolean().optional(), source_kind: SourceKindSchema, is_messaging_session: IsMessagingSessionSchema, sort_ts: SortTsSchema, read_only: ReadOnlySchema, read_only_reason: ReadOnlyReasonSchema, owner_attachment: OwnerAttachmentSchema.optional(), can_branch: CanBranchSchema, can_pin: CanPinSchema, can_archive: CanArchiveSchema, can_delete: CanDeleteSchema, can_duplicate: CanDuplicateSchema, source_tag: NullableString.optional(),
306
+ can_compress: z.boolean().optional().describe('TAL-614: a manual compression would start now: writable, no running turn, and at least four model-history messages. Only the full session detail carries it; absent means no.'),
307
+ confirms_deep_truncate: z.boolean().optional().describe('TAL-929: this server holds a `session.truncate` that discards more than one user turn until it is confirmed; absent on older servers, which cut at once.'),
226
308
  source_label: NullableString.optional(), session_source: NullableString.optional(), raw_source: NullableString.optional(), parent_session_id: NullableString.optional(), worktree_path: NullableString.optional(),
227
309
  worktree_branch: NullableString.optional(), worktree_repo_root: NullableString.optional(), share_token: NullableString.optional(), share_url: NullableString.optional(), share_created_at: NullableNumber.optional(), manual_title: z.boolean().optional(),
228
310
  compression_anchor_summary: NullableString.optional(), compression_recovery: z.record(z.string(), Json).optional(), recommended_recovery_action: NullableString.optional(), compression_recovery_action: NullableString.optional(),
@@ -237,18 +319,25 @@ export const SessionSchema = z.looseObject({
237
319
  assistant_name: z.string().optional(),
238
320
  workspace_name: WorkspaceNameSchema,
239
321
  compression_reference: CompressionReferenceSchema.nullable().optional(),
322
+ branched_from: BranchedFromSchema.nullable().optional().describe('TAL-454: the chat this one was branched from while it still loads (archived included); null for a compression continuation, a deleted or missing parent, and every other chat.'),
240
323
  });
241
324
  export const SessionEnvelopeSchema = z.looseObject({ session: SessionSchema });
242
325
  /** Sidebar row from `GET /api/sessions`. */
243
326
  export const SessionRowSchema = z.looseObject({
244
327
  session_id: SessionIdSchema, title: z.string(), workspace: z.string().optional(), model: NullableString.optional(), created_at: UnixSeconds.optional(), updated_at: UnixSeconds.optional(), last_message_at: NullableNumber.optional(),
245
328
  message_count: z.number().optional(), pinned: z.boolean().optional(), archived: z.boolean().optional(), project_id: NullableString.optional(), profile: NullableString.optional(), is_streaming: IsStreamingSchema,
246
- is_cli_session: z.boolean().optional(), source_kind: SourceKindSchema, is_messaging_session: IsMessagingSessionSchema, sort_ts: SortTsSchema, cron_running: z.boolean().optional(), read_only: ReadOnlySchema, can_branch: CanBranchSchema, can_pin: CanPinSchema, can_archive: CanArchiveSchema, can_delete: CanDeleteSchema, can_duplicate: CanDuplicateSchema, attention: z.looseObject({ kind: z.string().optional(), count: z.number().optional() }).nullable().optional(),
329
+ is_cli_session: z.boolean().optional(), source_kind: SourceKindSchema, is_messaging_session: IsMessagingSessionSchema, sort_ts: SortTsSchema, cron_running: z.boolean().optional(), read_only: ReadOnlySchema, read_only_reason: ReadOnlyReasonSchema, owner_attachment: OwnerAttachmentSchema.optional(), can_branch: CanBranchSchema, can_pin: CanPinSchema, can_archive: CanArchiveSchema, can_delete: CanDeleteSchema, can_duplicate: CanDuplicateSchema, attention: z.looseObject({ kind: z.string().optional(), count: z.number().optional() }).nullable().optional(),
247
330
  source_tag: NullableString.optional(), source_label: NullableString.optional(), session_source: NullableString.optional(), raw_source: NullableString.optional(), parent_session_id: NullableString.optional(),
248
331
  active_stream_id: ActiveStreamIdSchema, share_token: NullableString.optional(), share_url: NullableString.optional(), worktree_branch: NullableString.optional(), match_type: z.enum(['title', 'metadata', 'content']).optional(), match_preview: NullableString.optional(),
249
332
  workspace_name: WorkspaceNameSchema,
250
333
  ...ContextUsageFields,
251
334
  });
335
+ /**
336
+ * TAL-785: whether the Agent's `state.db` could serve the Agent session rows. `corrupt`: SQLite reports the file
337
+ * damaged; `unavailable`: it could not be read for another reason (permissions, a lock, an I/O error); `outdated`: a
338
+ * readable database from an older hermes-agent. `reason` explains a degraded state.
339
+ */
340
+ export const AgentStorageSchema = z.object({ state: z.enum(['ok', 'corrupt', 'unavailable', 'outdated']), reason: z.string().nullable() });
252
341
  export const SessionsListSchema = z.looseObject({
253
342
  sessions: z.array(SessionRowSchema).describe('Canonical display order (TAL-306): pinned first, then active (streaming or a pending prompt), then newest `sort_ts`, then `session_id`. Clients keep it.'), sidebar_reference_sessions: z.array(SessionRowSchema), server_time: z.number(), server_tz: z.string(), active_profile: z.string(), all_profiles: z.boolean(), include_archived: z.boolean(),
254
343
  archived_count: z.number().int(), archived_webui_count: z.number().int(), archived_cli_count: z.number().int(), other_profile_count: z.number().int(), cli_count: z.number().int(), webui_session_count: z.number().int(),
@@ -256,10 +345,11 @@ export const SessionsListSchema = z.looseObject({
256
345
  // TAL-482: sidebar counts for the "Scheduled sessions" / "Webhook sessions" groups; `_truncated` means more exist than are listed.
257
346
  scheduled_session_count: z.number().int().nonnegative(), scheduled_sessions_truncated: z.boolean(),
258
347
  webhook_session_count: z.number().int().nonnegative(), webhook_sessions_truncated: z.boolean(),
348
+ agent_storage: AgentStorageSchema.describe('Agent session storage health; a degraded state explains an empty or missing Agent session list.'),
259
349
  archived_limit: z.number().int().nullable().optional(), archived_offset: z.number().int().optional(),
260
350
  });
261
351
  export const SessionStatusSchema = z.looseObject({
262
- session_id: SessionIdSchema, title: z.string().optional(), active_stream_id: ActiveStreamIdSchema, active_turn_origin: ActiveTurnOriginSchema, agent_running: IsStreamingSchema, is_streaming: IsStreamingSchema, read_only: ReadOnlySchema, message_count: z.number().optional(), model: NullableString.optional(),
352
+ session_id: SessionIdSchema, title: z.string().optional(), active_stream_id: ActiveStreamIdSchema, active_turn_origin: ActiveTurnOriginSchema, agent_running: IsStreamingSchema, is_streaming: IsStreamingSchema, read_only: ReadOnlySchema, read_only_reason: ReadOnlyReasonSchema, owner_attachment: OwnerAttachmentSchema.optional(), message_count: z.number().optional(), model: NullableString.optional(),
263
353
  profile: NullableString.optional(), workspace: z.string().optional(), input_tokens: z.number().optional(), output_tokens: z.number().optional(), total_tokens: z.number().optional(), estimated_cost: NullableNumber.optional(), updated_at: UnixSeconds.optional(),
264
354
  });
265
355
  export const SessionUsageSchema = z.looseObject({ input_tokens: z.number().optional(), output_tokens: z.number().optional(), total_tokens: z.number().optional(), estimated_cost: NullableNumber.optional(), model: NullableString.optional() });
@@ -267,12 +357,20 @@ export const ProjectSchema = z.looseObject({ project_id: z.string(), name: z.str
267
357
  export const ProjectsSchema = z.looseObject({ projects: z.array(ProjectSchema), active_profile: z.string(), all_profiles: z.boolean(), other_profile_count: z.number().int() });
268
358
  export const SessionDeleteResultSchema = z.looseObject({ ok: z.literal(true), state_db_cleanup_failed: z.boolean().optional() });
269
359
  // ── chat ─────────────────────────────────────────────────────────────────
360
+ /** `voice_turn` (TAL-801): the turn was spoken (an App voice note), so the Agent runs it on `auxiliary.voice_chat`. */
270
361
  export const ChatStartRequestSchema = z.looseObject({
362
+ submission_id: z.string().min(1).max(128).optional().describe('Stable native-owner admission identity. Reuse only for an exact retry of the same submission.'),
271
363
  session_id: SessionIdSchema, message: z.string().optional(), model: z.string().nullable().optional(), model_provider: z.string().nullable().optional(), workspace: z.string().nullable().optional(), profile: z.string().nullable().optional(),
272
364
  explicit_model_pick: z.boolean().optional(), attachments: z.array(z.union([AttachmentSchema, z.string()])).optional(), moa_config: z.boolean().optional(), regenerate: z.boolean().optional(), regeneration_revision: z.string().optional(),
365
+ voice_turn: z.boolean().optional(),
366
+ /** TAL-818: this client renders `connect_card`, so a `manage_connections` call waits on the card; without it the Agent answers with plain links. */
367
+ connect_card: z.boolean().optional(),
368
+ /** The starting client renders the masked `skill_secret` prompt; omitted by older and native clients. */
369
+ skill_secret: z.boolean().optional(),
273
370
  });
274
371
  /** Accepted turn. `stream_id` is the owner identity for the SSE connection. */
275
372
  export const ChatStartResponseSchema = z.looseObject({
373
+ owner_attachment: OwnerAttachmentSchema.optional(), admission_id: z.string().optional(),
276
374
  stream_id: z.string().optional(), session_id: SessionIdSchema.optional(), turn_id: z.string().optional(), user_message_id: z.union([z.string(), z.number()]).nullable().optional(), pending_started_at: z.number().nullable().optional(),
277
375
  title: z.string().optional(), effective_model: z.string().optional(), effective_model_provider: z.string().optional(), queued: z.boolean().optional(), ok: z.boolean().optional(), status: z.string().optional(), reason: z.string().optional(), session: SessionSchema.optional(),
278
376
  });
@@ -297,14 +395,15 @@ export const SteerWithdrawRequestSchema = z.object({ session_id: SessionIdSchema
297
395
  /** `withdrawn: false` when the steer is unknown, already taken by the Agent (it then settles as consumed), or being sent. */
298
396
  export const SteerWithdrawResponseSchema = z.object({ withdrawn: z.boolean(), text: z.string().optional() });
299
397
  export const SteerSendNowRequestSchema = z.object({ session_id: SessionIdSchema, steer_id: z.string().min(1) });
300
- /** `redirected: false` when nothing is live to deliver it to now; the steer stays pending. */
301
- export const SteerSendNowResponseSchema = z.object({ redirected: z.boolean() });
398
+ /** The server decides why Send now did or did not deliver. `redirected` remains for older clients. */
399
+ export const SteerSendNowResponseSchema = z.object({ redirected: z.boolean(), outcome: z.enum(['delivered', 'next_step', 'not_running', 'taken', 'failed']) });
302
400
  /** `text` is delivered to the running agent; `display_text` is what the transcript shows. */
303
401
  export const SteerRequestSchema = z.looseObject({ session_id: SessionIdSchema, text: z.string().min(1), display_text: z.string().optional(), steer_id: z.string().optional() });
304
402
  /** `accepted: false` with a `fallback` reason means the message was not delivered; the caller keeps the draft. */
305
403
  /** TAL-460: a steer sent while a background turn runs starts the user's own turn instead; `started_turn` is its start response. */
306
404
  export const SteerResponseSchema = z.looseObject({ accepted: z.boolean(), fallback: NullableString.optional(), stream_id: NullableString.optional(), steer_id: z.string().optional(), started_turn: ChatStartResponseSchema.optional() });
307
405
  export const ApprovalPendingSchema = z.looseObject({
406
+ allowed_choices: z.array(z.enum(['once', 'session', 'always', 'deny'])).optional(), can_enable_yolo: z.boolean().optional(),
308
407
  approval_id: z.string().optional(), session_id: z.string().optional(), command: z.string().optional(), description: z.string().optional(), title: z.string().optional(), name: z.string().optional(), kind: z.string().optional(),
309
408
  reason: z.string().optional(), action: z.string().optional(), question: z.string().optional(), status: z.string().optional(), pending_count: z.number().optional(), run_id: z.string().optional(), mirror_token: z.string().optional(),
310
409
  });
@@ -317,14 +416,34 @@ export const ClarifyChoiceSchema = z.union([z.string(), z.looseObject({ label: z
317
416
  export const ClarifyStepSchema = z.looseObject({ qid: z.string(), question: z.string(), choices: z.array(z.string()), multi_select: z.boolean() });
318
417
  /** Keyed step answers; a multi-select step takes a list. The server shapes them into the Agent's reply. */
319
418
  export const ClarifyAnswersSchema = z.record(z.string(), z.union([z.string(), z.array(z.string())]));
419
+ /** TAL-887: one input of a password-manager prompt, answered under its `key`; a `secure` input is masked. */
420
+ export const VaultPromptFieldSchema = z.object({ key: z.enum(['master_password', 'identifier', 'password', 'code', 'secret']), secure: z.boolean() });
421
+ /**
422
+ * TAL-887: the Agent asks for a password manager's master password (`vault_unlock`), a login to save for the page it is
423
+ * on (`vault_save_login`), a one-time code (`vault_code`), or a skill environment secret (`skill_secret`). Clients answer through
424
+ * `/api/clarify/respond` with keyed `answers`, or `decline`; an empty answer, a decline, a cancel and `timeout_at` all
425
+ * resolve it unanswered. Answers never enter chat, logs, streams, or responses. Skill secrets are saved only to the invoking profile.
426
+ */
427
+ export const VaultPromptSchema = z.object({
428
+ kind: z.enum(['vault_unlock', 'vault_save_login', 'vault_code', 'skill_secret']),
429
+ backend: z.string().nullable().describe('The password manager (`vault_unlock`).'),
430
+ display_name: z.string().nullable().describe('Its display name (`vault_unlock`).'),
431
+ origin: z.string().nullable().describe('The page origin the login is saved for (`vault_save_login`).'),
432
+ site: z.string().nullable().describe('The site, by host (`vault_save_login`, `vault_code`).'),
433
+ hint: z.string().nullable().describe('Where the code was sent, when the Agent says (`vault_code`).'),
434
+ skill: z.object({ env_var: z.string(), skill_name: z.string(), help: z.string().nullable(), required_for: z.string().nullable() }).optional().describe('Profile-local environment setup metadata (`skill_secret`).'),
435
+ fields: z.array(VaultPromptFieldSchema),
436
+ timeout_at: z.number().describe('Unix seconds when the Agent stops waiting.'),
437
+ });
320
438
  export const ClarifyPendingSchema = z.looseObject({
321
439
  steps: z.array(ClarifyStepSchema).optional(),
440
+ vault: VaultPromptSchema.optional(),
322
441
  clarify_id: z.string().optional(), session_id: z.string().optional(), question: z.string().optional(), description: z.string().optional(), choices: z.array(ClarifyChoiceSchema).optional(), title: z.string().optional(),
323
442
  name: z.string().optional(), kind: z.string().optional(), reason: z.string().optional(), action: z.string().optional(), status: z.string().optional(), raw_preview: z.string().optional(), timeout_at: z.number().optional(),
324
443
  timeout_seconds: z.number().optional(), pending_count: z.number().optional(), index: z.number().optional(), total: z.number().optional(),
325
444
  });
326
445
  export const ClarifyPendingEnvelopeSchema = z.looseObject({ pending: ClarifyPendingSchema.nullable(), pending_count: z.number().int() });
327
- export const ClarifyRespondRequestSchema = z.looseObject({ session_id: SessionIdSchema, answers: ClarifyAnswersSchema.optional(), response: z.string().optional(), answer: z.string().optional(), choice: z.string().optional(), clarify_id: z.string().optional() });
446
+ export const ClarifyRespondRequestSchema = z.looseObject({ session_id: SessionIdSchema, answers: ClarifyAnswersSchema.optional(), response: z.string().optional(), answer: z.string().optional(), choice: z.string().optional(), clarify_id: z.string().optional(), decline: z.boolean().optional() });
328
447
  export const ClarifyRespondResponseSchema = z.looseObject({ ok: z.boolean(), response: z.string().optional(), error: z.string().optional(), stale: z.boolean().optional() });
329
448
  export const DraftSchema = z.looseObject({ text: z.string(), files: z.array(Json) });
330
449
  export const DraftResponseSchema = z.looseObject({ ok: z.literal(true), draft: DraftSchema, draft_version: z.string().nullable(), unchanged: z.boolean().optional() });
@@ -333,8 +452,6 @@ export const MAX_CHAT_ATTACHMENTS = 20;
333
452
  export const UploadResponseSchema = z.looseObject({ filename: z.string(), path: z.string(), size: z.number(), mime: z.string(), is_image: z.boolean().optional(), rollback_token: z.string().optional(), named_in_prompt: z.boolean().optional(), max_attachments_per_message: z.number().int().positive().optional() });
334
453
  export const GoalViewSchema = z.looseObject({ text: z.string().optional(), state: z.string().optional(), status: z.string().optional(), turns: z.number().optional(), max_turns: z.number().optional(), reason: z.string().optional() });
335
454
  export const GoalResponseSchema = z.looseObject({ ok: z.boolean().optional(), action: z.string().optional(), goal: GoalViewSchema.nullable().optional(), message: z.string().optional(), message_key: z.string().optional(), stream_id: z.string().optional(), status: z.string().optional(), reason: z.string().optional() });
336
- export const BackgroundResultSchema = z.looseObject({ id: z.string().optional(), task_id: z.string().optional(), status: z.string().optional(), title: z.string().optional(), summary: z.string().optional(), prompt: z.string().optional(), answer: NullableString.optional(), error: z.string().optional(), completed_at: z.number().nullable().optional() });
337
- export const BackgroundStatusSchema = z.looseObject({ results: z.array(BackgroundResultSchema) });
338
455
  /**
339
456
  * TAL-372: one piece of background work a session owns, the same record for every client, reload and restart.
340
457
  * - `task_id` is stable: the delegation unit's id, the process id, or the `/background` task id.
@@ -379,7 +496,15 @@ export const SessionNewRequestSchema = z.looseObject({
379
496
  title: z.string().optional(), workspace: z.string().optional(), workspace_inherited_from_prev_session: z.boolean().optional(), model: z.string().nullable().optional(), model_provider: z.string().nullable().optional(),
380
497
  profile: z.string().nullable().optional(), prev_session_id: z.string().optional(), project_id: z.string().nullable().optional(), worktree: Json.optional(), enabled_toolsets: z.array(z.string()).nullable().optional(), parent_session_id: z.string().optional(),
381
498
  });
382
- export const TranscribeCapabilitySchema = z.looseObject({ ok: z.literal(true), available: z.boolean(), provider: z.string() });
499
+ /**
500
+ * `streaming` (TAL-802): live dictation over `TRANSCRIBE_STREAM_PATH` would open for this profile (`stt.streaming` on, a live provider wire).
501
+ * `warm_supported` (TAL-916): a client starting a recording should `POST /api/transcribe/warm` so the engine loads while the user speaks.
502
+ */
503
+ export const TranscribeCapabilitySchema = z.looseObject({ ok: z.literal(true), available: z.boolean(), provider: z.string(), streaming: z.boolean(), warm_supported: z.boolean() });
504
+ /** TAL-906: what read-aloud speaks for a message; clients play it as is. */
505
+ export const SpeechPrepareSchema = z.object({ spoken_text: z.string() });
506
+ /** `POST /api/transcribe`: the recording's transcript; `filtered` (TAL-917) when the server emptied a phrase Whisper invents on silent audio. */
507
+ export const TranscribeResponseSchema = z.looseObject({ ok: z.literal(true), transcript: z.string(), filtered: z.boolean() });
383
508
  // ── settings, profiles, models, providers ───────────────────────────────
384
509
  /** TAL-411: the request profile's quota urgency thresholds. A save patches valid fields and clamps critical to at most warning. */
385
510
  export const QuotaThresholdsSchema = z.object({
@@ -391,7 +516,7 @@ export const SettingsSchema = z.looseObject({
391
516
  bot_name: z.string().optional(), default_model: z.string().optional(), default_workspace: z.string().optional(), language: z.string().optional(), send_key: z.string().optional(), font_size: z.string().optional(),
392
517
  full_width_chat: z.boolean().optional(), chat_width: z.enum(['comfortable', 'wide', 'full']).optional(), auto_scroll_follow: z.boolean().optional(), render_user_markdown: z.boolean().optional(), chat_activity_display_mode: z.string().optional(), default_message_mode: z.string().optional(),
393
518
  fade_text_effect: z.boolean().optional(), hidden_tabs: z.array(z.string()).optional(), composer_control_order: z.array(z.string()).optional(), show_cli_sessions: z.boolean().optional(), show_claude_code_sessions: z.boolean().optional(),
394
- show_cron_sessions: z.boolean().optional(), show_webhook_sessions: z.boolean().optional(), show_kanban_sessions: z.boolean().optional(), check_for_updates: z.boolean().optional(), ignore_agent_updates: z.boolean().optional(),
519
+ show_cron_sessions: z.boolean().optional(), show_webhook_sessions: z.boolean().optional(), show_kanban_sessions: z.boolean().optional(), check_for_updates: z.boolean().optional(), summarize_updates_automatically: z.boolean().optional(), ignore_agent_updates: z.boolean().optional(),
395
520
  auth_enabled: z.boolean().optional(), password_auth_enabled: z.boolean().optional(), password_env_var: z.boolean().optional(), passkeys_enabled: z.boolean().optional(), passwordless_enabled: z.boolean().optional(),
396
521
  auth_disabled_acknowledged: z.boolean().optional(), webui_version: z.string().optional(), agent_version: z.string().optional(), update_channel: z.string().optional(), agent_update_channel: z.enum(['stable', 'experimental']).optional(), update_channel_version: NullableString.optional(),
397
522
  max_tokens: NullableNumber.optional(), max_tokens_effective: NullableNumber.optional(), max_tokens_fallback: NullableNumber.optional(), tts_engine: z.string().optional(), tts_voice: z.string().optional(),
@@ -403,23 +528,61 @@ export const SettingsSchema = z.looseObject({
403
528
  export const ProfileSchema = z.looseObject({
404
529
  name: z.string(), path: z.string().optional(), is_active: z.boolean().optional(), is_default: z.boolean().optional(), model: NullableString.optional(), provider: NullableString.optional(), skill_count: z.number().optional(),
405
530
  total_skills: z.number().optional(), enabled_skills: z.number().optional(), gateway_running: z.boolean().optional(), has_env: z.boolean().optional(), visible: z.boolean().optional(),
531
+ /** TAL-805: the profile's gateway mode; `gateway_running` is true exactly for `own` and `served`. */
532
+ gateway_mode: GatewayModeSchema.optional(),
533
+ /** TAL-213: Bot Mode identity from `profile.yaml` (the bot's roster title, else the profile's display name), one bounded printable line each. */
534
+ display_name: z.string().optional(), description: z.string().optional(), has_avatar: z.boolean().optional(),
535
+ /** TAL-213: present only when the profile has a canonical Bot Chat; clients open `tip_session_id` after switching to the profile. */
536
+ canonical_session: CanonicalBotChatSchema.nullable().optional(),
537
+ /** TAL-819: skill and memory writes the Agent staged for approval, reviewed on the Skills and Memory pages. */
538
+ pending_writes: z.number().int().optional(),
406
539
  });
407
540
  export const ProfilesSchema = z.looseObject({ profiles: z.array(ProfileSchema), active: z.string(), single_profile_mode: z.boolean().optional() });
408
541
  export const ActiveProfileSchema = z.looseObject({ name: z.string(), path: z.string().optional(), is_default: z.boolean().optional(), default_workspace: NullableString.optional() });
409
542
  export const ProfileSwitchSchema = ProfilesSchema.extend({ is_default: z.boolean(), default_model: Json, default_model_provider: Json, default_workspace: z.string().nullable() });
410
543
  /** `/api/reasoning`: config.yaml agent.reasoning_effort / display.show_reasoning, resolved for a model. */
411
544
  export const ReasoningStatusSchema = z.looseObject({ show_reasoning: z.boolean().optional(), reasoning_effort: z.string().nullable().optional(), supported_efforts: z.array(z.string()).optional(), supports_reasoning_effort: z.boolean().optional(), supports_thinking_toggle: z.boolean().optional() });
412
- /** TAL-301: `provider_id`/`bare_id` are the server's split of `id`; clients match a stored `(model, provider)` against them and send `id` back. */
413
- export const ModelEntrySchema = z.looseObject({ id: z.string(), label: z.string().optional(), provider: z.string().optional(), provider_id: z.string().optional(), bare_id: z.string().optional(), supports_fast_tier: z.boolean().optional() });
414
- export const ModelGroupSchema = z.looseObject({ provider: z.string(), provider_id: z.string().optional(), models: z.array(ModelEntrySchema), extra_models: z.array(ModelEntrySchema).optional() });
415
- export const ModelsSchema = z.looseObject({ active_provider: NullableString.optional(), default_model: z.string().optional(), default_provider_id: NullableString.optional(), default_bare_id: z.string().optional(), default_option_id: NullableString.optional(), groups: z.array(ModelGroupSchema), aliases: z.record(z.string(), Json).optional(), configured_model_badges: z.record(z.string(), Json).optional() });
416
- /** `has_key` reports a credential; `configured` (TAL-570) is the row's setup status, which a keyless Ollama or LM Studio endpoint also meets. */
545
+ export const QuotaLevelSchema = z.enum(['healthy', 'warning', 'critical', 'stale', 'unavailable']);
546
+ /**
547
+ * TAL-301: `provider_id`/`bare_id` are the server's split of `id`; clients match a stored `(model, provider)` against them and send `id` back.
548
+ * TAL-807: `limit` is present while the provider's credential pool cannot serve this model until `resets_at` (ISO-8601 UTC).
549
+ */
550
+ export const ModelEntrySchema = z.looseObject({
551
+ id: z.string(), label: z.string().optional(), provider: z.string().optional(), provider_id: z.string().optional(), bare_id: z.string().optional(), supports_fast_tier: z.boolean().optional(), supported_speed_tiers: z.array(SpeedTierSchema).optional(),
552
+ limit: z.object({ scope: z.literal('model'), resets_at: z.string() }).optional(),
553
+ });
554
+ /** TAL-807: a group's rate limit: every pool account benched until `resets_at`, else the entry ids (`models`) cooling down until their reset. */
555
+ export const ModelGroupLimitSchema = z.discriminatedUnion('scope', [
556
+ z.object({ scope: z.literal('account'), resets_at: z.string() }),
557
+ z.object({ scope: z.literal('models'), models: z.record(z.string(), z.string()) }),
558
+ ]);
559
+ /**
560
+ * TAL-807: `limit` is the provider's live pool rate limit; `quota_level` is the TAL-411 `remaining` level of the tightest
561
+ * account-scope window in the provider's last account-usage read (a model-family window never counts), absent without one
562
+ * and while the group is account-limited.
563
+ */
564
+ export const CatalogAvailabilitySchema = z.object({ status: z.enum(['available', 'stale', 'unavailable']), authoritative: z.boolean(), unavailable_reason: NullableString, generation: z.string().min(1).describe('Opaque source generation; cached data from another generation must never be merged.') });
565
+ export const ModelGroupSchema = z.looseObject({
566
+ availability: CatalogAvailabilitySchema.optional(),
567
+ provider: z.string(), provider_id: z.string().optional(), models: z.array(ModelEntrySchema), extra_models: z.array(ModelEntrySchema).optional(),
568
+ limit: ModelGroupLimitSchema.optional(), quota_level: QuotaLevelSchema.optional(),
569
+ });
570
+ export const ModelsSchema = z.looseObject({ availability: CatalogAvailabilitySchema.optional(), active_provider: NullableString.optional(), default_model: z.string().optional(), default_provider_id: NullableString.optional(), default_bare_id: z.string().optional(), default_option_id: NullableString.optional(), groups: z.array(ModelGroupSchema), aliases: z.record(z.string(), Json).optional(), configured_model_badges: z.record(z.string(), Json).optional() });
571
+ export const ModelsLiveSchema = z.looseObject({ availability: CatalogAvailabilitySchema.optional(), provider: z.string().optional(), source: z.string().optional(), models: z.array(ModelEntrySchema), count: z.number().optional(), error: z.string().optional() });
572
+ /** TAL-603: where a card's credential comes from; `other` is a source the server cannot name, shown as its raw `key_source`. */
573
+ export const KeySourceKindSchema = z.enum(['env', 'oauth', 'config', 'pool', 'plugin', 'other']);
574
+ /**
575
+ * `has_key` reports a credential; `configured` (TAL-570) is the row's setup status, which a keyless Ollama or LM Studio endpoint also meets.
576
+ * TAL-603: `is_active` marks the active provider's card; `key_source_kind` is the key badge (`null` without a credential);
577
+ * `models_total` is the catalog size, never below `models.length`. `is_active` and `key_source_kind` are absent from older servers.
578
+ * TAL-641: `removable` marks a key Remove would clear: a built-in, not self-hosted, provider whose only key is its own profile `.env` variable.
579
+ */
417
580
  export const ProviderSchema = z.looseObject({
418
581
  id: z.string(), display_name: z.string().optional(), has_key: z.boolean().optional(), configured: z.boolean().optional(), configurable: z.boolean().optional(), is_oauth: z.boolean().optional(), is_plugin_provider: z.boolean().optional(), is_self_hosted: z.boolean().optional(),
419
582
  is_custom: z.boolean().optional(), key_source: z.string().optional(), base_url: NullableString.optional(), auth_error: NullableString.optional(), env_var: NullableString.optional(), models: z.array(ModelEntrySchema).optional(), models_total: z.number().optional(),
583
+ is_active: z.boolean().optional(), key_source_kind: KeySourceKindSchema.nullable().optional(), removable: z.boolean().optional(),
420
584
  });
421
- export const ProvidersSchema = z.looseObject({ providers: z.array(ProviderSchema), active_provider: NullableString.optional() });
422
- export const QuotaLevelSchema = z.enum(['healthy', 'warning', 'critical', 'stale', 'unavailable']);
585
+ export const ProvidersSchema = z.looseObject({ availability: CatalogAvailabilitySchema.optional(), providers: z.array(ProviderSchema), active_provider: NullableString.optional() });
423
586
  /** TAL-411: the server's classification per colour basis; `/api/provider/quotas` only, at the profile's thresholds. */
424
587
  export const QuotaUrgencySchema = z.looseObject({ remaining: QuotaLevelSchema, pace: QuotaLevelSchema.describe('Burn rate and pace tolerance; the remaining classification when the window has no pace.') });
425
588
  /** TAL-409: a window's pace as of the envelope's `computed_at`. Stale once `valid_until` (the window's reset) passes. */
@@ -444,6 +607,7 @@ export const QuotaWindowSchema = z.looseObject({
444
607
  });
445
608
  const QuotaWindowIndex = z.number().int().nullable().optional();
446
609
  export const QuotaSourceSchema = z.looseObject({
610
+ computed_at: z.string().optional().describe('Original reference time of this source, retained with stale windows.'),
447
611
  source_id: z.string(), provider_id: z.string().optional(), provider_label: z.string().optional(), account_label: z.string().optional(), status: z.string().optional(), supported: z.boolean().optional(), message: z.string().nullable().optional(),
448
612
  is_active_provider: z.boolean().optional(), quota: Json.optional(), windows: z.array(QuotaWindowSchema).optional(), balances: Json.optional(), plan: Json.optional(), details: Json.optional(), unavailable_reason: Json.optional(), retry_after: Json.optional(), fetched_at: Json.optional(),
449
613
  pace_window_index: QuotaWindowIndex.describe('The window a pace-coloured widget shows: the weekly one, else the first 5h/session window.'),
@@ -464,8 +628,8 @@ export const ProviderQuotaSchema = z.looseObject({
464
628
  account_limits: QuotaAccountLimitsSchema.nullable().optional(),
465
629
  });
466
630
  /** Python `get_provider_quotas`: the stable identity envelope the iOS quota widget persists (`scope_id`/`profile_id`). */
467
- export const ProviderQuotasSchema = z.looseObject({
468
- version: z.number(), computed_at: z.string().describe('ISO-8601 UTC reference time of every window `pace`.'), scope_id: z.string(), profile_id: z.string(), active_provider: NullableString, requested_source_id: NullableString,
631
+ export const ProviderQuotasSchema = z.looseObject({ availability: CatalogAvailabilitySchema.optional(),
632
+ version: z.number(), computed_at: z.string().describe('ISO-8601 UTC request reference time; coalesced or stale sources retain their own computed_at.'), scope_id: z.string(), profile_id: z.string(), active_provider: NullableString, requested_source_id: NullableString,
469
633
  missing_source: z.boolean().describe('True when `?source=` names a source id this scope no longer has; `sources` is then empty.'),
470
634
  sources: z.array(QuotaSourceSchema).describe('Each live source id exactly once, ordered by `provider_id`, then `account_label`, then `source_id`. Clients render this list as-is; a `?source=` read returns that one row.'),
471
635
  });
@@ -500,8 +664,12 @@ export const AuxiliaryTaskSchema = z.looseObject({
500
664
  selected_option_id: z.string().nullable(),
501
665
  /** False only for a pinned model the current catalog does not list; it stays visible and editable. */
502
666
  in_catalog: z.boolean(),
667
+ /** TAL-800: the slot a plugin task inherits its route from; while the task is Auto, `value_label` names that slot's model. */
668
+ inherit_from: z.string().nullable(),
669
+ /** TAL-800: the reasoning effort the slot runs with (the Agent's default until one is saved; `''` = provider default); null when the Agent does not report its slots. */
670
+ reasoning_effort: z.string().nullable(),
503
671
  });
504
- export const AuxiliaryModelsSchema = z.looseObject({ main: z.looseObject({ model: z.string().optional(), provider: z.string().optional(), base_url: z.string().optional(), api_key_set: z.boolean().optional() }), tasks: z.array(AuxiliaryTaskSchema) });
672
+ export const AuxiliaryModelsSchema = z.looseObject({ main: z.looseObject({ model: z.string().optional(), provider: z.string().optional(), base_url: z.string().optional(), api_key_set: z.boolean().optional(), service_tier: z.enum(['', 'default', 'priority', 'ultrafast']).optional().describe('Empty string is the Standard value returned by older servers.'), supported_speed_tiers: z.array(SpeedTierSchema).optional() }), tasks: z.array(AuxiliaryTaskSchema) });
505
673
  export const MaxTokensSchema = z.looseObject({ max_tokens: NullableNumber, max_tokens_effective: NullableNumber, max_tokens_fallback: NullableNumber });
506
674
  // ── workspaces, files, git ───────────────────────────────────────────────
507
675
  /** A registry entry; `name` is never empty (the folder name when none is registered, `Home` for `default`), so pickers show it as-is (TAL-303). */
@@ -521,16 +689,23 @@ export const FileContentSchema = z.looseObject({
521
689
  preview: FilePreviewKindSchema.optional(), table: z.array(z.array(z.string())).optional(), table_truncated: z.boolean().optional(),
522
690
  preview_url: z.string().optional(),
523
691
  });
524
- export const GitInfoSchema = z.looseObject({ git: z.looseObject({ is_git: z.boolean().optional(), branch: NullableString.optional(), dirty: z.number().optional(), modified: z.number().optional(), untracked: z.number().optional(), ahead: z.number().optional(), behind: z.number().optional() }).nullable().optional() });
692
+ const GitBadgeSchema = z.looseObject({ is_git: z.boolean().optional(), branch: NullableString.optional(), dirty: z.number().optional(), modified: z.number().optional(), untracked: z.number().optional(), ahead: z.number().optional(), behind: z.number().optional() });
693
+ export const GitInfoSchema = z.looseObject({ git: GitBadgeSchema.nullable().optional() });
525
694
  // ── tools ────────────────────────────────────────────────────────────────
526
- export const SkillSchema = z.looseObject({ name: z.string(), description: NullableString.optional(), category: NullableString.optional(), disabled: z.boolean().optional() });
695
+ export const SkillSchema = z.looseObject({ ...SkillCatalogIdentitySchema.shape, name: z.string(), description: NullableString.optional(), category: NullableString.optional(), disabled: z.boolean().optional() });
527
696
  export const SkillsSchema = z.looseObject({ skills: z.array(SkillSchema), categories: z.array(Json).optional() });
528
- export const SkillContentSchema = z.looseObject({ name: z.string().optional(), content: z.string().optional(), path: z.string().optional(), success: z.boolean().optional(), message: z.string().optional(), ok: z.boolean().optional() });
697
+ /**
698
+ * `linked_files` groups the skill directory's files by folder (`references`, `templates`, ...); open one with `?file=`.
699
+ * A linked file that is not UTF-8 text carries `binary: true` and its `size` instead of `content`.
700
+ */
701
+ export const SkillContentSchema = z.looseObject({ editable: z.boolean().optional(), name: z.string().optional(), content: z.string().optional(), path: z.string().optional(), linked_files: z.record(z.string(), z.array(z.string())).optional(), binary: z.literal(true).optional(), size: z.number().optional(), success: z.boolean().optional(), message: z.string().optional(), ok: z.boolean().optional() });
529
702
  export const SkillsUsageSchema = z.looseObject({ usage: z.record(z.string(), z.looseObject({ use_count: z.number().optional(), view_count: z.number().optional(), patch_count: z.number().optional() })), skill_names: z.array(z.string()).optional(), total_invocations: z.number().optional(), unique_skills_used: z.number().optional() });
530
703
  export const MemorySchema = z.looseObject({
531
704
  memory: z.string(), user: z.string(), soul: z.string(), project_context: z.string().optional(), memory_path: z.string().optional(), user_path: z.string().optional(), soul_path: z.string().optional(), project_context_path: z.string().optional(),
532
705
  project_context_name: z.string().optional(), project_context_workspace: z.string().optional(), memory_mtime: NullableNumber.optional(), user_mtime: NullableNumber.optional(), soul_mtime: NullableNumber.optional(), project_context_mtime: NullableNumber.optional(),
533
706
  project_context_shadowed: z.array(Json).optional(), external_notes_enabled: z.boolean().optional(),
707
+ /** TAL-784: the revision of each file as this read found it. A save sends the revision its edit started from. */
708
+ memory_revision: z.string(), user_revision: z.string(), soul_revision: z.string(),
534
709
  });
535
710
  export const PromptSchema = z.looseObject({ id: z.string().optional(), name: z.string().optional(), title: z.string().optional(), label: z.string().optional(), text: z.string().optional(), content: z.string().optional(), created_at: z.number().optional() });
536
711
  export const PromptsSchema = z.looseObject({ prompts: z.array(PromptSchema) });
@@ -540,12 +715,15 @@ export const PromptsSchema = z.looseObject({ prompts: z.array(PromptSchema) });
540
715
  * clients can run it, and `unsupported_message` is the English text a client outside `clients` shows when it is typed.
541
716
  * Clients suggest the entries listing them whose name or any alias starts with the typed text (case-insensitive), and
542
717
  * resolve a typed alias to the entry's `name`. `exec` marks an Agent entry `POST /api/commands/exec` runs: the client
543
- * posts the typed text there and shows the returned output instead of starting a chat turn (TAL-561).
718
+ * posts the typed text there with the open chat's `session_id` and shows the returned output instead of starting a chat turn
719
+ * (TAL-561); `/refine` reviews that chat (TAL-902).
720
+ * `handler: 'prompt'` marks an Agent prompt built-in: the client sends the typed text as a chat turn and the server starts the
721
+ * turn with the Agent's expansion of it (TAL-817).
544
722
  */
545
723
  export const CommandClientSchema = z.enum(['web', 'ios']);
546
724
  export const CommandRowSchema = z.looseObject({
547
725
  name: z.string(), description: z.string().optional(), aliases: z.array(z.string()), args_hint: z.string().optional(), category: z.string().optional(),
548
- handler: z.enum(['client', 'agent']), clients: z.array(CommandClientSchema), unsupported_message: z.string().optional(),
726
+ handler: z.enum(['client', 'agent', 'prompt']), clients: z.array(CommandClientSchema), unsupported_message: z.string().optional(),
549
727
  cli_only: z.boolean().optional(), gateway_only: z.boolean().optional(), subcommands: z.array(Json).optional(), exec: z.boolean().optional(),
550
728
  });
551
729
  export const CommandsSchema = z.looseObject({ commands: z.array(CommandRowSchema) });
@@ -557,23 +735,94 @@ export const InsightsSchema = z.looseObject({
557
735
  models: z.array(z.looseObject({ model: z.string().optional(), sessions: z.number().optional(), tokens: z.number().optional(), cost: z.number().optional() })).optional(),
558
736
  });
559
737
  export const DashboardStatusSchema = z.looseObject({ running: z.boolean(), enabled: z.string().optional(), url: z.string().optional(), browser_url: z.string().optional(), host: z.string().optional(), port: z.number().int().optional(), version: z.string().optional(), error: z.string().optional() });
560
- export const AgentHealthSchema = z.looseObject({ alive: z.boolean().nullable().optional(), checked_at: z.string().optional(), details: z.looseObject({ state: z.string().optional(), reason: z.string().optional() }).optional(), gateway_chat: z.looseObject({ enabled: z.boolean().optional(), backend: z.string().optional(), base_url_configured: z.boolean().optional(), api_key_configured: z.boolean().optional() }).optional(), error: z.string().optional() });
738
+ /**
739
+ * Gateway status for the Settings banner (TAL-266). `running` and `configured` come from the Agent health probe; when it
740
+ * is inconclusive, stale running metadata or known gateway sessions mark the gateway configured. `platforms` are the
741
+ * messaging platforms of known gateway sessions; `last_active` is the gateway session index's ISO mtime while running, else "".
742
+ * TAL-805: `gateway_mode` is the active profile's mode and decides `running` when it is not null. It is null for a remote
743
+ * gateway, or for the root profile when the sidecar cannot answer; a named profile whose mode is unconfirmed reads `stopped`.
744
+ */
745
+ export const GatewayStatusSchema = z.object({
746
+ running: z.boolean(), configured: z.boolean(), gateway_mode: GatewayModeSchema.nullable(), platforms: z.array(z.object({ name: z.string(), label: z.string() })), last_active: z.string(), session_count: z.number().int(),
747
+ health: z.object({ state: NullableString, reason: NullableString, gateway_state: NullableString }),
748
+ });
749
+ export const GatewayActionSchema = z.object({ ok: z.literal(true), action: z.enum(['start', 'stop', 'restart']), message: z.string(), status: GatewayStatusSchema });
750
+ export const DashboardConfigSchema = z.object({ enabled: z.enum(['auto', 'always', 'never']), url: z.string() });
751
+ /**
752
+ * Project OS dashboard for a repo found from the last workspace or the `board`'s default workdir (TAL-266). Every field is
753
+ * null or empty when the profile has no local workspace I/O or no repo is found. `onboarding.active` means the docs came
754
+ * from the repo root's `PROJECT.md`/`PLAN.md`/`STATUS.md` instead of `docs/project-os/`.
755
+ */
756
+ export const ProjectOsDashboardSchema = z.object({
757
+ workspace: NullableString, repo_root: NullableString, selected_board_slug: NullableString.optional(), git: GitBadgeSchema.nullable(),
758
+ docs: z.object({ project: FileContentSchema.nullable(), plan: FileContentSchema.nullable(), status: FileContentSchema.nullable(), blocker_resolver: FileContentSchema.nullable() }).partial(),
759
+ handoff: z.record(z.string(), Json).nullable(), active: z.record(z.string(), Json).nullable(), heartbeat: z.record(z.string(), Json).nullable(),
760
+ onboarding: z.looseObject({ active: z.boolean(), doc_source: z.enum(['project-os', 'root']) }).optional(), goal_summary: z.string(),
761
+ });
762
+ /** One server projection used by chat readiness, HTTP refusals, and persisted stream errors. */
763
+ export const AgentRuntimeMetadataSchema = z.object({
764
+ agent_runtime_reason: z.enum(['update_active', 'recovery_incomplete', 'source_changed', 'unknown']).optional(),
765
+ agent_update_state: z.string().optional(),
766
+ retryable: z.boolean().optional(),
767
+ restart_scheduled: z.boolean().optional(),
768
+ });
769
+ /** TAL-951: whether Web chat can run, from the Agent sidecar's status; `condition` says why not, null when available. */
770
+ export const ChatReadinessSchema = z.discriminatedUnion('available', [
771
+ z.object({ available: z.literal(true), condition: z.null() }),
772
+ z.object({ available: z.literal(false), condition: z.enum(['sidecar_unavailable', 'agent_incompatible', 'agent_runtime_stale']), ...AgentRuntimeMetadataSchema.shape }),
773
+ ]);
774
+ /** Messaging-gateway liveness (`alive`, `details`, `gateway_chat`) for Settings → System, plus Web chat readiness in `chat`. */
775
+ export const AgentHealthSchema = z.looseObject({ chat: ChatReadinessSchema.optional().describe('Always sent since TAL-951; absent from older servers.'), alive: z.boolean().nullable().optional(), checked_at: z.string().optional(), details: z.looseObject({ state: z.string().optional(), reason: z.string().optional() }).optional(), gateway_chat: z.looseObject({ enabled: z.boolean().optional(), backend: z.string().optional(), base_url_configured: z.boolean().optional(), api_key_configured: z.boolean().optional() }).optional(), error: z.string().optional() });
561
776
  export const SystemHealthSchema = z.looseObject({
562
777
  available: z.boolean().optional(), status: z.string().optional(), checked_at: z.string().optional(), cpu: Json.nullable().optional(), memory: Json.nullable().optional(), disk: z.looseObject({ percent: z.number().optional(), total_bytes: z.number().optional(), used_bytes: z.number().optional() }).nullable().optional(),
563
778
  errors: z.array(z.looseObject({ code: z.string().optional(), metric: z.string().optional() })).optional(), webui_runtime: Json.optional(),
564
779
  });
565
780
  export const UpdateStateSchema = z.enum(['up_to_date', 'release_ready', 'commits_behind', 'local_changes', 'finish', 'manual', 'check_failed', 'off', 'unknown']);
566
- export const UpdateTargetSchema = z.looseObject({ name: z.string().optional(), channel: z.enum(['stable', 'experimental']).optional(), unsupported: z.boolean().optional(), supported_revision: z.string().optional(), supported_version: z.string().optional(), candidate_revision: z.string().optional(), behind: z.number().nullable().optional(), current_sha: z.string().nullable().optional(), latest_sha: z.string().nullable().optional(), compare_url: z.string().optional(), repo_url: z.string().optional(), error: z.string().optional(), ok: z.boolean().optional(), manual_update: z.boolean().optional(), no_git: z.boolean().optional(), install_kind: z.literal('npm').optional(), npm: z.string().optional(), channel_switch: z.boolean().optional(), dirty: z.boolean().optional(), metadata_repair: z.boolean().optional(), release_url: z.string().optional(), ignored: z.boolean().optional(), stale_check: z.boolean().optional(), current_version: z.string().optional(),
781
+ const AgentSupportSchema = z.enum(['supported', 'unchecked']);
782
+ export const UpdateVersionSchema = z.object({
783
+ version: z.string().describe('The release as shown: the Agent tag or the Web release tag.'),
784
+ ref: z.string().describe('What `updates.apply`/`updates.force` accept back as `version`: the Agent tag or the Web semver.'),
785
+ sha: z.string().optional().describe('Agent only: the commit the tag peels to.'),
786
+ release_url: z.string().nullable(),
787
+ latest: z.boolean().describe('The channel\'s newest release, the one an update without `version` installs.'),
788
+ folded: z.boolean().describe('Shown behind the folded "more versions" node: every entry except the newest.'),
789
+ support: AgentSupportSchema.optional().describe('Agent only: this release\'s classification against the verified Agent window.'),
790
+ support_label: z.string().optional().describe('Agent only: `support` worded to fit one phone line.'),
791
+ }).describe('TAL-946: one installable Stable release newer than the installed one.');
792
+ export const UpdateTargetSchema = z.looseObject({
793
+ failed_update: z.object({ version: z.string(), restored_version: z.string() }).optional().describe('Web only: the update that failed before HTTP readiness and the restored package version. Manual retry remains available.'),
794
+ name: z.string().optional(), channel: z.enum(['stable', 'experimental']).optional(),
795
+ unsupported: z.boolean().optional().describe('Deprecated (TAL-350): `support === \'unchecked\'`, kept for older clients.'),
796
+ support: AgentSupportSchema.optional().describe('Agent target only (TAL-350): `supported` when the target revision is exactly a release in the verified Agent window, else `unchecked`; an `unchecked` apply requires `confirmed_agent_revision`.'),
797
+ support_label: z.string().optional().describe('Agent target only (TAL-946): `support` worded to fit one phone line.'),
798
+ current_support: AgentSupportSchema.optional().describe('Agent target only: the same classification for the installed Agent revision.'),
799
+ supported_revision: z.string().optional(), supported_version: z.string().optional(), candidate_revision: z.string().optional(), behind: z.number().nullable().optional(), current_sha: z.string().nullable().optional(), latest_sha: z.string().nullable().optional(), compare_url: z.string().optional(), repo_url: z.string().optional(), error: z.string().optional(), ok: z.boolean().optional(), manual_update: z.boolean().optional(), no_git: z.boolean().optional(), install_kind: z.literal('npm').optional(), npm: z.string().optional(), channel_switch: z.boolean().optional(), dirty: z.boolean().optional(), metadata_repair: z.boolean().optional(), release_url: z.string().optional(), ignored: z.boolean().optional(), stale_check: z.boolean().optional(), current_version: z.string().optional(),
567
800
  state: UpdateStateSchema.describe('TAL-559: the status clients word; `off` when checks are disabled or Agent updates are ignored.'),
568
801
  can_apply: z.boolean().describe('The apply action is available for this target.'),
569
802
  installed_unverified: z.boolean().describe('`release_ready` only: the installed revision sits on no release tag.'),
570
803
  manual_link: z.boolean().describe('Show the manual-install (releases) link.'),
571
804
  target_version: z.string().nullable().describe('The version the update path leads to, when one is shown.'),
805
+ summary_available: z.boolean().describe('TAL-759: offer "Summarize with AI" for this target (`release_ready` or `commits_behind`).'),
806
+ versions: z.array(UpdateVersionSchema).describe('TAL-946: every installable Stable release newer than the installed one, newest first; empty unless `release_ready` can apply.'),
807
+ folded_count: z.number().int().nonnegative().describe('How many `versions` are `folded`.'),
808
+ });
809
+ export const AgentRecoveryStateSchema = z.object({ phase: z.enum(['idle', 'running', 'waiting', 'complete', 'blocked']), attempt: z.number().int().nonnegative(), detail: z.string(), retry_at: z.string().nullable() });
810
+ export const UpdatesCheckSchema = z.looseObject({ agent_recovery: AgentRecoveryStateSchema.optional(), disabled: z.boolean().optional(), cached: z.boolean().optional(), channel: z.string().optional(), agent_channel: z.enum(['stable', 'experimental']).optional(), checked_at: z.number().optional(), include_agent: z.boolean().optional(), webui: UpdateTargetSchema.nullable().optional(), agent: UpdateTargetSchema.nullable().optional() });
811
+ export const UpdatesSummarySchema = z.object({
812
+ ok: z.boolean(), target: z.enum(['webui', 'agent']), summary: z.string(),
813
+ current_version: z.string().nullable().describe('The installed version this summary starts from.'),
814
+ target_version: z.string().nullable().describe('The version this summary leads to, when the update names one.'),
815
+ summary_sections: z.array(z.object({ title: z.string(), items: z.array(z.string()) })).describe('Display-ready headed bullet lists; empty when nothing is pending.'),
816
+ generated_by: z.enum(['llm', 'fallback', 'none']).describe('`fallback` when no model answered; `none` when the target has no pending update to summarize.'),
817
+ source: z.enum(['release_notes', 'commits', 'none']).describe('Stable targets summarize the release notes they install; commit-tracking targets, or unreadable notes, use commit subjects.'),
818
+ cached: z.boolean(),
819
+ });
820
+ export const UpdateApplySchema = z.looseObject({
821
+ recovery_compatibility: z.object({ manual_execution: z.boolean(), bot_chat_delivery: z.boolean() }).optional()
822
+ .describe('Whether the target Agent can reconcile retained manual execution and pending Bot Chat records. A false value blocks the update before any source or repair-intent change.'),
823
+ ok: z.boolean().optional(), status: z.string().optional(), error: z.string().optional(), message: z.string().optional(), lock: Json.optional(), confirmation_required: z.boolean().optional(), refresh_confirmation: AgentRefreshConfirmationSchema.optional(), candidate_revision: z.string().optional(), supported_revision: z.string().optional(), supported_version: z.string().optional(), agent_channel: z.enum(['stable', 'experimental']).optional(), notification_id: z.uuid().optional(), channel_switch: z.boolean().optional(), backup_dir: z.string().nullable().optional(), backup_failed: z.boolean().optional(), stash_conflict: z.boolean().optional(), gateway_restart: z.string().optional().describe('TAL-953: `waiting_for_idle` when an Agent update left the gateway restart to wait for the gateway\'s running work; the update notification reports its outcome.')
572
824
  });
573
- export const UpdatesCheckSchema = z.looseObject({ disabled: z.boolean().optional(), cached: z.boolean().optional(), channel: z.string().optional(), agent_channel: z.enum(['stable', 'experimental']).optional(), checked_at: z.number().optional(), include_agent: z.boolean().optional(), webui: UpdateTargetSchema.nullable().optional(), agent: UpdateTargetSchema.nullable().optional() });
574
- export const UpdatesSummarySchema = z.looseObject({ summary: z.string().optional(), text: z.string().optional(), ok: z.boolean().optional(), error: z.string().optional(), diff_links: z.array(Json).optional() });
575
- export const UpdateApplySchema = z.looseObject({ ok: z.boolean().optional(), status: z.string().optional(), error: z.string().optional(), message: z.string().optional(), lock: Json.optional(), confirmation_required: z.boolean().optional(), candidate_revision: z.string().optional(), supported_revision: z.string().optional(), supported_version: z.string().optional(), agent_channel: z.enum(['stable', 'experimental']).optional(), notification_id: z.uuid().optional(), channel_switch: z.boolean().optional(), backup_dir: z.string().nullable().optional(), backup_failed: z.boolean().optional(), stash_conflict: z.boolean().optional() });
576
- export const UpdateNotificationPhaseSchema = z.enum(['applying', 'awaiting_confirmation', 'restarting', 'succeeded', 'blocked', 'failed', 'unknown']);
825
+ export const UpdateNotificationPhaseSchema = z.enum(['applying', 'awaiting_confirmation', 'waiting_for_idle', 'restarting', 'succeeded', 'blocked', 'failed', 'unknown']);
577
826
  export const UpdateNotificationActionSchema = z.object({
578
827
  id: z.string().min(1).max(64).regex(/^[a-z0-9][a-z0-9_-]*$/),
579
828
  label: z.string().min(1).max(80),
@@ -631,12 +880,72 @@ export const UpdateNotificationsSchema = z.object({
631
880
  clearable_count: z.number().int().nonnegative(),
632
881
  can_clear: z.boolean(),
633
882
  });
634
- export const PluginSchema = z.looseObject({ key: z.string(), name: z.string().optional(), kind: z.string().optional(), enabled: z.boolean().optional(), description: z.string().optional(), version: z.string().optional(), activation: z.string().optional(), is_active_provider: z.boolean().optional(), hooks: z.array(Json).optional() });
635
- export const PluginsSchema = z.looseObject({ plugins: z.array(PluginSchema), empty: z.boolean().optional(), read_only: z.boolean().optional(), supported_hooks: z.array(z.string()).optional(), unavailable: z.boolean().optional() });
883
+ /** TAL-806: a plugin setting as Settings shows it; `locked` marks a secret the server's launch environment supplies, so an edit would not take effect. */
884
+ export const PluginSettingViewSchema = PluginSettingFieldSchema.extend({ locked: z.boolean().optional() });
885
+ export const PluginSchema = z.looseObject({ key: z.string(), name: z.string().optional(), kind: z.string().optional(), enabled: z.boolean().optional(), description: z.string().optional(), version: z.string().optional(), activation: z.string().optional(), is_active_provider: z.boolean().optional(), hooks: z.array(Json).optional(),
886
+ /** TAL-806: the manifest's `config_schema` fields with current values; absent when the plugin declares none. */
887
+ settings: z.array(PluginSettingViewSchema).optional() });
888
+ /**
889
+ * `read_only` (TAL-806): this session may not save plugin `settings` (not the server owner, or no sidecar).
890
+ * `plugin_isolation_advisory` (TAL-820): more than one profile has third-party plugins installed and one of those runs them
891
+ * `in_process`, so Settings recommends `plugins.isolation: host`; Talaria never sets the mode.
892
+ */
893
+ export const PluginsSchema = z.looseObject({ plugins: z.array(PluginSchema), empty: z.boolean().optional(), read_only: z.boolean().optional(), supported_hooks: z.array(z.string()).optional(), unavailable: z.boolean().optional(), plugin_isolation_advisory: z.boolean().optional() });
636
894
  export const McpServerSchema = z.looseObject({ name: z.string(), id: z.string().optional(), transport: z.string().optional(), enabled: z.boolean().optional(), can_toggle: z.boolean().optional().describe('Whether PATCH can enable or disable this server; false for invalid config.'), active: z.boolean().optional(), status: z.string().optional(), tools: z.number().optional(), tool_count: z.number().int().nullable().optional(), health: z.string().optional(), health_detail: z.string().optional(), health_checked_at: Json.optional(), health_pending: z.boolean().optional() });
637
895
  export const McpServersSchema = z.looseObject({ servers: z.array(McpServerSchema), health_pending: z.boolean().optional(), reload_required: z.boolean().optional(), toggle_supported: z.boolean().optional() });
896
+ /** TAL-818: one connected account of a Nous tool-gateway app; `name` is its alias, else the vendor label. `problem` is its gateway status while it is not active, else null. */
897
+ export const ConnectorAccountViewSchema = z.object({ connection_id: z.string(), name: z.string(), problem: z.enum(['pending', 'failed', 'expired', 'revoked', 'inactive']).nullable(), status_reason: z.string().nullable(), active: z.boolean(), created_at: z.string() });
898
+ /**
899
+ * TAL-818: one app on the Connectors page. `accounts` are its live accounts, active first. `enabled`: Hermes may use it (the
900
+ * member rule allows it and no other layer locks it). `org_locked` means an organization or role rule keeps it off whatever the member rule says. `rules_editable`: the switch
901
+ * may change the member rule (readable rules and no lock). `tools_off` counts the tools the member rules turn off.
902
+ */
903
+ export const ConnectorViewSchema = z.object({
904
+ slug: z.string(), name: z.string(), description: z.string(), category: z.string(), logo_url: z.string().nullable(),
905
+ connected: z.boolean(), accounts: z.array(ConnectorAccountViewSchema), enabled: z.boolean(), org_locked: z.boolean(), rules_editable: z.boolean(), tools_off: z.number().int(),
906
+ });
907
+ /**
908
+ * TAL-818: the Connectors page. `available` is the Agent's connectors gate; false hides the page. `policy_revision` is the
909
+ * member rules' revision every rule change sends back; null when the rules are read-only (no member layer, or unreadable),
910
+ * so no switch changes them. `rules_error`: why the rules could not be read; every app then reads off (fails closed).
911
+ */
912
+ export const ConnectorsPageSchema = z.object({ available: z.boolean(), connectors: z.array(ConnectorViewSchema), policy_revision: z.string().nullable(), rules_error: z.string().nullable() });
913
+ /**
914
+ * TAL-818: one app's tools: `enabled`: Hermes may use it (the member rules leave it on and nothing locks it); `locked`: an
915
+ * organization or role rule (by name or tag) keeps it off. `editable`: its switch may change the member rule (readable rules, the app on in them, and no lock).
916
+ */
917
+ export const ConnectorToolsViewSchema = z.object({
918
+ connector: z.string(), policy_revision: z.string().nullable(),
919
+ tools: z.array(z.object({ slug: z.string(), name: z.string(), description: z.string(), facet: z.enum(['read', 'write', 'destructive', 'unclassified']), deprecated: z.boolean(), enabled: z.boolean(), locked: z.boolean(), editable: z.boolean() })),
920
+ });
921
+ /**
922
+ * TAL-818: a connection operation, from the Connectors page's Connect or a chat's `manage_connections` card. Each target's
923
+ * `connect_url` is the authorization link while it waits on one; `actions` are the moves the user may make on it now, and
924
+ * `env_fields` the credentials an `approve` sends, each with its `label`. `can_continue`: the card may end the wait for the
925
+ * rest (chat only). `outcome` is null while the operation is open, then `connected` when every target connected.
926
+ */
927
+ export const ConnectorOperationViewSchema = z.object({
928
+ op_id: z.string(), settled: z.boolean(), outcome: z.enum(['connected', 'not_connected']).nullable(), can_continue: z.boolean(), tool_call_id: z.string().nullable(),
929
+ targets: z.array(z.object({
930
+ name: z.string(), title: z.string(), kind: z.string(), state: z.enum(['pending', 'initiated', 'connected', 'skipped', 'failed', 'expired', 'not_connected']),
931
+ detail: z.string(), connect_url: z.string().nullable(), actions: z.array(z.enum(['skip', 'retry', 'approve'])),
932
+ env_fields: z.array(z.object({ name: z.string(), label: z.string(), required: z.boolean(), secret: z.boolean() })),
933
+ })),
934
+ });
638
935
  export const McpToolsSchema = z.looseObject({ tools: z.array(z.looseObject({ name: z.string().optional(), server: z.string().optional(), description: z.string().optional() })), total: z.number().int().optional(), source: z.string().optional(), inventory_scope: z.string().optional(), unavailable_servers: z.array(z.string()).optional() });
639
936
  export const NotesSourcesSchema = z.looseObject({ enabled: z.boolean().optional(), sources: z.array(Json).optional(), source: z.string().optional(), inventory_scope: z.string().optional(), recent_ai_notes: z.array(Json).optional(), attach_supported: z.boolean().optional(), automatic_recall_unchanged: z.boolean().optional() });
937
+ /** Joplin search hit: bounded, redacted display text and a snippet around the query; `updated_time` is Joplin's raw value. */
938
+ export const NoteSearchResultSchema = z.object({ id: z.string(), title: z.string(), snippet: z.string(), parent_id: z.string(), updated_time: Json, source: z.literal('joplin') });
939
+ export const NotesSearchSchema = z.object({ source: z.literal('joplin'), query: z.string(), results: z.array(NoteSearchResultSchema) });
940
+ /** One Joplin note for preview: `body` is cut at 50,000 characters (with a notice) and redacted. */
941
+ export const NoteItemSchema = z.object({ source: z.literal('joplin'), note: z.object({ id: z.string(), title: z.string(), body: z.string(), parent_id: z.string(), updated_time: Json, created_time: Json, source: z.literal('joplin') }) });
942
+ /** LLM wiki summary: counts and timestamps only, never page bodies or the wiki path. `error` names the exception type when `status` is `error`. */
943
+ export const WikiStatusSchema = z.object({
944
+ available: z.boolean(), enabled: z.boolean(), status: z.enum(['missing', 'not_directory', 'ready', 'empty', 'error']), entry_count: z.number().int(), page_count: z.number().int(), raw_source_count: z.number().int(),
945
+ last_updated: z.string().nullable(), last_writer: z.string(), path_configured: z.boolean(), path_source: z.string(), toggle_available: z.literal(false), toggle_reason: z.string(), docs_url: z.string(), error: z.string().optional(),
946
+ });
947
+ export const WikiBrowseSchema = z.object({ pages: z.array(z.object({ name: z.string(), path: z.string(), size: z.number().int(), mtime: z.number().int() })) });
948
+ export const WikiPageSchema = z.object({ content: z.string(), path: z.string() });
640
949
  export const TodoItemSchema = z.looseObject({ id: z.union([z.string(), z.number()]).optional(), text: z.string().optional(), content: z.string().optional(), title: z.string().optional(), status: z.string().optional(), done: z.boolean().optional(), completed: z.boolean().optional() });
641
950
  export const TodoStateSchema = z.looseObject({ session_id: z.string().optional(), todos: z.array(TodoItemSchema).optional(), summary: z.record(z.string(), z.unknown()).optional(), version: z.number().optional(), ts: z.number().optional(), source: z.string().optional(), description: z.string().optional(), pending_count: z.number().optional() });
642
951
  // ── onboarding ───────────────────────────────────────────────────────────
@@ -674,8 +983,20 @@ export const CronJobViewSchema = z.looseObject({
674
983
  context_from: z.union([z.array(z.string()), z.string(), z.null()]).optional(), reasoning_effort: NullableString.optional(), toast_notifications: z.boolean().optional(), running: z.boolean().optional(),
675
984
  /** Server-derived (TAL-296): the job's status, whether it needs attention, and whether its primary action is Resume. Live runs overlay it. */
676
985
  derived_state: CronDerivedStateSchema.optional(), needs_attention: z.boolean().optional(), resumable: z.boolean().optional(),
986
+ /** Server-derived (TAL-599): a known cause clients explain in the needs-attention banner. */
987
+ attention_hint: z.enum(['croniter_missing']).nullable(),
988
+ /** Server-normalized (TAL-810): the job's latest failed run, kept after later successes; `at` is epoch seconds, null when unparseable. */
989
+ last_failure: z.object({ at: z.number().nullable(), detail: z.string() }).nullable(),
990
+ });
991
+ /** TAL-810: a listed profile's cron store whose writes fail (full disk, read-only mount, permissions), so its scheduler skips every run. `error` and `fix` are the Agent's text; `since` is the last write that landed (epoch seconds), null when unknown. */
992
+ export const CronStoreHealthSchema = z.object({ profile: z.string(), store: z.string(), error: z.string(), fix: z.string(), since: z.number().nullable() });
993
+ export const CronsSchema = z.looseObject({
994
+ jobs: z.array(CronJobViewSchema),
995
+ /** TAL-601: `jobs` arrive in display order (running first, then soonest next run, none last, then name); clients keep it. Absent from older servers. */
996
+ ordered: z.boolean().optional(),
997
+ active_profile: z.string().optional(), all_profiles: z.boolean().optional(), other_profile_count: z.number().optional(), cron_unavailable: z.boolean().optional(),
998
+ unwritable_stores: z.array(CronStoreHealthSchema),
677
999
  });
678
- export const CronsSchema = z.looseObject({ jobs: z.array(CronJobViewSchema), active_profile: z.string().optional(), all_profiles: z.boolean().optional(), other_profile_count: z.number().optional(), cron_unavailable: z.boolean().optional() });
679
1000
  export const CronContextSourcesSchema = z.object({ profile: z.string(), sources: z.array(z.object({ job_id: z.string(), label: z.string(), selectable: z.boolean() })) });
680
1001
  /** `GET /api/crons/recent` row: one job's latest completion, ordered and classified by the server. */
681
1002
  export const CronRecentCompletionSchema = z.object({
@@ -687,25 +1008,29 @@ export const CronMutationSchema = z.looseObject({ ok: z.boolean().optional(), jo
687
1008
  export const CronRunUsageSchema = z.looseObject({ input_tokens: NullableNumber.optional(), output_tokens: NullableNumber.optional(), total_tokens: NullableNumber.optional(), estimated_cost_usd: NullableNumber.optional(), duration_seconds: NullableNumber.optional(), model: z.string().optional(), provider: z.string().optional() });
688
1009
  export const CronRunSummarySchema = z.looseObject({ filename: z.string(), size: z.number(), modified: z.number(), usage: CronRunUsageSchema.optional() });
689
1010
  export const CronHistorySchema = z.looseObject({ job_id: z.string().optional(), runs: z.array(CronRunSummarySchema), total: z.number().optional(), offset: z.number().optional() });
690
- export const CronRunSchema = z.looseObject({ content: z.string().optional(), snippet: z.string().optional(), usage: CronRunUsageSchema.optional(), error: z.string().optional() });
1011
+ /** `response` is server-extracted (TAL-599): the agent's reply after the run file's `## Response` heading, else the whole text. */
1012
+ export const CronRunSchema = z.looseObject({ content: z.string().optional(), snippet: z.string().optional(), response: z.string().optional(), usage: CronRunUsageSchema.optional(), error: z.string().optional() });
691
1013
  export const CronStatusSchema = z.looseObject({ running: z.union([z.boolean(), z.record(z.string(), z.number())]).optional() });
692
1014
  /** Card actions the server offers for a task's current status (TAL-557); clients show exactly these. `move_to` lists the statuses a Move may target, in display order. */
693
1015
  export const KanbanTaskActionsSchema = z.object({ block: z.boolean(), unblock: z.boolean(), complete: z.boolean(), archive: z.boolean(), move_to: z.array(z.string()) });
694
1016
  /** Server-computed card policy on every task the Kanban routes return (TAL-557). `requires_running_exit_confirmation`: any action takes the task out of a live run, so clients confirm first. */
695
1017
  export const KanbanTaskPolicyShape = { available_actions: KanbanTaskActionsSchema, requires_running_exit_confirmation: z.boolean() };
696
1018
  export const KanbanTaskViewSchema = z.looseObject({ id: z.union([z.string(), z.number()]), title: z.string().optional(), status: z.string().optional(), assignee: NullableString.optional(), priority: z.union([z.string(), z.number()]).nullable().optional(), description: z.string().optional(), tags: z.array(z.string()).optional(), session_id: NullableString.optional(), archived: z.boolean().optional(), created_at: Json.optional(), updated_at: Json.optional(), ...KanbanTaskPolicyShape });
697
- export const KanbanColumnSchema = z.looseObject({ name: z.string(), tasks: z.array(KanbanTaskViewSchema) });
1019
+ /** A board column in the Agent workflow's order (TAL-796): `name` is the status key, `label` the Agent's English label. */
1020
+ export const KanbanColumnSchema = z.looseObject({ name: z.string(), label: z.string(), tasks: z.array(KanbanTaskViewSchema) });
698
1021
  /** One profile lane (TAL-567): an assignee's tasks (`null` = unassigned) across every board column, with its task count. */
699
1022
  export const KanbanLaneSchema = z.object({ assignee: z.string().nullable(), count: z.number().int(), columns: z.array(KanbanColumnSchema) });
700
1023
  export const KanbanBoardViewSchema = z.looseObject({ columns: z.array(KanbanColumnSchema).optional(), assignees: z.array(Json).optional(), filters: Json.optional(), latest_event_id: z.number().optional(), read_only: z.boolean().optional(), tenants: z.array(Json).optional(), changed: z.boolean().optional(),
701
1024
  /** Statuses a bulk status change may target (TAL-557). */
702
1025
  bulk_move_targets: z.array(z.string()),
1026
+ /** Statuses a new task may start in (TAL-796). */
1027
+ create_statuses: z.array(z.string()),
703
1028
  /** The saved board view (TAL-567): `true` groups the board into `lanes` by assignee. */
704
1029
  lane_by_profile: z.boolean(),
705
1030
  /** Present when `lane_by_profile`: the columns split into profile lanes, assignees in name order, unassigned last. */
706
1031
  lanes: z.array(KanbanLaneSchema).optional() });
707
1032
  export const KanbanBoardsViewSchema = z.looseObject({ boards: z.array(z.looseObject({ slug: z.string(), name: z.string().nullable().optional(), is_current: z.boolean().optional(), total: z.number().optional(), archived: z.boolean().optional(), color: z.string().optional(), icon: z.string().optional(), description: z.string().optional(),
708
- /** Whether the board can be archived or deleted; the default board cannot (TAL-567). */
1033
+ /** Whether the board can be archived or deleted; the default board (TAL-567) and an archived board (TAL-795) cannot. */
709
1034
  removable: z.boolean() })), current: z.string().optional(), read_only: z.boolean().optional() });
710
1035
  /** Board-wide task counts (TAL-567): `status_counts` in board-column order, unknown statuses after; `total` excludes archived tasks. */
711
1036
  export const KanbanStatsViewSchema = z.looseObject({ by_status: z.record(z.string(), z.number()), total: z.number().int(), status_counts: z.array(z.object({ status: z.string(), count: z.number().int() })) });