@crossworks/client-types 0.230.43

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.
package/src/index.ts ADDED
@@ -0,0 +1,2324 @@
1
+ /**
2
+ * @mantle/client-types — wire-shape (JSON) types for the HTTP API, shared by the
3
+ * client components that consume `/api/**` (TanStack Query) and the server code
4
+ * that produces the responses.
5
+ *
6
+ * Pure types: ZERO runtime, ZERO dependencies. That's the whole point — a client
7
+ * component can name a row shape without importing `@mantle/db` (which drags
8
+ * `postgres` into the browser bundle). This is the single source of truth for the
9
+ * frontend/backend contract as screens move to client data-fetching (Phase 2 ·
10
+ * Task 4); the server aliases its summary types to these so drift is a type error.
11
+ *
12
+ * Since the jackdaw-repo-split P0 this package is the CONTRACT package: subpath
13
+ * modules (`version`, `turn-streaming`, `traces-format`, `types/*`, `lib/*`, …)
14
+ * carry the small dependency-free constants and helpers both sides of the wire
15
+ * share. This root index stays types-only; subpaths may hold runtime code but
16
+ * must remain zero-dependency and browser-safe.
17
+ *
18
+ * Dates are ISO strings here — that's how they cross the wire (JSON has no Date).
19
+ */
20
+ import type { AuditSeverity, SystemReport } from './types/integrity';
21
+ import type { TraceDetail } from './traces-format';
22
+
23
+ // ── Skills ────────────────────────────────────────────────────────────────────
24
+
25
+ /** A skill as returned by `GET /api/skills`. */
26
+ export interface SkillDTO {
27
+ id: string;
28
+ slug: string;
29
+ name: string;
30
+ description: string;
31
+ instructions: string;
32
+ /** Template state heartbeats inherit on create. */
33
+ defaultState: Record<string, unknown>;
34
+ enabled: boolean;
35
+ createdAt: string;
36
+ updatedAt: string;
37
+ }
38
+
39
+ /** A heartbeat that references a skill — drives the "used by N heartbeats" badge. */
40
+ export interface HeartbeatRef {
41
+ slug: string;
42
+ name: string;
43
+ status: string;
44
+ }
45
+
46
+ /** `GET /api/skills/backrefs` — heartbeat refs keyed by skill slug. */
47
+ export type SkillBackrefs = Record<string, HeartbeatRef[]>;
48
+
49
+ // ── Tools ─────────────────────────────────────────────────────────────────────
50
+
51
+ /**
52
+ * Tool handler descriptor — the canonical wire shape. Mirrors @mantle/db's
53
+ * `ToolHandler` union; kept standalone here so this package stays zero-dep (no
54
+ * postgres type graph). Drift is caught where it matters: `@mantle/tools` aliases
55
+ * `ToolSummary = ToolDTO`, so if db's union ever diverges from this one, that
56
+ * package fails to compile.
57
+ */
58
+ export interface RecipeStep {
59
+ tool: string;
60
+ input?: Record<string, unknown>;
61
+ as?: string;
62
+ }
63
+
64
+ export type ToolHandler =
65
+ | { kind: 'builtin'; ref: string }
66
+ | {
67
+ kind: 'http';
68
+ url: string;
69
+ method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
70
+ headers?: Record<string, string>;
71
+ query?: Record<string, string>;
72
+ body?: string | null;
73
+ headersRef?: string | null;
74
+ authRef?: string | null;
75
+ timeoutMs?: number;
76
+ }
77
+ | { kind: 'shell'; cmd: string }
78
+ | { kind: 'recipe'; steps: RecipeStep[]; output?: unknown };
79
+
80
+ /** A tool as returned by `GET /api/tools`. */
81
+ export interface ToolDTO {
82
+ id: string;
83
+ slug: string;
84
+ name: string;
85
+ description: string;
86
+ inputSchema: Record<string, unknown>;
87
+ handler: ToolHandler;
88
+ requiresConfirm: boolean;
89
+ enabled: boolean;
90
+ createdAt: string;
91
+ updatedAt: string;
92
+ }
93
+
94
+ /** `GET/PUT /api/tools/settings` — the two owner-level tool policy toggles. */
95
+ export interface ToolSettings {
96
+ /** Tools an agent authors (Toolsmith) start confirm-gated until cleared. */
97
+ requireApproval: boolean;
98
+ /** Unattended heartbeats park email/web calls for approval. */
99
+ egressGate: boolean;
100
+ }
101
+
102
+ // ── Tool groups ───────────────────────────────────────────────────────────────
103
+
104
+ /**
105
+ * The service binding on a group that IS an API integration: where its calls go,
106
+ * which vault entry authenticates them, where that credential is placed, and
107
+ * pointers to the stored API docs + usage skill. Mirrors the `ToolGroupIntegration`
108
+ * type in @mantle/db. `secretRef` is a `service/label` pointer and auth-template
109
+ * values are `{{secret:…}}` refs — a plaintext key never crosses this wire.
110
+ */
111
+ export interface ToolGroupIntegrationDTO {
112
+ service: string;
113
+ baseUrl?: string;
114
+ secretRef?: string;
115
+ authTemplate?: {
116
+ headers?: Record<string, string>;
117
+ query?: Record<string, string>;
118
+ };
119
+ docsNodeId?: string;
120
+ docsSourceUrl?: string;
121
+ docsUpdatedAt?: string;
122
+ /** Slug of the usage skill that travels with this group's grant. */
123
+ skillSlug?: string;
124
+ }
125
+
126
+ /** A tool group — a named bundle of tool slugs granted to agents wholesale. */
127
+ export interface ToolGroupDTO {
128
+ id: string;
129
+ slug: string;
130
+ name: string;
131
+ description: string;
132
+ toolSlugs: string[];
133
+ /** Set when the group is an API integration; null for capability-only bundles. */
134
+ integration: ToolGroupIntegrationDTO | null;
135
+ enabled: boolean;
136
+ createdAt: string;
137
+ updatedAt: string;
138
+ }
139
+
140
+ /** `GET /api/tool-groups` — each group plus which agent slugs grant it. */
141
+ export interface ToolGroupWithRefs extends ToolGroupDTO {
142
+ grantedTo: string[];
143
+ }
144
+
145
+ // ── AI workers ────────────────────────────────────────────────────────────────
146
+
147
+ /** Worker kinds (mirrors the @mantle/db `ai_worker_kind` enum). Drift is caught
148
+ * by `toAiWorkerDTO` in lib/ai-workers, whose mapping won't compile if the db
149
+ * enum gains/renames a value. */
150
+ export type AiWorkerKind =
151
+ | 'reflector'
152
+ | 'extractor'
153
+ | 'summarizer'
154
+ | 'tts'
155
+ | 'stt'
156
+ | 'vision'
157
+ | 'document'
158
+ | 'image_gen'
159
+ | 'embedding'
160
+ | 'search'
161
+ | 'search_advanced'
162
+ | 'narrator'
163
+ | 'suggester';
164
+
165
+ /** An AI worker as returned by `GET /api/ai-workers`. `params` is jsonb (shape
166
+ * varies by kind) — kept loose here; the form narrows per kind. */
167
+ export interface AiWorkerDTO {
168
+ id: string;
169
+ slug: string;
170
+ name: string;
171
+ kind: AiWorkerKind;
172
+ provider: string;
173
+ model: string;
174
+ apiKeyId: string | null;
175
+ systemPrompt: string | null;
176
+ params: Record<string, unknown>;
177
+ enabled: boolean;
178
+ priority: number;
179
+ isDefault: boolean;
180
+ backupProvider: string | null;
181
+ backupModel: string | null;
182
+ backupApiKeyId: string | null;
183
+ backupEnabled: boolean;
184
+ baseUrl: string | null;
185
+ viaTailnet: boolean;
186
+ backupBaseUrl: string | null;
187
+ backupViaTailnet: boolean;
188
+ usageCount: number;
189
+ lastUsedAt: string | null;
190
+ createdAt: string;
191
+ updatedAt: string;
192
+ }
193
+
194
+ /** `GET /api/ai-workers/config` — static-ish bits the worker form needs. */
195
+ export interface AiWorkerConfig {
196
+ /** Providers with a native-PDF document adapter (vs. rasterize-at-ingest). */
197
+ nativeDocProviders: string[];
198
+ /** Online tailnet peer MagicDNS names (route base-URL datalist). */
199
+ tailnetPeers: string[];
200
+ }
201
+
202
+ // ── Agents ────────────────────────────────────────────────────────────────────
203
+
204
+ /** Conversational + worker roles an agent row can carry. Mirrors the
205
+ * `agent_role` enum (`packages/db/src/schema/agents.ts`); the `/settings/agents`
206
+ * page only lists the conversational ones. */
207
+ export type AgentRole =
208
+ | 'assistant'
209
+ | 'responder'
210
+ | 'extractor'
211
+ | 'summarizer'
212
+ | 'reflector'
213
+ | 'custom'
214
+ // Runner-queue worker template (docs/runs.md) — never conversational.
215
+ | 'worker';
216
+
217
+ /** Per-agent generated avatar (style + seed → DiceBear). null = initials. */
218
+ export interface AgentAvatarDTO {
219
+ style: string;
220
+ seed: string;
221
+ }
222
+
223
+ /** Memory/budget tuning (jsonb). All fields optional — empty = runtime defaults.
224
+ * Replicated standalone (NOT re-exported from @mantle/db) to keep this package
225
+ * zero-dep; the server aliases its `AgentMemoryConfig` against this so drift is
226
+ * a compile error. */
227
+ export interface AgentMemoryConfigDTO {
228
+ history_limit?: number;
229
+ history_window_hours?: number | null;
230
+ digest_limit?: number;
231
+ fact_limit?: number;
232
+ content_hit_limit?: number;
233
+ chunk_limit?: number;
234
+ inject_journal?: boolean;
235
+ summarize_threshold?: number;
236
+ summarize_batch?: number;
237
+ extract_types?: string[];
238
+ extract_facts?: boolean;
239
+ extract_cost_cap_micro_usd?: number | null;
240
+ delegate_to?: string[];
241
+ max_iterations?: number;
242
+ result_handling?: {
243
+ inline_max_kb?: number;
244
+ embed_min_kb?: number;
245
+ spill_max_kb?: number;
246
+ };
247
+ }
248
+
249
+ /** Sampling + voice-reply params (jsonb). */
250
+ export interface AgentParamsDTO {
251
+ temperature?: number;
252
+ max_tokens?: number;
253
+ top_p?: number;
254
+ max_retries?: number;
255
+ voice?: {
256
+ enabled?: boolean;
257
+ name?: 'alloy' | 'echo' | 'fable' | 'nova' | 'onyx' | 'shimmer';
258
+ model?: 'tts-1' | 'tts-1-hd';
259
+ speed?: number;
260
+ };
261
+ /** Propose a follow-up question after each completed turn (the suggester
262
+ * worker's chip above the chat composer). Absent/false = off. */
263
+ suggest_follow_up?: boolean;
264
+ }
265
+
266
+ /** One persona note (jsonb element). Soft-retired, never deleted — the read
267
+ * path filters `retiredAt`. `at`/`retiredAt` are ISO strings. */
268
+ export interface PersonaNoteDTO {
269
+ id?: string;
270
+ kind: 'style' | 'relationship' | 'correction';
271
+ content: string;
272
+ at: string;
273
+ source?: { type: 'turn' | 'digest'; id: string };
274
+ retiredAt?: string;
275
+ retiredReason?: 'superseded' | 'removed';
276
+ supersededBy?: string;
277
+ }
278
+
279
+ /** An agent as returned by `GET /api/agents` (and `…/[id]`). Dates are ISO
280
+ * strings. The server aliases its `AgentSummary` to this so the wire shape and
281
+ * the consuming client can't drift. */
282
+ export interface AgentDTO {
283
+ id: string;
284
+ slug: string;
285
+ name: string;
286
+ description: string | null;
287
+ role: AgentRole;
288
+ provider: string;
289
+ model: string;
290
+ apiKeyId: string | null;
291
+ backupProvider: string | null;
292
+ backupModel: string | null;
293
+ backupApiKeyId: string | null;
294
+ backupEnabled: boolean;
295
+ baseUrl: string | null;
296
+ viaTailnet: boolean;
297
+ backupBaseUrl: string | null;
298
+ backupViaTailnet: boolean;
299
+ ttsWorkerId: string | null;
300
+ systemPrompt: string;
301
+ skillSlugs: string[];
302
+ toolGroupSlugs: string[];
303
+ memoryConfig: AgentMemoryConfigDTO;
304
+ params: AgentParamsDTO;
305
+ avatar: AgentAvatarDTO | null;
306
+ personaNotes: PersonaNoteDTO[];
307
+ /** The co-admin login this agent is the personal assistant for (migration
308
+ * 0143), or null for a shared agent. Set, it becomes that login's default
309
+ * chat target — the mechanism that keeps two people typing at once out of
310
+ * one interleaved thread. Not a privacy boundary: every login still sees
311
+ * and can open every agent. */
312
+ assignedUserId: string | null;
313
+ /** ISO timestamp of the current assignment; null when unassigned. */
314
+ assignedAt: string | null;
315
+ priority: number;
316
+ enabled: boolean;
317
+ /** True when this agent ships from the system manifest (a def-synced
318
+ * specialist). Since 2026-07-29 only its params/memoryConfig tuning
319
+ * re-syncs on upgrade — prompt, model, provider and key are operator-owned
320
+ * and survive. Drives the "system" badge on the agents screens. */
321
+ manifestManaged: boolean;
322
+ lastUsedAt: string | null;
323
+ usageCount: number;
324
+ createdAt: string;
325
+ updatedAt: string;
326
+ }
327
+
328
+ /** A lightweight agent option (slug + name + role) for picker dropdowns —
329
+ * `GET /api/agents/options`. Unlike `GET /api/agents` (conversational roles
330
+ * only), this lists EVERY agent, so heartbeats can bind worker-role agents. */
331
+ export interface AgentOptionDTO {
332
+ slug: string;
333
+ name: string;
334
+ role: AgentRole;
335
+ }
336
+
337
+ // ── Calendar ────────────────────────────────────────────────────────────────────
338
+
339
+ /** A subscribed calendar feed as returned by `GET /api/calendar` — the wire
340
+ * projection of @mantle/db's `CalendarAccount` row. The sealed `feedUrlEnc`
341
+ * credential, `ownerId`, and `syncState` are server-only and intentionally
342
+ * omitted; dates are ISO strings. The route maps its rows to this so the wire
343
+ * shape and the consuming client can't drift. */
344
+ export interface CalendarAccountDTO {
345
+ id: string;
346
+ /** 'ics' (future: 'google' | 'microsoft'). */
347
+ provider: string;
348
+ displayName: string;
349
+ /** Optional UI accent (hex) so multiple calendars are distinguishable. */
350
+ color: string | null;
351
+ enabled: boolean;
352
+ lastEventCount: number | null;
353
+ lastSyncAt: string | null;
354
+ lastSyncError: string | null;
355
+ }
356
+
357
+ // ── Microsoft (SharePoint / OneDrive) ───────────────────────────────────────────
358
+
359
+ /** A discovered drive as returned by `GET/POST /api/microsoft/accounts/[id]/drives`
360
+ * — the wire projection of @mantle/db's `MsDrive` row. The Graph `deltaLink`
361
+ * cursor and `accountId` are server-only and omitted; `lastSyncAt` is an ISO
362
+ * string. The route maps its rows to this so the shapes can't drift. */
363
+ export interface MsDriveDTO {
364
+ id: string;
365
+ /** Graph drive id. */
366
+ driveId: string;
367
+ /** `personal` (OneDrive) | `documentLibrary` (SharePoint) | other. */
368
+ driveType: string;
369
+ name: string;
370
+ /** SharePoint site display name; null for OneDrive. */
371
+ siteName: string | null;
372
+ webUrl: string | null;
373
+ enabled: boolean;
374
+ lastSyncAt: string | null;
375
+ lastError: string | null;
376
+ /** How many scope selections the drive has; 0 = syncing everything. */
377
+ scopeCount: number;
378
+ }
379
+
380
+ /** One scope selection on a drive, as stored/returned by
381
+ * `GET/PUT /api/microsoft/drives/[id]/scopes`. Folder scopes include the
382
+ * whole subtree (path prefix); file scopes match that one item. */
383
+ export interface MsDriveScopeDTO {
384
+ itemId: string;
385
+ /** After-`root:` path, always starting with `/` (e.g. `/Reports/2026`). */
386
+ path: string;
387
+ isFolder: boolean;
388
+ name: string | null;
389
+ }
390
+
391
+ /** One row of a drive-folder listing from
392
+ * `GET /api/microsoft/drives/[id]/browse` — the scope picker's navigation
393
+ * unit. Selection state is client-derived by matching against the scope set. */
394
+ export interface MsDriveChildDTO {
395
+ itemId: string;
396
+ name: string;
397
+ isFolder: boolean;
398
+ childCount: number | null;
399
+ size: number | null;
400
+ path: string | null;
401
+ webUrl: string | null;
402
+ }
403
+
404
+ // ── Email (inbox reading pane) ──────────────────────────────────────────────────
405
+
406
+ /** One message as returned by `GET /api/email/messages/[id]` — the wire
407
+ * projection of @mantle/db's `Email` row, trimmed to what the reading pane
408
+ * renders. Server-only/sensitive columns are dropped: the raw `bodyHtml` (it's
409
+ * sanitized server-side into `MessageDetailDTO.bodyHtmlSafe` and must never
410
+ * cross the wire untrusted), plus account/node/provider ids, labels, snippet,
411
+ * etc. `internalDate` is an ISO string. */
412
+ export interface EmailDTO {
413
+ id: string;
414
+ subject: string | null;
415
+ fromAddr: string;
416
+ fromName: string | null;
417
+ toAddrs: string[];
418
+ ccAddrs: string[];
419
+ internalDate: string;
420
+ folder: string | null;
421
+ isRead: boolean;
422
+ isStarred: boolean;
423
+ bodyText: string | null;
424
+ }
425
+
426
+ /** One attachment row returned with a message. */
427
+ export interface EmailAttachmentDTO {
428
+ id: string;
429
+ filename: string;
430
+ mimeType: string | null;
431
+ sizeBytes: number | null;
432
+ }
433
+
434
+ /** `GET /api/email/messages/[id]` — a message, its attachments, and the
435
+ * server-sanitized HTML body (the raw `bodyHtml` never crosses the wire). */
436
+ export interface MessageDetailDTO {
437
+ email: EmailDTO;
438
+ attachments: EmailAttachmentDTO[];
439
+ bodyHtmlSafe: string | null;
440
+ }
441
+
442
+ // ── Heartbeats ─────────────────────────────────────────────────────────────────
443
+
444
+ /** A heartbeat's schedule (jsonb). `cron` is read-only in v1 (the form locks it);
445
+ * create/update only accept once/interval/manual. `at` is an ISO string. */
446
+ export type HeartbeatScheduleSpecDTO =
447
+ | { kind: 'once'; at: string }
448
+ | { kind: 'interval'; every_minutes: number; jitter_minutes?: number }
449
+ | { kind: 'cron'; expr: string }
450
+ | { kind: 'manual' };
451
+
452
+ /** Where a heartbeat's reply is delivered (jsonb). */
453
+ export type HeartbeatSurfaceDTO = { kind: 'telegram'; chat_id: string } | { kind: 'web' };
454
+
455
+ /** Optional quiet-hours window (jsonb). null tz = use the profile timezone. */
456
+ export interface HeartbeatQuietHoursDTO {
457
+ from: string;
458
+ to: string;
459
+ tz?: string | null;
460
+ }
461
+
462
+ /** A heartbeat as returned by `GET /api/heartbeats(/[id])`. Dates are ISO
463
+ * strings. The server aliases its `HeartbeatSummary` to this so the wire shape
464
+ * and the consuming client can't drift. */
465
+ /** Alias kept for the heartbeats settings screens (formerly @server/lib/heartbeats). */
466
+ export type HeartbeatSummary = HeartbeatDTO;
467
+
468
+ export interface HeartbeatDTO {
469
+ id: string;
470
+ slug: string;
471
+ name: string;
472
+ description: string | null;
473
+ agentSlug: string;
474
+ skillSlug: string;
475
+ scheduleKind: 'once' | 'interval' | 'cron' | 'manual';
476
+ schedule: HeartbeatScheduleSpecDTO;
477
+ surface: HeartbeatSurfaceDTO;
478
+ nextFireAt: string | null;
479
+ lastFiredAt: string | null;
480
+ fireCount: number;
481
+ maxFires: number | null;
482
+ minIdleMinutes: number | null;
483
+ quietHours: HeartbeatQuietHoursDTO | null;
484
+ earliestAt: string | null;
485
+ cooldownMinutes: number | null;
486
+ state: Record<string, unknown>;
487
+ status: 'active' | 'paused' | 'completed' | 'cancelled';
488
+ completionReason: string | null;
489
+ createdAt: string;
490
+ updatedAt: string;
491
+ }
492
+
493
+ // ── Live turn streaming ─────────────────────────────────────────────────────────
494
+
495
+ /**
496
+ * The cross-client contract for live "what the agent is doing" updates during a
497
+ * turn — consumed identically by the web client and the Flutter companion (see
498
+ * `docs/live-turn-streaming.md`). One event stream unifies coarse status, tool
499
+ * activity, reasoning, and token deltas.
500
+ *
501
+ * This is the wire shape ONLY (zero-runtime, per this package's invariant): the
502
+ * server-side channel + publisher + schema-version constant live in
503
+ * `@mantle/turn-stream`; the producer stamps `v`/`seq`/`round`.
504
+ *
505
+ * Evolution rule: new `type`s and new `data` fields are additive (non-breaking) —
506
+ * a client ignores a `type` it doesn't recognise. A breaking change to an
507
+ * existing event's shape bumps `v` (`TURN_EVENT_SCHEMA_VERSION`).
508
+ */
509
+ export type TurnEventType =
510
+ | 'turn-start'
511
+ | 'status'
512
+ | 'tool-start'
513
+ | 'tool-end'
514
+ | 'reasoning-delta'
515
+ | 'text-delta'
516
+ | 'done'
517
+ | 'error';
518
+
519
+ /** A pending outbound message now exists; the client can bind UI to `turnId`. */
520
+ export interface TurnStartData {
521
+ agentSlug: string;
522
+ /** Resolved model id, when known at turn start (else null). */
523
+ model: string | null;
524
+ /** Durable `assistant_messages` id of the inbound (user) row, persisted before
525
+ * the model runs. Lets a client swap its optimistic user bubble for the
526
+ * canonical row without waiting on the POST. Optional (additive): a client
527
+ * that predates this field ignores it. */
528
+ inboundId?: string;
529
+ /** Durable `assistant_messages` id of the outbound (reply) row, inserted
530
+ * `pending` at turn start. This is the turn's authoritative reconciliation
531
+ * handle — the client binds the reply bubble to it and, on `done`, reads the
532
+ * final text from this row (vs. the advisory streamed buffer). Optional. */
533
+ outboundId?: string;
534
+ }
535
+
536
+ /** A short "what it's doing now" line ("Searching your brain…"). `kind` is an
537
+ * optional coarse bucket the UI can theme/iconify. `stepId` ties together the
538
+ * grounded line and its later narrated upgrade for the SAME step, so the client
539
+ * replaces the line in place rather than appending a duplicate. */
540
+ export interface TurnStatusData {
541
+ label: string;
542
+ kind?: string;
543
+ /** Stable id for the step this status describes. Two events sharing a stepId
544
+ * are the same step (grounded → narrated); the client upserts by it. */
545
+ stepId?: string;
546
+ /** Present (true) only on the narrator's rephrased line for a step — the warm
547
+ * first-person paragraph. Grounded lines omit it. Lets clients keep narrated
548
+ * text visible while later grounded lines tick past. */
549
+ narrated?: true;
550
+ }
551
+
552
+ /** A tool round began. `summary` is an optional one-line, secret-free preview. */
553
+ export interface TurnToolStartData {
554
+ name: string;
555
+ summary?: string;
556
+ }
557
+
558
+ /** A tool round finished (`ok=false` = it errored — the turn may still recover). */
559
+ export interface TurnToolEndData {
560
+ name: string;
561
+ ok: boolean;
562
+ }
563
+
564
+ /** A chunk of the model's reasoning stream (raw; may be curated before display). */
565
+ export interface TurnReasoningDeltaData {
566
+ text: string;
567
+ }
568
+
569
+ /** A chunk of the visible reply text. */
570
+ export interface TurnTextDeltaData {
571
+ text: string;
572
+ }
573
+
574
+ /** Terminal success. The client now reconciles against the durable message row;
575
+ * the streamed text is advisory, the DB row is authoritative. */
576
+ export interface TurnDoneData {
577
+ status: 'complete';
578
+ /** Real output-token total for the whole turn (summed across rounds). The
579
+ * client shows a streamed char-based estimate while the reply types out, then
580
+ * swaps it for this exact figure on `done`. Optional + additive: absent when
581
+ * no provider reported usage, or from a producer that predates the field. */
582
+ tokensOut?: number;
583
+ }
584
+
585
+ /** Terminal failure. */
586
+ export interface TurnErrorData {
587
+ status: 'failed';
588
+ message: string;
589
+ }
590
+
591
+ /** Fields every turn event carries. */
592
+ export interface TurnEventBase {
593
+ /** Schema version (`TURN_EVENT_SCHEMA_VERSION` at emit time). */
594
+ v: number;
595
+ /** Durable turn id = the outbound `assistant_messages` id. Stable for the turn. */
596
+ turnId: string;
597
+ /** Monotonic per-turn sequence — the SSE `id:` field and the resume cursor. */
598
+ seq: number;
599
+ /** Tool-loop round this event belongs to (0 = before the first round). */
600
+ round: number;
601
+ }
602
+
603
+ /** One live turn event. Discriminated on `type`; `data` is the matching payload. */
604
+ export type TurnEvent =
605
+ | (TurnEventBase & { type: 'turn-start'; data: TurnStartData })
606
+ | (TurnEventBase & { type: 'status'; data: TurnStatusData })
607
+ | (TurnEventBase & { type: 'tool-start'; data: TurnToolStartData })
608
+ | (TurnEventBase & { type: 'tool-end'; data: TurnToolEndData })
609
+ | (TurnEventBase & { type: 'reasoning-delta'; data: TurnReasoningDeltaData })
610
+ | (TurnEventBase & { type: 'text-delta'; data: TurnTextDeltaData })
611
+ | (TurnEventBase & { type: 'done'; data: TurnDoneData })
612
+ | (TurnEventBase & { type: 'error'; data: TurnErrorData });
613
+
614
+ // ── ask_human questionnaire (runner queues) ───────────────────────────────────
615
+ // THE single source of truth for the questionnaire contract. The plan parser
616
+ // (@mantle/tools) validates against these caps, the answer path (@mantle/runs)
617
+ // re-checks submissions against them, and the client renders whatever they
618
+ // admit. They lived in three places once and immediately disagreed — the
619
+ // client's id fallback diverged from the server's, and the client had no
620
+ // question cap while the API capped answers at 4, so a 5-question form
621
+ // rendered fine and then 400'd on submit.
622
+
623
+ /** One selectable answer. `description` is the muted subtext on the chip. */
624
+ export interface AskHumanFormOption {
625
+ label: string;
626
+ description?: string;
627
+ }
628
+
629
+ /** One sub-question of a questionnaire. `id` is the routing key answers are
630
+ * submitted under; `header` is the short chip shown beside the question. */
631
+ export interface AskHumanFormQuestion {
632
+ id: string;
633
+ header?: string;
634
+ question: string;
635
+ options: AskHumanFormOption[];
636
+ multi_select?: boolean;
637
+ /** Free-text escape. Defaults ON — a question whose options don't fit and
638
+ * offers no way to say so forces a wrong answer. */
639
+ allow_other?: boolean;
640
+ }
641
+
642
+ export interface AskHumanForm {
643
+ questions: AskHumanFormQuestion[];
644
+ }
645
+
646
+ /** One answered sub-question, as submitted to `PATCH /api/pending/:id` and
647
+ * `pending_approve`. `question` is the form question's `id`. */
648
+ export interface AskHumanFormAnswer {
649
+ question: string;
650
+ selected: string[];
651
+ other?: string;
652
+ }
653
+
654
+ /**
655
+ * Caps on a questionnaire. These are a CONTRACT, not advice: every answer
656
+ * surface renders whatever the parser admits, so an unbounded form is an
657
+ * unanswerable screen — and a cap enforced on only one side is a 400 the
658
+ * operator can't act on.
659
+ */
660
+ export const ASK_HUMAN_FORM_LIMITS = {
661
+ /** Ask more than this and the answers to the first few probably change what
662
+ * you still need to ask — use a later `ask_human` step. */
663
+ maxQuestions: 4,
664
+ maxOptions: 8,
665
+ /** A header renders as a chip, not a sentence. */
666
+ maxHeaderChars: 24,
667
+ maxQuestionChars: 300,
668
+ maxLabelChars: 80,
669
+ maxDescriptionChars: 200,
670
+ maxOtherChars: 2_000,
671
+ /** The form rides in `run_items.payload` AND the pending row's args, and
672
+ * both are read into prompts. */
673
+ maxFormJsonBytes: 8_000,
674
+ } as const;
675
+
676
+ // ── Row/DTO shapes moved from the server packages (jackdaw split P0) ─────────
677
+ // Sources: @mantle/content, @mantle/email, @mantle/microsoft, @mantle/agent-runtime
678
+ // re-export these names, so server code keeps its original import paths.
679
+
680
+ export type TaskRow = {
681
+ id: string;
682
+ title: string;
683
+ body: string;
684
+ status: TaskStatus;
685
+ priority: TaskPriority;
686
+ dueAt: string | null;
687
+ tags: string[];
688
+ summary: string | null;
689
+ createdAt: string;
690
+ updatedAt: string;
691
+ };
692
+
693
+ export type JournalRow = {
694
+ id: string;
695
+ title: string;
696
+ body: string;
697
+ mood: string | null;
698
+ category: string | null;
699
+ entryDate: string | null;
700
+ tags: string[];
701
+ summary: string | null;
702
+ createdAt: string;
703
+ updatedAt: string;
704
+ };
705
+
706
+ export type EventRow = {
707
+ id: string;
708
+ title: string;
709
+ body: string;
710
+ startsAt: string;
711
+ endsAt: string | null;
712
+ location: string | null;
713
+ remindMinutesBefore: number;
714
+ remindAt: string;
715
+ reminderSentAt: string | null;
716
+ /** IANA timezone (e.g. "Africa/Johannesburg") captured from the
717
+ * client at create time. Used for display only — `starts_at` is
718
+ * always a UTC instant so the reminder fires at the right moment
719
+ * regardless of where the agent process or DB run. Defaults to
720
+ * 'UTC' if the client didn't supply one. */
721
+ timezone: string;
722
+ /** Recurrence frequency; 'none' for a one-shot event. */
723
+ recur: RecurFreq;
724
+ /** Optional end-of-series cutoff (ISO). null = repeats until deleted. */
725
+ recurUntil: string | null;
726
+ tags: string[];
727
+ summary: string | null;
728
+ createdAt: string;
729
+ updatedAt: string;
730
+ };
731
+
732
+ /** Notion-style content width: centered/narrow vs full available space. */
733
+ export type PageWidth = 'narrow' | 'wide';
734
+
735
+ export type PageRow = {
736
+ id: string;
737
+ /** Parent page id, or null for a top-level page. Drives the /pages tree
738
+ * and the `childPage` card (Phase 4a sub-pages). */
739
+ parentId: string | null;
740
+ title: string;
741
+ icon: string | null;
742
+ tags: string[];
743
+ summary: string | null;
744
+ visibility: PageVisibility;
745
+ width: PageWidth;
746
+ createdAt: string;
747
+ updatedAt: string;
748
+ };
749
+
750
+ export type AppRow = {
751
+ id: string;
752
+ title: string;
753
+ icon: string | null;
754
+ tags: string[];
755
+ summary: string | null;
756
+ description: string | null;
757
+ /** Number of declared api_tool slugs. */
758
+ toolCount: number;
759
+ /** Whether the published source has a green build (renders today). */
760
+ hasBuild: boolean;
761
+ /** Whether an uncommitted draft exists. */
762
+ hasDraft: boolean;
763
+ /**
764
+ * The app's exposure: mode of its active share ('public' | 'team'), or null
765
+ * when it has never been shared / the share is revoked (owner-only).
766
+ */
767
+ shareMode: ShareMode | null;
768
+ /** Whether this app is the designated Team Hub (prefs.teamHubAppId). */
769
+ isHub: boolean;
770
+ createdAt: string;
771
+ updatedAt: string;
772
+ };
773
+
774
+ export type AppDetail = AppRow & {
775
+ source: AppSource;
776
+ draft: AppSource | null;
777
+ manifest: AppManifest;
778
+ draftBuild: BuildRef | null;
779
+ publishedBuild: BuildRef | null;
780
+ };
781
+
782
+ export type ProfilePreferences = {
783
+ /** IANA timezone, e.g. 'Africa/Johannesburg'. UTC when not set. */
784
+ timezone: string;
785
+ /** The last zone the auto-from-location hook DERIVED (not necessarily the one
786
+ * in `timezone`, if the user manually overrode since). Used purely for
787
+ * hysteresis: the hook only acts when the freshly-derived zone differs from
788
+ * this, so it won't fight a manual change or re-switch every turn at the same
789
+ * place. See auto-timezone.ts. */
790
+ lastAutoTimezone?: string;
791
+ /** BCP-47 locale, e.g. 'en-GB'. Drives date/number/currency
792
+ * formatting. Falls back to en-GB to match the legacy pinned
793
+ * format-datetime behaviour, so existing UI doesn't shift for
794
+ * users who haven't visited /settings/profile yet. */
795
+ locale: string;
796
+ /** Avatar style id — the BRAIN's avatar visual language, applied to every
797
+ * generated avatar (the owner's and every agent's). Brain-level alongside
798
+ * colorTheme and the display fonts, because it is a branding choice, not a
799
+ * personal one: one style with a different seed per entity reads as one
800
+ * product, six unrelated styles at once read as noise. Individuality lives
801
+ * in `avatarSeed`, which stays personal. See @mantle/web-ui/avatar for the
802
+ * registry; unknown ids resolve to the default rather than stranding. */
803
+ avatarStyle?: string;
804
+ /** How much of the theme generated avatars take on: 'native' (the style's own
805
+ * palette), 'mixed' (themed background, original artwork — the default) or
806
+ * 'theme' (theme colours throughout). Brain-level for the same reason as
807
+ * avatarStyle: it describes how this brain's avatars look, not one login's
808
+ * taste. Read via projectAvatarTint, never raw. */
809
+ avatarTint?: string;
810
+ /** Which generated background each area of the shell shows, as
811
+ * `area=style` pairs (`menu=waves,header=off`). Brain-level for the same
812
+ * reason as avatarStyle and colorTheme: it is the look of the product.
813
+ * `off` is a real, storable choice, see @mantle/web-ui/backgrounds. Areas
814
+ * on their default are omitted, so a default change still reaches brains
815
+ * that never chose. Read via projectBackgrounds, never raw. */
816
+ backgrounds?: string;
817
+ /** Seed for THIS user's avatar; the UI defaults it to the user id when unset
818
+ * so an avatar still renders. Personal — two admins share the brain's style
819
+ * but never the same avatar. */
820
+ avatarSeed?: string;
821
+ /** Slug of the responder agent whose Telegram bot delivers event reminders.
822
+ * Unset → the reminder worker falls back to the most-recently-active allowed
823
+ * DM (whichever bot you last messaged). Set it to pin reminders to one
824
+ * persona, e.g. 'telegram-default' (Saskia), so they don't come from
825
+ * whichever bot happened to be most recent. */
826
+ reminderAgentSlug?: string;
827
+ /** Where event reminders are delivered: 'telegram' (a bot DM) or 'mobile' (a
828
+ * push to the companion app). Auto-tracked — it follows the last channel the
829
+ * user actually messaged on (see noteInboundChannel), and can be set manually
830
+ * from the profile; a manual choice holds until the next message on the other
831
+ * channel supersedes it. Unset ⇒ the reminder worker defaults to 'telegram'
832
+ * (backward-compatible). See docs/reminder-delivery-routing.md. */
833
+ reminderChannel?: ReminderChannel;
834
+ /** What the user likes to be called (captured during onboarding). Cosmetic —
835
+ * the assistant's real knowledge of the user comes from the Journal identity
836
+ * block; this is for greetings/UI. */
837
+ displayName?: string;
838
+ /** Custom site name rendered as the header wordmark in place of "mantle" —
839
+ * a per-box label (e.g. 'Refinery') so anyone with several brains can see at
840
+ * a glance which one they're on. Cosmetic only; unset ⇒ the Mantle wordmark.
841
+ * Read via projectSiteName, never raw. */
842
+ siteName?: string;
843
+ /** This brain's peer name — shown in the header CENTRE (replacing the old page
844
+ * title) as this node's federation-facing identity label. Cosmetic; unset ⇒
845
+ * the header centre is empty. Read via projectPeerName, never raw. */
846
+ peerName?: string;
847
+ /** The owner's writing conventions, in their own words — appended to EVERY
848
+ * agent's composed system prompt as a `## House style` block (see
849
+ * composeSystemPromptWithSkills). Brain-level, because it describes how this
850
+ * brain writes, not how one login works.
851
+ *
852
+ * Free text rather than a checkbox on purpose: the first rule anyone wants
853
+ * is "no em dashes", the second is "don't say 'delve'", and a boolean per
854
+ * rule is a migration per taste. Unset ⇒ no block is emitted at all, so the
855
+ * cached prompt prefix is byte-identical to before the feature existed.
856
+ * Read via projectHouseStyle, never raw. */
857
+ houseStyle?: string;
858
+ /** The UI colour-theme id (the header theme toggler / random shuffle). The
859
+ * DB copy is the source of truth so the choice follows the owner across
860
+ * browsers and brands member-facing surfaces (/s, /team) — localStorage
861
+ * stays only as the before-paint fast path. Unset ⇒ the default theme.
862
+ * Read via projectColorTheme, never raw. */
863
+ colorTheme?: string;
864
+ /** Selectable header WORDMARK font key (Settings → Appearance → Fonts). The
865
+ * font LIST lives in the web app (apps/web/lib/display-fonts.ts); the server
866
+ * stores any well-formed slug and the client falls back to the default for
867
+ * keys it doesn't know, so trimming the library never strands the preference.
868
+ * Unset ⇒ the default wordmark face (Bricolage Grotesque). Read via
869
+ * projectFontKey, never raw. */
870
+ fontLogo?: string;
871
+ /** Selectable header page-TITLE font key — same contract as `fontLogo`.
872
+ * Unset ⇒ the default UI sans. Read via projectFontKey, never raw. */
873
+ fontTitle?: string;
874
+ /** The INTERFACE font key — what the whole UI is set in, not just a header
875
+ * ornament. Same contract as `fontLogo`; unset ⇒ Inter (the always-loaded
876
+ * next/font face). Read via projectFontKey, never raw. */
877
+ fontUi?: string;
878
+ /** The PAGES/NOTES font key — what long-form prose is set in, in the editor,
879
+ * on shared pages, and in the PDF export. Same contract as `fontLogo`; unset
880
+ * ⇒ 'inherit' (follow the interface font). This is the one slot where the
881
+ * choice leaves the browser: a page exported to PDF is typeset in it. */
882
+ fontProse?: string;
883
+ /** UI scale: 'xsmall' | 'small' | 'medium' | 'large'. Drives the ROOT
884
+ * font-size, so the rem-based shell scales with it rather than only the
885
+ * letters. Unset ⇒ 'medium'. Read via projectFontSize, never raw. */
886
+ fontSize?: string;
887
+ /** Wordmark scale — same vocabulary as `fontSize`, but a LOCAL multiplier on
888
+ * one element rather than the root font-size (a wordmark that rescaled the
889
+ * whole shell would be a bug). Unset ⇒ 'medium'. */
890
+ fontLogoSize?: string;
891
+ /** Peer-name scale. Same contract as `fontLogoSize`. */
892
+ fontTitleSize?: string;
893
+ /** Pages/Notes prose scale. Same contract as `fontLogoSize`. */
894
+ fontProseSize?: string;
895
+ /** Brand logo: the content-addressed storage key of the uploaded image
896
+ * (attachments/aa/bb/<sha256> — @mantle/storage contentKey). Set/cleared
897
+ * ONLY via PUT/DELETE /api/profile/logo, which validates the bytes; when
898
+ * set, both headers render the image in place of the siteName wordmark.
899
+ * The sha in the key doubles as the cache-busting version. Read via
900
+ * projectLogoKey, never raw. */
901
+ logoKey?: string;
902
+ /** The logo's mime type, from the validated upload (svg/png/jpeg/webp
903
+ * allowlist — projectLogoType). The public serve route replays it. */
904
+ logoType?: string;
905
+ /** Optional DARK-MODE logo variant — same storage/validation contract as
906
+ * logoKey, uploaded via PUT /api/profile/logo?variant=dark. Renderers show
907
+ * it when the UI is in dark mode and fall back to the base logo (then the
908
+ * wordmark) when unset — so a light-on-transparent mark stays readable on
909
+ * both themes without forcing every brain to upload two files. */
910
+ logoDarkKey?: string;
911
+ /** The dark variant's mime type (same allowlist as logoType). */
912
+ logoDarkType?: string;
913
+ /** Free-text "what this brain is for" — captured at onboarding, editable in
914
+ * Settings → Profile. Injected as the "# Purpose of this brain" section of the
915
+ * always-on identity block (identity-context.ts), so every agent knows the
916
+ * brain's mission. */
917
+ purpose?: string;
918
+ /** The brain's speciality archetype key (see onboarding-questions.ts
919
+ * PURPOSE_ARCHETYPES — 'personal' | 'analytics' | 'research' | 'robotics' |
920
+ * 'team' | 'custom'). Descriptive for now; the seam a later phase can branch
921
+ * default provisioning on. */
922
+ purposeArchetype?: string;
923
+ /** ISO instant onboarding was completed. Unset ⇒ the onboarding wizard runs
924
+ * on next login; the (app) shell redirects there. Set ⇒ shell renders normally. */
925
+ onboardedAt?: string;
926
+ /** Resume marker for the onboarding wizard — the key of the furthest step the
927
+ * user has reached. Lets a refreshed/re-entered wizard pick up where it left off. */
928
+ onboardingStep?: string;
929
+ /** Model choices captured by the onboarding "Models" step — the operator
930
+ * overlay `provisionDefaults()` applies on top of the manifest seed (the
931
+ * assistant's chat model + the indexing workers' fast model). When
932
+ * `route: 'azure'`, those rows are pinned to an Azure OpenAI endpoint via
933
+ * the `custom` provider (key stored under service `custom`). */
934
+ onboardingModels?: OnboardingModelChoices;
935
+ /** When true, tools an AGENT authors (via Toolsmith / api_tool_create) start
936
+ * confirm-gated: every call parks for operator approval until the operator
937
+ * clears "requires confirm" for that tool in Settings → Tools. Defaults
938
+ * OFF — a simple single-owner brain trusts itself; turn it ON if you grant
939
+ * tool-authoring to an agent that reads untrusted content (email/web), so an
940
+ * injected agent can't stand up a silent exfiltration endpoint. Independent
941
+ * of the always-on guards (self-grant block, no-lower-via-update, SSRF). */
942
+ toolsmithRequireApproval?: boolean;
943
+ /** APP_VERSION the boot-time manifest reconcile last synced this brain to.
944
+ * The reconcile (apps/web instrumentation → reconcileManifestOnBoot) runs once
945
+ * per version on a deployed/updated instance, so a self-hoster who only pulls a
946
+ * new image still gets new tools/skills/group-membership without running seed
947
+ * scripts. Equal to APP_VERSION ⇒ already reconciled, skip. */
948
+ lastReconciledVersion?: string;
949
+ /** When true, outbound/egress tools (email_send, web_fetch, web_search)
950
+ * fired during an UNATTENDED heartbeat run park for operator approval
951
+ * instead of executing inline. Only tools that reach OUT are gated — the
952
+ * heartbeat's own surface reply (the final Telegram message) is not a tool
953
+ * and still goes through. Defaults OFF: most heartbeats are trusted
954
+ * routines. Turn it ON for an agent that reads untrusted content on a
955
+ * timer, so an injected instruction can't silently email or fetch on your
956
+ * behalf while you're away. Pairs with the interactive Telegram approval
957
+ * card so a parked egress call can be cleared from a phone. */
958
+ heartbeatEgressGate?: boolean;
959
+ /** Show the live "thinking" trail + stream the reply token-by-token in the
960
+ * /assistant chat (and the companion). **Defaults ON** (undefined → on); set
961
+ * false to fall back to a static thinking bubble + the reply appearing whole
962
+ * on completion. This is the per-brain runtime control for live turn
963
+ * streaming; the `MANTLE_TURN_STREAMING` env var is a deploy-level override
964
+ * (env off wins). Read by the web turn route (202 vs blocking + the SSE gate)
965
+ * via `isStreamThoughtsEnabled`. */
966
+ streamThoughts?: boolean;
967
+ /** How the LIVE thinking trail renders during a turn: 'list' stacks completed
968
+ * actions above the active line (default); 'replace' shows only the current
969
+ * action, each one replacing the last (compact, single line). The frozen
970
+ * record view (after the turn) is unaffected. */
971
+ thoughtTrailMode?: ThoughtTrailMode;
972
+ /** Persist the thought trail onto the finished message so it survives a page
973
+ * refresh — reconstructed from the turn's tool actions and stored on the
974
+ * durable row, so it reloads on web AND the companion. **Defaults ON**; set
975
+ * false to keep it ephemeral (in-memory only; clears on reload). See
976
+ * `isPersistThoughtsEnabled`. */
977
+ persistThoughts?: boolean;
978
+ /** Per-user thinking budget in tokens. Real model reasoning is requested only
979
+ * when the live-thinking switch is ON (`streamThoughts`) AND this is > 0;
980
+ * 0 / unset = no thinking. Maps to the provider's knob in the adapters
981
+ * (Anthropic adaptive, OpenRouter `reasoning.max_tokens`, Gemini
982
+ * `thinkingConfig`, Copilot `reasoning_effort`). This is the per-user
983
+ * replacement for the old per-box `MANTLE_THINKING_BUDGET` env gate. Resolve
984
+ * via `resolveThinkingBudget` — never read raw, so the switch gate always
985
+ * applies. **Defaults unset (off).** */
986
+ thinkingBudget?: number;
987
+ /** Whether this box exposes its remote MCP connector (the OAuth-gated
988
+ * `/api/mcp` endpoint addable as a claude.ai custom connector). **Defaults
989
+ * OFF** — it's an explicit opt-in because it puts the tool surface on the
990
+ * public internet (behind OAuth). When off, `/api/mcp` + the OAuth
991
+ * authorize/register endpoints 404, so no new client can connect and existing
992
+ * tokens stop working. Flip it in Settings → MCP. */
993
+ remoteMcpEnabled?: boolean;
994
+ /** Whether the external Team Chat responder may read the owner's PRIVATE
995
+ * corpus — email + journal — on a team member's behalf. **Defaults OFF**:
996
+ * team members always get brain-wide knowledge reads (search, files, notes,
997
+ * pages, tables, tasks, contacts, app data), but the owner's personal email
998
+ * history and journal stay off-limits unless this is explicitly turned on.
999
+ * Enforced at the team turn's tool resolution (`isTeamPrivateReadsEnabled`
1000
+ * strips `email_*`/`journal_*` when off), independent of the `team-read`
1001
+ * group grant, so the switch can't be bypassed by a manifest change. Flip it
1002
+ * from the Team admin surface. */
1003
+ teamPrivateReads?: boolean;
1004
+ /** Node id of the mini-app designated as this brain's TEAM HUB. When set (and
1005
+ * the app has a green published build + an active team-mode share), the /team
1006
+ * shell renders that app full-bleed in place of the built-in hub body; the
1007
+ * built-in hub remains the fallback for every other state. Resolve via
1008
+ * `resolveTeamHubApp` (team-hub.ts), never raw — designation is only honoured
1009
+ * when the whole chain (pref → app → build → share) is intact. Read via
1010
+ * projectTeamHubAppId, never raw. */
1011
+ teamHubAppId?: string;
1012
+ /** Tags the owner curates as Dashboard sections on the /team overview: each
1013
+ * tag renders a section of up to 5 team-visible shared pages carrying it
1014
+ * (newest-updated first, title + summary + /s link). Order here = section
1015
+ * order. The share stays the single source of truth for WHAT is visible —
1016
+ * this pref only chooses which tag groupings get pinned. Unset/empty ⇒ no
1017
+ * curated sections. Read via projectTeamHubTags, never raw. */
1018
+ teamHubTags?: string[];
1019
+ };
1020
+
1021
+ export type BackupConfig = {
1022
+ enabled: boolean;
1023
+ frequency: BackupFrequency;
1024
+ /** Hour of day (0-23) in the USER's timezone (profiles.preferences.timezone). */
1025
+ hour: number;
1026
+ /** Newest N dumps retained in the directory. */
1027
+ keep: number;
1028
+ /** Absolute destination directory. Empty/unset → resolveBackupDir default. */
1029
+ location?: string;
1030
+ };
1031
+
1032
+ export type BackupFile = { name: string; bytes: number; mtime: string };
1033
+
1034
+ export type BackupStatus = {
1035
+ lastRunAt: string;
1036
+ ok: boolean;
1037
+ /** Set when ok=false. */
1038
+ error?: string;
1039
+ file?: string;
1040
+ bytes?: number;
1041
+ durationMs?: number;
1042
+ /** 'schedule' | 'manual' — what triggered the run. */
1043
+ trigger: string;
1044
+ /** When the last SUCCESSFUL run finished — preserved across failed runs,
1045
+ * so the /debug/integrity staleness check can tell "failing for a week"
1046
+ * from "failed once after last night's good dump". */
1047
+ lastSuccessAt?: string;
1048
+ /** Sqlite-native table workbooks snapshotted beside the dump (durability
1049
+ * gate 2). failed>0 is surfaced in the settings card — a backup that
1050
+ * silently skips a workbook is the gap this closes. */
1051
+ tableDbs?: { snapshotted: number; missing: number; failed: number };
1052
+ /** Per-app mini-app SQLite databases snapshotted beside the dump. Same
1053
+ * durability gate as tableDbs: these live on their own volume, so pg_dump
1054
+ * alone misses them and a scheduled backup would silently omit all app
1055
+ * data (e.g. a Team Hub app's DB) without this pass. */
1056
+ appDbs?: { snapshotted: number; missing: number; failed: number };
1057
+ };
1058
+
1059
+ export type CuratedTeamSection = {
1060
+ /** The curated tag — the section heading (display-cased by the UI). */
1061
+ tag: string;
1062
+ /** Up to {@link TEAM_CURATED_SECTION_LIMIT} team-visible page shares carrying
1063
+ * the tag, newest node update first. */
1064
+ items: TeamVisibleShare[];
1065
+ };
1066
+
1067
+ export type TeamMemberActivity = {
1068
+ contactId: string;
1069
+ /** Contact node title; '(deleted contact)' can't occur here — membership
1070
+ * rows cascade with the contact. */
1071
+ contactName: string;
1072
+ memberSince: string;
1073
+ tokenLastUsedAt: string | null;
1074
+ lastMessageAt: string | null;
1075
+ lastMessageText: string | null;
1076
+ lastMessageDirection: 'inbound' | 'outbound' | null;
1077
+ messageCount: number;
1078
+ /** Member inbound messages since the owner last read this thread in
1079
+ * /team-admin (all inbound when never read). Drives the unread badge. */
1080
+ unread: number;
1081
+ };
1082
+
1083
+ export type TeamRequest = {
1084
+ taskId: string;
1085
+ title: string;
1086
+ body: string;
1087
+ status: 'open' | 'done';
1088
+ priority: string;
1089
+ createdAt: string;
1090
+ /** Provenance from data.teamRequest — null contactId means a malformed row
1091
+ * (shouldn't happen; team_request_create always stamps it). */
1092
+ contactId: string | null;
1093
+ contactName: string | null;
1094
+ /** When the owner last posted a resolution to the member for this request. */
1095
+ notifiedAt: string | null;
1096
+ };
1097
+
1098
+ export type ForumTopicListItem = {
1099
+ id: string;
1100
+ title: string;
1101
+ kind: ForumTopicKind;
1102
+ visibility: ForumTopicVisibility;
1103
+ pinned: boolean;
1104
+ status: ForumTopicStatus;
1105
+ authorName: string;
1106
+ createdByContactId: string | null;
1107
+ postCount: number;
1108
+ lastPostAt: string;
1109
+ createdAt: string;
1110
+ lastPostAuthor: string | null;
1111
+ lastPostPreview: string | null;
1112
+ /** Posts by OTHERS since this viewer last read the topic (all of them when
1113
+ * never read). Drives the unread dot. */
1114
+ unread: number;
1115
+ };
1116
+
1117
+ export type ForumMemberActivity = {
1118
+ contactId: string;
1119
+ postCount: number;
1120
+ topicsStarted: number;
1121
+ lastPostAt: string | null;
1122
+ lastPostBody: string | null;
1123
+ lastPostTopicTitle: string | null;
1124
+ /** This member's posts newer than the OWNER's read cursor on the containing
1125
+ * topic. Deliberately only cleared by opening the TOPIC — reading someone's
1126
+ * activity feed is not reading the thread the whole room saw. */
1127
+ unread: number;
1128
+ };
1129
+
1130
+ export type ForumMemberPost = {
1131
+ id: string;
1132
+ body: string;
1133
+ createdAt: string;
1134
+ /** Set when this post filed a review/feature/bug request. */
1135
+ kind: ForumPostRequestKind | null;
1136
+ attachments: ConversationAttachment[];
1137
+ topicId: string;
1138
+ topicTitle: string;
1139
+ topicVisibility: ForumTopicVisibility;
1140
+ topicStatus: ForumTopicStatus;
1141
+ /** The agent's answer to THIS post, or null when the turn was waved off
1142
+ * ("no answer needed") or is still owed. */
1143
+ reply: {
1144
+ id: string;
1145
+ body: string;
1146
+ authorName: string;
1147
+ traceId: string | null;
1148
+ status: 'pending' | 'complete' | 'failed';
1149
+ error: string | null;
1150
+ createdAt: string;
1151
+ } | null;
1152
+ };
1153
+
1154
+ export type ForumAuthoredTopic = {
1155
+ id: string;
1156
+ title: string;
1157
+ kind: ForumTopicKind;
1158
+ visibility: ForumTopicVisibility;
1159
+ status: ForumTopicStatus;
1160
+ pinned: boolean;
1161
+ postCount: number;
1162
+ lastPostAt: string | null;
1163
+ createdAt: string;
1164
+ };
1165
+
1166
+ export type PendingForumUpload = {
1167
+ id: string;
1168
+ topicId: string | null;
1169
+ postId: string | null;
1170
+ topicTitle: string | null;
1171
+ contactId: string | null;
1172
+ contactName: string | null;
1173
+ filename: string;
1174
+ mime: string;
1175
+ sizeBytes: number;
1176
+ createdAt: string;
1177
+ };
1178
+
1179
+ export type AccountFoldersResult =
1180
+ | {
1181
+ ok: true;
1182
+ address: string;
1183
+ /** Every folder the server reports right now (the pick list). */
1184
+ allFolders: string[];
1185
+ /** The current explicit allow-list, or null = "scan all non-excluded". */
1186
+ included: string[] | null;
1187
+ /** Folders the operator opted OUT of (rendered disabled). */
1188
+ excluded: string[];
1189
+ /** Folders the sync has actually touched (per the cursor). */
1190
+ scanned: string[];
1191
+ }
1192
+ | { ok: false; error: string };
1193
+
1194
+ export interface FolderFacet {
1195
+ folder: string;
1196
+ count: number;
1197
+ unread: number;
1198
+ }
1199
+
1200
+ export interface MessageListItem {
1201
+ id: string;
1202
+ fromAddr: string;
1203
+ fromName: string | null;
1204
+ subject: string | null;
1205
+ snippet: string | null;
1206
+ internalDate: Date;
1207
+ isRead: boolean;
1208
+ }
1209
+
1210
+ export interface MsConfigStatus {
1211
+ configured: boolean;
1212
+ /** Where the active config comes from — drives the UI ("set here" vs "from
1213
+ * environment, read-only"). */
1214
+ source: 'db' | 'env' | null;
1215
+ clientId: string | null;
1216
+ tenant: string;
1217
+ redirectUri: string | null;
1218
+ /** Masked secret for display; never the plaintext. */
1219
+ secretMasked: string | null;
1220
+ }
1221
+
1222
+ /** One retrieved (or near-miss) item: capped text + its ranking distance. */
1223
+ export type SnapshotItem = {
1224
+ text: string;
1225
+ /** Ranking distance (cosine, salience/recency-adjusted where the section
1226
+ * ranks that way). Null for always-injected items (preferences) that
1227
+ * bypass the vector race. */
1228
+ dist: number | null;
1229
+ kind?: string | null;
1230
+ entity?: string | null;
1231
+ nodeId?: string | null;
1232
+ title?: string | null;
1233
+ heading?: string | null;
1234
+ };
1235
+
1236
+ export type ContextSnapshot = {
1237
+ query: {
1238
+ /** The inbound text as given to retrieval (snipped). */
1239
+ inbound: string;
1240
+ /** The anaphora-enriched text actually embedded, when it differs. */
1241
+ enriched: string | null;
1242
+ /** False when embedding was skipped or failed — retrieval ran blind. */
1243
+ embedded: boolean;
1244
+ };
1245
+ facts: { sent: SnapshotItem[]; dropped: SnapshotItem[]; guard: number };
1246
+ contentHits: { sent: SnapshotItem[]; dropped: SnapshotItem[]; cutoff: number };
1247
+ chunkHits: { sent: SnapshotItem[]; dropped: SnapshotItem[]; cutoff: number };
1248
+ relations: string[];
1249
+ digests: { count: number; topics: string[] };
1250
+ history: {
1251
+ count: number;
1252
+ /** How many outbound turns carried a [tool record: …] read-back suffix. */
1253
+ toolRecords: number;
1254
+ /** How many turns carried a [media record: …] read-back suffix. */
1255
+ mediaRecords: number;
1256
+ };
1257
+ personaNotes: { count: number };
1258
+ corpusMap: { count: number; truncated: boolean };
1259
+ };
1260
+
1261
+ export type BackupFrequency = 'daily' | 'weekly';
1262
+
1263
+ export type PageVisibility = 'private' | 'public';
1264
+
1265
+ /**
1266
+ * Recurrence frequencies an event can repeat on. `none` is the default —
1267
+ * a one-shot event. The reminder worker rolls a recurring event's single
1268
+ * row forward to its next occurrence after each ping (no instance
1269
+ * materialisation), so one node always represents the next upcoming hit.
1270
+ */
1271
+ export type RecurFreq = 'none' | 'daily' | 'weekly' | 'monthly' | 'yearly';
1272
+
1273
+ /** Transports that can deliver a reminder out-of-band. A browser ('web') can't
1274
+ * receive a push, so it never becomes a reminder target. */
1275
+ export type ReminderChannel = 'telegram' | 'mobile';
1276
+
1277
+ /** Live thinking-trail display modes. */
1278
+ export type ThoughtTrailMode = 'list' | 'replace';
1279
+
1280
+ /** The onboarding "Models" step's stored choices. Kept as one object so the
1281
+ * projection can't half-apply; every field optional so partial saves survive. */
1282
+ export interface OnboardingModelChoices {
1283
+ /** OpenRouter slug for the assistant/persona agent (e.g. `anthropic/claude-sonnet-4.6`). */
1284
+ assistantModel?: string;
1285
+ /** OpenRouter slug for the indexing workers (e.g. `google/gemini-3.1-flash-lite`). */
1286
+ workerModel?: string;
1287
+ /** Where the models run: OpenRouter (default) or an Azure OpenAI endpoint. */
1288
+ route?: 'openrouter' | 'azure';
1289
+ /** Azure OpenAI base URL (the OpenAI-compatible v1 endpoint), when route=azure. */
1290
+ azureBaseUrl?: string;
1291
+ }
1292
+
1293
+ /**
1294
+ * Who a share admits. Lives in `shares.settings.mode` (absent = 'public', so
1295
+ * every pre-existing share keeps its behavior).
1296
+ *
1297
+ * public — anyone with the link (the original model).
1298
+ * team — the visitor must additionally present a live team credential
1299
+ * (see @mantle/content/team-tokens). Enforced for every kind on
1300
+ * the /s/ surface (page render, asset bytes, app brokers).
1301
+ * Team-mode PAGE shares double as the /team hub's briefing
1302
+ * sections (see ./team-hub).
1303
+ */
1304
+ export type ShareMode = 'public' | 'team';
1305
+
1306
+ export type TeamVisibleShare = {
1307
+ /** Share token — the workspace opens /s/<token>. */
1308
+ token: string;
1309
+ nodeId: string;
1310
+ title: string;
1311
+ icon: string | null;
1312
+ summary: string | null;
1313
+ updatedAt: string;
1314
+ /** 'team' or 'public' — a member may open both, the badge tells them apart. */
1315
+ mode: 'team' | 'public';
1316
+ /** Parent node id — lets the pages section rebuild the sub-page tree over
1317
+ * the SHARED subset (an unshared parent leaves its children as roots). */
1318
+ parentId: string | null;
1319
+ tags: string[];
1320
+ };
1321
+
1322
+ // ── Mirrors of @mantle/db jsonb/enum shapes (jackdaw split P0) ────────────────
1323
+ // Kept standalone so this package stays zero-dep (same convention as ToolHandler
1324
+ // above). Drift is caught where the server builds these DTOs from db rows —
1325
+ // an incompatible change there is a compile error at the row-builder.
1326
+
1327
+ /** Task lifecycle vocabulary — mirrors content's TASK_STATUSES/TASK_PRIORITIES
1328
+ * consts, which are `satisfies`-checked against these unions. */
1329
+ export type TaskStatus = 'open' | 'done';
1330
+ export type TaskPriority = 'low' | 'normal' | 'high';
1331
+
1332
+ /** Mirrors @mantle/db `ForumTopicKind`. */
1333
+ export type ForumTopicKind = 'question' | 'review' | 'feature' | 'bug' | 'discussion';
1334
+ /** Mirrors @mantle/db `ForumTopicVisibility`. */
1335
+ export type ForumTopicVisibility = 'team' | 'private';
1336
+ /** Mirrors @mantle/db `ForumTopicStatus`. */
1337
+ export type ForumTopicStatus = 'open' | 'answered' | 'closed';
1338
+ /** Mirrors @mantle/db `ForumPostRequestKind` — the topic kinds that file an
1339
+ * owner review task. */
1340
+ export type ForumPostRequestKind = 'review' | 'feature' | 'bug';
1341
+
1342
+ /** Mirrors @mantle/db `ConversationAttachment` (jsonb on conversation rows). */
1343
+ export type ConversationAttachment = {
1344
+ kind: 'image' | 'audio' | 'voice' | 'document' | 'video';
1345
+ mime?: string;
1346
+ caption?: string;
1347
+ nodeId?: string;
1348
+ fileId?: string;
1349
+ url?: string;
1350
+ };
1351
+
1352
+ /** Mirrors @mantle/db `AppSource` — a mini app's virtual file tree. */
1353
+ export type AppSource = {
1354
+ /** Path of the entry module within `files`; must `export default App`. */
1355
+ entry: string;
1356
+ /** path → TSX/TS source. Bounded (~30 files / ~256 KB) to stay a mini app. */
1357
+ files: Record<string, string>;
1358
+ };
1359
+
1360
+ /** Mirrors @mantle/db `AppManifest` — the runtime contract for a running app. */
1361
+ export type AppManifest = {
1362
+ toolSlugs?: string[];
1363
+ sqlite?: { schemaSql: string; schemaVersion: number };
1364
+ description?: string;
1365
+ };
1366
+
1367
+ /** Mirrors @mantle/db `BuildRef` — pointer to a bundled artifact in storage. */
1368
+ export type BuildRef = {
1369
+ storageKey: string;
1370
+ sha256: string;
1371
+ builtAt: string;
1372
+ esbuildVersion: string;
1373
+ bytes: number;
1374
+ ok: boolean;
1375
+ warnings?: string[];
1376
+ };
1377
+
1378
+ // ── Redacted account DTOs (hand-mirrored; jackdaw split P0) ───────────────────
1379
+ // These mirror db-derived server types (`Omit<EmailAccount,…>` etc.) that can't
1380
+ // be re-exported without dragging the postgres type graph in. Timestamps are
1381
+ // ISO strings here — the wire truth — where the server-side originals carry
1382
+ // `Date`. Key-set drift checks live next to the server definitions
1383
+ // (email/accounts.ts, microsoft/accounts.ts, email's sync-runs consumer).
1384
+
1385
+ /** Mirrors @mantle/email `PublicEmailAccount` (an `email_accounts` row minus
1386
+ * the sealed IMAP secret). */
1387
+ export interface PublicEmailAccount {
1388
+ id: string;
1389
+ userId: string;
1390
+ provider: 'gmail' | 'microsoft' | 'imap';
1391
+ address: string;
1392
+ displayName: string | null;
1393
+ imapHost: string | null;
1394
+ imapPort: number | null;
1395
+ imapSecure: boolean;
1396
+ smtpHost: string | null;
1397
+ smtpPort: number | null;
1398
+ smtpSecure: boolean;
1399
+ /** @deprecated historical reads only (migration 0002). */
1400
+ imapFolders: string[];
1401
+ imapExcludedFolders: string[];
1402
+ imapIncludedFolders: string[] | null;
1403
+ firstScanDays: number;
1404
+ ingestPolicy: 'approve_list' | 'block_list';
1405
+ branchPath: string;
1406
+ msAccountId: string | null;
1407
+ syncState: Record<string, unknown>;
1408
+ lastSyncAt: string | null;
1409
+ lastSyncError: string | null;
1410
+ enabled: boolean;
1411
+ createdAt: string;
1412
+ updatedAt: string;
1413
+ }
1414
+
1415
+ /** Mirrors @mantle/db `SyncRun` (a `sync_runs` row) as it crosses the wire. */
1416
+ export interface SyncRun {
1417
+ id: string;
1418
+ accountId: string;
1419
+ startedAt: string;
1420
+ finishedAt: string | null;
1421
+ durationMs: number | null;
1422
+ status: 'running' | 'ok' | 'error';
1423
+ scanned: number;
1424
+ ingested: number;
1425
+ error: string | null;
1426
+ }
1427
+
1428
+ /** Mirrors @mantle/microsoft `PublicMsAccount` (an `ms_accounts` row with the
1429
+ * sealed OAuth tokens replaced by presence flags). */
1430
+ export interface PublicMsAccount {
1431
+ id: string;
1432
+ userId: string;
1433
+ upn: string;
1434
+ displayName: string | null;
1435
+ tenantId: string | null;
1436
+ tokenExpiresAt: string | null;
1437
+ scopes: string[];
1438
+ branchPath: string;
1439
+ surfaces: Record<string, boolean>;
1440
+ syncState: Record<string, unknown>;
1441
+ lastSyncAt: string | null;
1442
+ lastSyncError: string | null;
1443
+ enabled: boolean;
1444
+ createdAt: string;
1445
+ updatedAt: string;
1446
+ hasAccessToken: boolean;
1447
+ hasRefreshToken: boolean;
1448
+ }
1449
+
1450
+ // ── Server-lib view/query DTOs (jackdaw split P0 follow-up: @server/* purge) ──
1451
+ // Moved from server/web/lib/* and @mantle/content; the originals re-export
1452
+ // these names so server import paths are unchanged.
1453
+
1454
+ /** Sort order for the pages list. 'edited' (last updated) is the default. */
1455
+ export type PageSort = 'edited' | 'newest' | 'oldest' | 'title';
1456
+
1457
+ /** A node that links TO a given page — one inbound `references` edge, resolved
1458
+ * to its source node. Powers the "Referenced by" panel. */
1459
+ export type Backlink = {
1460
+ id: string;
1461
+ title: string;
1462
+ /** The source node's type (wire truth: the db enum widens to string here). */
1463
+ type: string;
1464
+ icon: string | null;
1465
+ };
1466
+
1467
+ export type CapacityZone = 'green' | 'watch' | 'split';
1468
+
1469
+ export type CapacityMetric = {
1470
+ count: number;
1471
+ watch: number;
1472
+ split: number;
1473
+ /** count / split — may exceed 1 when the split point is passed. */
1474
+ ratio: number;
1475
+ zone: CapacityZone;
1476
+ };
1477
+
1478
+ export type BrainCapacity = {
1479
+ docs: CapacityMetric;
1480
+ chunkVectors: CapacityMetric;
1481
+ /** Worst zone across both axes — the brain's headline state. */
1482
+ zone: CapacityZone;
1483
+ /** Worst-axis fill as an integer percentage of the split budget (may exceed 100). */
1484
+ pctOfSplit: number;
1485
+ };
1486
+
1487
+ export type AgentContext = {
1488
+ agentId: string;
1489
+ agentName: string | null;
1490
+ agentSlug: string | null;
1491
+ modelSlug: string;
1492
+ lastTokensIn: number;
1493
+ contextLimit: number | null;
1494
+ /** Where contextLimit came from: live OpenRouter data, the static
1495
+ * fallback, or unknown (slug not in either). Surfaced in the UI. */
1496
+ contextSource: ContextSource;
1497
+ pct: number | null;
1498
+ lastRunAt: string;
1499
+ };
1500
+
1501
+ export type SpendRange = 'day' | 'week' | 'month';
1502
+
1503
+ export type AgentSpend = {
1504
+ agentId: string | null;
1505
+ agentName: string | null;
1506
+ agentSlug: string | null;
1507
+ costMicroUsd: number;
1508
+ tokensIn: number;
1509
+ tokensOut: number;
1510
+ cacheReadTokens: number;
1511
+ runs: number;
1512
+ };
1513
+
1514
+ export type ModelSpend = {
1515
+ /** The OpenRouter model slug captured in trace_steps.meta.model. */
1516
+ model: string;
1517
+ costMicroUsd: number;
1518
+ tokensIn: number;
1519
+ tokensOut: number;
1520
+ cacheReadTokens: number;
1521
+ calls: number;
1522
+ };
1523
+
1524
+ export type DailySpend = {
1525
+ /** ISO date (YYYY-MM-DD) in the server's local timezone. */
1526
+ day: string;
1527
+ costMicroUsd: number;
1528
+ tokensIn: number;
1529
+ tokensOut: number;
1530
+ cacheReadTokens: number;
1531
+ runs: number;
1532
+ };
1533
+
1534
+ export type RecentFailure = {
1535
+ id: string;
1536
+ kind: string;
1537
+ startedAt: string;
1538
+ error: string;
1539
+ };
1540
+
1541
+ export type TopError = {
1542
+ message: string;
1543
+ count: number;
1544
+ lastAt: string;
1545
+ lastTraceId: string;
1546
+ };
1547
+
1548
+ /** Per-tool tallies of calls the central validator flagged. Clean calls
1549
+ * write no `arg_validation` meta at all, so these are problem counts,
1550
+ * not rates — an empty result means nothing was flagged, not no calls. */
1551
+ export type ToolValidationAgg = {
1552
+ tool: string;
1553
+ flaggedCalls: number;
1554
+ withRepairs: number;
1555
+ withUnknownKeys: number;
1556
+ withViolations: number;
1557
+ lastAt: string;
1558
+ };
1559
+
1560
+ export type ToolValidationEvent = {
1561
+ stepId: string;
1562
+ traceId: string;
1563
+ tool: string;
1564
+ mode: string;
1565
+ repairs: Array<{ key: string; kind: string; note: string }>;
1566
+ unknownKeys: Array<{ key: string; suggestion: string | null }>;
1567
+ violations: string[];
1568
+ startedAt: string;
1569
+ };
1570
+
1571
+ export type AgentActivityRow = {
1572
+ id: string;
1573
+ slug: string;
1574
+ name: string;
1575
+ role: string;
1576
+ model: string;
1577
+ priority: number;
1578
+ enabled: boolean;
1579
+ lastUsedAt: string | null;
1580
+ usageCount: number;
1581
+ };
1582
+
1583
+ export type ChatRow = {
1584
+ id: string;
1585
+ title: string | null;
1586
+ username: string | null;
1587
+ telegramChatId: string;
1588
+ allowlistStatus: string;
1589
+ totalTurns: number;
1590
+ digested: number;
1591
+ undigested: number;
1592
+ lastActivity: string | null;
1593
+ responderAgentId: string | null;
1594
+ };
1595
+
1596
+ export type PersonaNotesRow = {
1597
+ agentId: string;
1598
+ agentName: string;
1599
+ agentSlug: string;
1600
+ notes: PersonaNoteDTO[];
1601
+ };
1602
+
1603
+ export type ContentIndexCoverage = {
1604
+ total: number;
1605
+ indexed: number;
1606
+ byType: Array<{ type: string; total: number; indexed: number }>;
1607
+ };
1608
+
1609
+ /**
1610
+ * Awareness of duplicate graph edges. Going forward the extractor rebuilds
1611
+ * edges per node (idempotent), but content re-edited *before* that fix may
1612
+ * carry historical duplicate `mentioned_in` / `references` rows. This surfaces
1613
+ * the count + a few labelled samples so the operator knows to run
1614
+ * `pnpm dedupe:edges`. Read-only — cleaning stays the deliberate CLI tool.
1615
+ */
1616
+ export type DuplicateEdgeStats = {
1617
+ groups: number; // logical edges with >1 row
1618
+ redundant: number; // rows that could be removed (sum of count-1)
1619
+ samples: { relation: string; label: string; count: number }[];
1620
+ };
1621
+
1622
+ /** One responder turn: the question, the retrieval snapshot the turn's
1623
+ * 'load_context' trace step persisted (null for pre-instrumentation turns),
1624
+ * and the outbound reply. See ContextSnapshot in @mantle/agent-runtime. */
1625
+ /** Mirrors @mantle/tracing `ContextSource`. */
1626
+ export type ContextSource = 'live' | 'fallback' | 'unknown';
1627
+
1628
+ export type ContextTurnRow = {
1629
+ traceId: string;
1630
+ startedAt: string;
1631
+ status: string;
1632
+ surface: string | null;
1633
+ agentSlug: string | null;
1634
+ model: string | null;
1635
+ durationMs: number | null;
1636
+ question: string | null;
1637
+ snapshot: ContextSnapshot | null;
1638
+ response: string | null;
1639
+ };
1640
+
1641
+ export type DigestRow = {
1642
+ id: string;
1643
+ title: string;
1644
+ createdAt: string;
1645
+ /** All fields below are pulled out of nodes.data (jsonb). */
1646
+ chatId: string;
1647
+ telegramChatId: string | null;
1648
+ periodStart: string;
1649
+ periodEnd: string;
1650
+ sourceTurnCount: number;
1651
+ model: string;
1652
+ agent: string;
1653
+ summary: string;
1654
+ topic: string | null;
1655
+ topicSlug: string | null;
1656
+ };
1657
+
1658
+ export type FactRow = {
1659
+ id: string;
1660
+ content: string;
1661
+ kind: string;
1662
+ confidence: number;
1663
+ entityName: string | null;
1664
+ entityKind: string | null;
1665
+ sourceNodeId: string | null;
1666
+ sourceTitle: string | null;
1667
+ createdAt: string;
1668
+ };
1669
+
1670
+ export type TopicRow = {
1671
+ topic: string;
1672
+ topicSlug: string;
1673
+ digestCount: number;
1674
+ turnCount: number;
1675
+ firstSeen: string;
1676
+ lastSeen: string;
1677
+ };
1678
+
1679
+ /** One key/count bucket in the corpus histograms below. */
1680
+ export type Bucket = { key: string; count: number };
1681
+
1682
+ export type BrainCounts = {
1683
+ nodesTotal: number;
1684
+ nodesByType: Bucket[];
1685
+ factsTotal: number;
1686
+ factsByKind: Bucket[];
1687
+ entitiesTotal: number;
1688
+ entitiesByKind: Bucket[];
1689
+ edgesTotal: number;
1690
+ edgesByRelation: Bucket[];
1691
+ };
1692
+
1693
+ /** A health check, not a fixer. Counts active edges that share the same
1694
+ * (source, target, relation) — i.e. duplicates. The extractor's
1695
+ * delete-then-rebuild discipline (see architecture §9k) means this should
1696
+ * stay 0; a non-zero value flags a regression in edge writing. The remedy is
1697
+ * the one-shot `pnpm dedupe:edges --apply`, NOT a recurring auto-clean (which
1698
+ * would mask the regression). */
1699
+ export type GraphIntegrity = {
1700
+ /** Distinct (source, target, relation) groups with more than one row. */
1701
+ duplicateEdgeGroups: number;
1702
+ /** Total redundant rows across those groups (Σ count-1) — how many
1703
+ * `dedupe:edges --apply` would remove. */
1704
+ redundantEdgeRows: number;
1705
+ };
1706
+
1707
+ export type VectorCounts = {
1708
+ nodesIndexed: number;
1709
+ nodesTotal: number;
1710
+ factsIndexed: number;
1711
+ factsTotal: number;
1712
+ entitiesIndexed: number;
1713
+ entitiesTotal: number;
1714
+ /** The headline: total embedded vectors across nodes + facts + entities. */
1715
+ vectorsTotal: number;
1716
+ /** Global content-addressed embedding cache (not owner-scoped). */
1717
+ embeddingCacheRows: number;
1718
+ };
1719
+
1720
+ export type EmailStats = {
1721
+ total: number;
1722
+ unread: number;
1723
+ withAttachments: number;
1724
+ byAccount: { accountId: string; address: string; total: number; unread: number }[];
1725
+ latestSync: {
1726
+ accountId: string;
1727
+ address: string;
1728
+ status: string;
1729
+ finishedAt: string | null;
1730
+ ingested: number;
1731
+ scanned: number;
1732
+ error: string | null;
1733
+ }[];
1734
+ };
1735
+
1736
+ export type HeartbeatStats = {
1737
+ byStatus: Bucket[];
1738
+ recentFiresByDisposition: Bucket[];
1739
+ };
1740
+
1741
+ export type TelegramStats = {
1742
+ messagesTotal: number;
1743
+ unprocessed: number;
1744
+ chatsByStatus: Bucket[];
1745
+ };
1746
+
1747
+ export type IngestDay = {
1748
+ day: string; // YYYY-MM-DD
1749
+ total: number;
1750
+ byType: Record<string, number>;
1751
+ };
1752
+
1753
+ /** One model as shown in the explorer. Normalised fields are best-effort
1754
+ * (absent when the provider's API doesn't return them); `raw` is always the
1755
+ * untouched object the API gave us. */
1756
+ export type ExplorerModel = {
1757
+ /** Provider model id / slug (e.g. 'anthropic/claude-sonnet-4.6'). */
1758
+ id: string;
1759
+ /** Friendly display name if the API provides one. */
1760
+ name?: string;
1761
+ description?: string;
1762
+ /** Total context window in tokens. */
1763
+ contextTokens?: number;
1764
+ /** Max output/completion tokens, when stated separately. */
1765
+ maxOutputTokens?: number;
1766
+ /** USD per 1M input (prompt) tokens. 0 means free; undefined means unknown. */
1767
+ inputPricePerM?: number;
1768
+ /** USD per 1M output (completion) tokens. */
1769
+ outputPricePerM?: number;
1770
+ /** Other priced dimensions the API exposes, surfaced verbatim. */
1771
+ extraPricing?: { label: string; value: string }[];
1772
+ /** e.g. 'text+image→text'. */
1773
+ modality?: string;
1774
+ /** Coarse type: chat | embedding | image | tts | stt | rerank | other. */
1775
+ kind?: string;
1776
+ /** Release/creation time as ISO, when provided. */
1777
+ created?: string;
1778
+ /** The provider's untouched model object. */
1779
+ raw: unknown;
1780
+ };
1781
+
1782
+ export type ModelSort = 'name' | 'context' | 'input' | 'output' | 'created';
1783
+
1784
+ export type StudioNode = {
1785
+ /** Stable canvas id, namespaced by kind: `agent:<slug>` / `skill:<slug>`. */
1786
+ id: string;
1787
+ kind: StudioNodeKind;
1788
+ slug: string;
1789
+ label: string;
1790
+ /** Secondary line — model for agents, tool-count for skills. */
1791
+ sublabel: string;
1792
+ enabled: boolean;
1793
+ isPersona: boolean;
1794
+ /** Node-local referential problems (dangling tool/skill/delegate, disabled). */
1795
+ issues: string[];
1796
+ };
1797
+
1798
+ export type StudioEdge = {
1799
+ id: string;
1800
+ source: string;
1801
+ target: string;
1802
+ kind: 'skill' | 'delegate' | 'group';
1803
+ };
1804
+
1805
+ export type NodeBiographyView = {
1806
+ node: {
1807
+ id: string;
1808
+ type: string;
1809
+ title: string;
1810
+ path: string;
1811
+ tags: string[];
1812
+ createdAt: string;
1813
+ updatedAt: string;
1814
+ /** First N chars of the summary the extractor wrote — null if
1815
+ * the extractor hasn't run (or refused to). */
1816
+ summary: string | null;
1817
+ /** True when the node has an embedding vector — second half of
1818
+ * the "is this node ready for retrieval?" check. */
1819
+ hasEmbedding: boolean;
1820
+ /** Bytes of the content field (text-shaped nodes) or 0
1821
+ * otherwise. Useful for "did extractor skip because body too
1822
+ * short?" debugging. */
1823
+ contentChars: number;
1824
+ /** First 4KB of content. Lets the biography page show a quick
1825
+ * preview of what was actually saved. */
1826
+ contentPreview: string | null;
1827
+ /** The data jsonb truncated and key-summarised so we don't blow
1828
+ * up the page rendering a 1MB blob inline. */
1829
+ dataKeys: string[];
1830
+ };
1831
+ /** Traces in chronological order (oldest first). Operators read
1832
+ * these top-to-bottom as a story: ingest → extractor → ... */
1833
+ traces: TraceDetail[];
1834
+ stats: {
1835
+ totalTraces: number;
1836
+ totalCostMicroUsd: number;
1837
+ totalTokensIn: number;
1838
+ totalTokensOut: number;
1839
+ /** ISO timestamp of the earliest trace touching this node, or
1840
+ * the node's own createdAt if there are no traces. */
1841
+ firstSeen: string;
1842
+ /** ISO timestamp of the most recent trace. Equal to firstSeen
1843
+ * when there's only one. */
1844
+ lastTouched: string;
1845
+ /** Counts by kind + status for the header chips. */
1846
+ byKind: Record<string, number>;
1847
+ byStatus: Record<string, number>;
1848
+ };
1849
+ };
1850
+
1851
+ export type AssistantAgentOption = {
1852
+ id: string;
1853
+ slug: string;
1854
+ name: string;
1855
+ role: string;
1856
+ model: string;
1857
+ };
1858
+
1859
+ export type AssistantTimelineRow = {
1860
+ id: string;
1861
+ direction: 'inbound' | 'outbound';
1862
+ text: string;
1863
+ model: string | null;
1864
+ /** Transport the turn arrived/left on — drives the channel badge in the UI.
1865
+ * 'web' for native /assistant turns; 'telegram' (etc.) for turns that came
1866
+ * in on another surface and now show in the unified stream. */
1867
+ channel: string;
1868
+ /** Execution state (migration 0105). 'complete' for every historical/inbound
1869
+ * row; an outbound row is 'pending' while the durable runner works and
1870
+ * 'failed' if it errored — so a reload mid-turn renders a live "thinking…"
1871
+ * bubble (or the error) instead of nothing. See docs/live-turn-streaming.md. */
1872
+ status: 'pending' | 'complete' | 'failed';
1873
+ /** Human-readable failure reason for a 'failed' turn; null otherwise. */
1874
+ error: string | null;
1875
+ /** Persisted media (images, voice notes, docs) so the turn renders its
1876
+ * attachments on load — no bytes, just node/file references. */
1877
+ attachments: ConversationAttachment[];
1878
+ /** Persisted thought trail (grounded action labels), present on an outbound
1879
+ * row when the brain has trail-persistence on — lets the "Thought process"
1880
+ * record survive a reload. Undefined when not persisted. */
1881
+ thoughts?: Array<{ kind: string; label: string; elapsedMs?: number }>;
1882
+ /** Deterministic tool-outcome tally for the turn — the runtime's own
1883
+ * ledger, persisted at finalize. Drives the "N tool calls · M failed"
1884
+ * footer so the record is independent of the reply's claims. */
1885
+ toolStats?: ToolOutcomeStatsRow;
1886
+ /** True when this row belongs to a superseded (replaced) turn pair — the
1887
+ * user cancelled the turn mid-stream and re-sent original + correction as
1888
+ * one combined turn (data.superseded_by). The pair stays in the transcript,
1889
+ * rendered dimmed with a "replaced" tag; prompt history and digests skip it. */
1890
+ superseded?: boolean;
1891
+ createdAt: string;
1892
+ };
1893
+
1894
+ export type TestApiKeyResult = {
1895
+ ok: boolean;
1896
+ /** One-line summary for the UI — e.g. '13 models accessible' or
1897
+ * 'OpenAI rejected the key (401)'. */
1898
+ message: string;
1899
+ /** Provider label for the result line. Empty when we can't resolve the
1900
+ * provider from the key's service. */
1901
+ provider: string;
1902
+ /** Which adapter ran the probe ('openai-tts', 'anthropic-chat', …). */
1903
+ adapter: string;
1904
+ /** Number of models accessible to this key, if discovery succeeded. */
1905
+ modelsFound?: number;
1906
+ };
1907
+
1908
+ export type ComposeStatus = {
1909
+ state: ComposeState;
1910
+ /** The updater's last refresh outcome verbatim (e.g. 'refreshed',
1911
+ * 'modified', 'no-baseline', 'unavailable'), for the details view. */
1912
+ refresh: string | null;
1913
+ /** The CLIENT stack's compose (v0.200 split). 'absent' state = a
1914
+ * server-only box (no docker-compose.client.yml — nothing to drift). */
1915
+ client: { state: ComposeState | 'absent'; refresh: string | null };
1916
+ /** The updater sidecar's own script (v0.206+). Before the self-refresh
1917
+ * landed this was the silent failure: a stale script rolled the server
1918
+ * stack, reported ok, and skipped the client stack with no error anywhere.
1919
+ * 'unknown' on any box still running that script — it reports no sha. */
1920
+ updater: { state: UpdaterScriptState; refresh: string | null };
1921
+ checkedAt: string | null;
1922
+ };
1923
+
1924
+ export type UpdateCheck = {
1925
+ currentVersion: string;
1926
+ latest: ReleaseInfo | null;
1927
+ updateAvailable: boolean;
1928
+ checkedAt: string;
1929
+ /** Set when the check itself failed (network, rate limit, no releases yet). */
1930
+ error: string | null;
1931
+ };
1932
+
1933
+ export type UpdaterStatus = {
1934
+ phase: UpdaterPhase;
1935
+ target: string;
1936
+ startedAt: string | null;
1937
+ finishedAt: string | null;
1938
+ ok: boolean | null;
1939
+ error: string | null;
1940
+ };
1941
+
1942
+ export interface TailnetStatus {
1943
+ available: true;
1944
+ /** tailscaled backend state: "Running" when connected; "NeedsLogin",
1945
+ * "Stopped", "Starting" otherwise. */
1946
+ backendState: string;
1947
+ /** This node's MagicDNS name + hostname (how peers reach US). */
1948
+ self: { dnsName: string; hostName: string; online: boolean } | null;
1949
+ /** The tailnet domain, e.g. "tail1234.ts.net". */
1950
+ magicDNSSuffix: string | null;
1951
+ peers: TailnetPeer[];
1952
+ }
1953
+
1954
+ export interface TailnetUnavailable {
1955
+ available: false;
1956
+ /** Human-readable why — shown in the status tile. */
1957
+ reason: string;
1958
+ }
1959
+
1960
+ export type TailnetResult = TailnetStatus | TailnetUnavailable;
1961
+
1962
+ export type TailscaleConfigSummary = {
1963
+ hostname: string;
1964
+ masked: string;
1965
+ lastActivatedAt: Date | null;
1966
+ };
1967
+
1968
+ export type SystemHealth = {
1969
+ ts: string;
1970
+ scope: 'container' | 'host';
1971
+ host: {
1972
+ cpuLoadPct: number | null;
1973
+ mem: { usedBytes: number; totalBytes: number; usedPct: number } | null;
1974
+ disk: DiskInfo | null;
1975
+ uptimeSec: number;
1976
+ heapUsedBytes: number;
1977
+ rssBytes: number;
1978
+ loadAvg: number[];
1979
+ cpuCores: number;
1980
+ };
1981
+ postgres: {
1982
+ up: boolean;
1983
+ dbSizeBytes: number | null;
1984
+ connections: number | null;
1985
+ cacheHitPct: number | null;
1986
+ topTables: { name: string; bytes: number }[];
1987
+ };
1988
+ storage: {
1989
+ minioUp: boolean | null;
1990
+ attachmentBytes: number | null;
1991
+ filesDisk: DiskInfo | null;
1992
+ };
1993
+ /** Tier-2 document parser fallback (.odt / .pptx / .doc / .rtf / .epub /
1994
+ * …) — sibling docker service. `up: false` means the fallback path
1995
+ * degrades cleanly to `no_text_layer` on every new ingest of those
1996
+ * formats; in-process parsers (pdf/docx/xlsx/text) keep working. */
1997
+ tika: {
1998
+ up: boolean;
1999
+ version: string | null;
2000
+ };
2001
+ /** The browser sidecar (browserless/chromium) — the Pages → PDF export
2002
+ * engine, a sibling docker service like Tika. `up: false` means PDF
2003
+ * downloads 503 until it's back (Markdown/Word unaffected); `up: null`
2004
+ * means BROWSER_WS_ENDPOINT isn't configured (e.g. detached dev). */
2005
+ browser: {
2006
+ up: boolean | null;
2007
+ version: string | null;
2008
+ };
2009
+ /** The configured embedding server. For the `local` provider this is the
2010
+ * self-hosted Ollama/LM Studio/TEI on MANTLE_LOCAL_EMBEDDING_URL (the
2011
+ * bundled `ollama` compose service in prod). `up: true` means it's
2012
+ * reachable AND the configured model is loaded — the only state in which
2013
+ * ingest can actually embed. `up: null` = a remote/cloud embedder
2014
+ * (openrouter/openai/google), which isn't pingable from here without a key,
2015
+ * so it's surfaced as "remote" rather than a misleading red dot. */
2016
+ embedder: {
2017
+ up: boolean | null;
2018
+ provider: string | null;
2019
+ model: string | null;
2020
+ detail: string | null;
2021
+ /** Where the embedder runs: a self-hosted server ('local') or a cloud
2022
+ * provider ('remote'). Shown on the dashboard pill label. */
2023
+ scope: 'remote' | 'local' | null;
2024
+ };
2025
+ /** CLI sandboxes supervisor (sandboxd) — profile-gated like the tailnet,
2026
+ * so `up: null` (muted pill) is the resting state on a box without the
2027
+ * `sandboxes` compose profile. `up: true` requires sandboxd answering
2028
+ * (its own /healthz additionally verifies docker); counts and the disk
2029
+ * budget come from its live listing. */
2030
+ sandboxes: {
2031
+ up: boolean | null;
2032
+ total: number | null;
2033
+ running: number | null;
2034
+ disk: { usedBytes: number | null; budgetBytes: number } | null;
2035
+ };
2036
+ /** Tailscale / local network — the optional tailnet that lets a cloud VPS
2037
+ * reach a LAN model box by MagicDNS name. Profile-gated and off by default
2038
+ * in dev, so `up: null` (a muted/disabled pill) is the normal resting state;
2039
+ * `up: true` only when tailscaled reports backendState 'Running'. */
2040
+ network: {
2041
+ up: boolean | null;
2042
+ detail: string | null;
2043
+ };
2044
+ degraded: string[];
2045
+ };
2046
+
2047
+ export type ProvisionResult = {
2048
+ createdWorkers: { kind: string; name: string; provider: string; model: string }[];
2049
+ createdAgent: { slug: string; name: string } | null;
2050
+ /** Capabilities skipped because the optional key wasn't provided. */
2051
+ skipped: string[];
2052
+ /** Specialist agents seeded alongside the persona (Pages, Ledger, Remy,
2053
+ * Researcher, Coder) and wired into the assistant's delegate_to. Names of the
2054
+ * ones that seeded successfully; a seed that throws is logged + omitted (it
2055
+ * never aborts onboarding — the persona is what matters). */
2056
+ seededSpecialists: string[];
2057
+ };
2058
+
2059
+ export type HeartbeatFireSummary = {
2060
+ id: string;
2061
+ firedAt: string;
2062
+ traceId: string | null;
2063
+ disposition: string;
2064
+ stateBefore: Record<string, unknown> | null;
2065
+ stateAfter: Record<string, unknown> | null;
2066
+ replyText: string | null;
2067
+ replySurfaceRef: Record<string, unknown> | null;
2068
+ errorMessage: string | null;
2069
+ };
2070
+
2071
+ export type AgentTelegramBinding = {
2072
+ accountId: string;
2073
+ botUsername: string;
2074
+ enabled: boolean;
2075
+ lastPollAt: string | null;
2076
+ lastPollError: string | null;
2077
+ };
2078
+
2079
+ export type AgentTelegramChat = {
2080
+ id: string;
2081
+ telegramChatId: string;
2082
+ label: string;
2083
+ status: 'pending' | 'allowed' | 'denied';
2084
+ lastMessageAt: string | null;
2085
+ };
2086
+
2087
+ export type DiffStatus =
2088
+ /** Live matches the template (for tracked fields). */
2089
+ | 'ok'
2090
+ /** In the template, absent (or disabled) in the brain — a capability not landed. */
2091
+ | 'missing'
2092
+ /** In the brain, not in the template — operator-added, informational. */
2093
+ | 'extra'
2094
+ /** Present in both, but a tracked field diverges. */
2095
+ | 'modified';
2096
+
2097
+ export type FieldDiff = {
2098
+ /** 'toolGroupSlugs' | 'skillSlugs' | 'delegate_to' | 'instructions' |
2099
+ * 'toolSlugs' | 'model' | 'systemPrompt' | 'enabled' */
2100
+ field: string;
2101
+ /** The template value — what an "adopt" would write. */
2102
+ manifest: string | string[] | null;
2103
+ /** The live value in the brain. */
2104
+ live: string | string[] | null;
2105
+ /** Set fields only: members in `live` but not `manifest` (operator-added). */
2106
+ added?: string[];
2107
+ /** Set fields only: members in `manifest` but not `live` (not landed). */
2108
+ removed?: string[];
2109
+ /** Informational-only diff (e.g. a specialist prompt) — shown, not weighted. */
2110
+ info?: boolean;
2111
+ };
2112
+
2113
+ export type EntityDiff = {
2114
+ kind: EntityKind;
2115
+ /** Agent/skill/group slug, or the worker kind. */
2116
+ slug: string;
2117
+ name: string;
2118
+ status: DiffStatus;
2119
+ severity: AuditSeverity;
2120
+ /** One-line human summary of the difference. */
2121
+ summary: string;
2122
+ /** Tracked fields that differ (empty when status is 'ok'). */
2123
+ fields: FieldDiff[];
2124
+ /** Can the operator "Adopt from template" this item? True for missing/modified
2125
+ * (apply the manifest version); false for ok (nothing to do) and extra
2126
+ * (operator-added — adopting would mean deleting, which we never do). */
2127
+ adoptable: boolean;
2128
+ };
2129
+
2130
+ export type ConfigDiffReport = {
2131
+ generatedAt: string;
2132
+ /** The shipped template version (APP_VERSION). */
2133
+ appVersion: string;
2134
+ /** The version the brain was last auto-reconciled to (null if never). */
2135
+ lastReconciledVersion: string | null;
2136
+ entities: EntityDiff[];
2137
+ counts: { ok: number; missing: number; extra: number; modified: number };
2138
+ };
2139
+
2140
+ export type AdoptKind = 'persona' | 'agent' | 'skill' | 'tool-group' | 'worker';
2141
+
2142
+ // ── Server-lib view/query DTOs (jackdaw split P0 follow-up: @server/* purge) ──
2143
+ // Moved from server/web/lib/* and @mantle/content; the originals re-export
2144
+ // these names so server import paths are unchanged.
2145
+
2146
+ export type UpdaterPhase =
2147
+ 'idle' | 'pulling' | 'rolling' | 'done' | 'error' | 'unconfigured' | 'requested';
2148
+
2149
+ /** The updater SCRIPT's own currency. Deliberately not `ComposeState`: the
2150
+ * script has no `no-baseline` standoff (it self-adopts, having no supported
2151
+ * box-local variation), so a missing baseline is not a state an operator can
2152
+ * act on — the only actionable state is `modified`. */
2153
+ export type UpdaterScriptState =
2154
+ | 'in-sync' // box script == this release's canonical
2155
+ | 'stale' // differs — self-refreshes on the next successful update
2156
+ | 'modified' // differs from its baseline: hand-edited, refresh refused
2157
+ | 'unknown'; // no stack.json, or a pre-v0.206 updater that reports no sha
2158
+
2159
+ export type ComposeState =
2160
+ | 'in-sync' // box compose == this release's canonical
2161
+ | 'stale' // pristine (== baseline) but not this release's — refresh hasn't run
2162
+ | 'modified' // hand-edited canonical file — auto-refresh disabled, needs adoption
2163
+ | 'no-baseline' // pre-adoption box — run scripts/compose-adopt.sh once
2164
+ | 'unknown'; // no stack.json (old updater.sh / no sidecar / dev)
2165
+
2166
+ export type ReleaseInfo = {
2167
+ /** Tag as published, e.g. "v0.20.67". */
2168
+ tag: string;
2169
+ /** Bare version, e.g. "0.20.67". */
2170
+ version: string;
2171
+ name: string;
2172
+ url: string;
2173
+ publishedAt: string | null;
2174
+ };
2175
+
2176
+ export type DiskInfo = { usedBytes: number; totalBytes: number; usedPct: number; mount: string };
2177
+
2178
+ export type EntityKind = 'persona' | 'agent' | 'skill' | 'tool-group' | 'worker';
2179
+
2180
+ export type StudioNodeKind = 'agent' | 'skill' | 'group';
2181
+
2182
+ /** One peer on the tailnet (another device sharing your tailnet). */
2183
+ export interface TailnetPeer {
2184
+ /** MagicDNS name, trailing dot stripped — e.g. "gemma-box.tail1234.ts.net".
2185
+ * This is what you'd put in a route base URL: http://<dnsName>:<port>/v1 */
2186
+ dnsName: string;
2187
+ /** Short hostname — e.g. "gemma-box". */
2188
+ hostName: string;
2189
+ /** Tailscale IPs (100.x.y.z / fd7a:…). Surfaced for reference; prefer names. */
2190
+ ips: string[];
2191
+ online: boolean;
2192
+ /** OS string tailscaled reports (linux / windows / macOS …), best-effort. */
2193
+ os: string | null;
2194
+ }
2195
+
2196
+ export type ToolOutcomeStatsRow = {
2197
+ calls: number;
2198
+ succeeded: number;
2199
+ failed: number;
2200
+ skipped: number;
2201
+ /** Confirm-gated calls parked behind operator approval — not yet run. */
2202
+ queued: number;
2203
+ failures: Array<{ slug: string; error: string }>;
2204
+ };
2205
+
2206
+ // ── Server-lib view/query DTOs (jackdaw split P0 follow-up: @server/* purge) ──
2207
+ // Moved from server/web/lib/* and @mantle/content; the originals re-export
2208
+ // these names so server import paths are unchanged.
2209
+
2210
+ export type CacheHitStats = {
2211
+ hits: number;
2212
+ misses: number;
2213
+ apiCalls: number;
2214
+ };
2215
+
2216
+ export type DuplicateSuppression = {
2217
+ /** Model slug captured in trace_steps.meta.model at suppression time. */
2218
+ model: string;
2219
+ /** How many duplicate tool_use blocks were suppressed in the window. */
2220
+ count: number;
2221
+ /** Distinct tool slugs the duplicates targeted (top 5, comma-separated). */
2222
+ topSlugs: string;
2223
+ /** Most recent suppression, ISO string. */
2224
+ lastAt: string;
2225
+ };
2226
+
2227
+ export type FactCostCapStats = {
2228
+ /** Extractor model slug captured in trace_steps.meta.model. */
2229
+ model: string;
2230
+ /** How many process_facts steps dropped facts to the cap in the window. */
2231
+ runs: number;
2232
+ /** Total facts discarded across those runs (sum of meta.dropped). */
2233
+ factsDropped: number;
2234
+ /** Most recent occurrence, ISO string. */
2235
+ lastAt: string;
2236
+ };
2237
+
2238
+ export type Traffic = {
2239
+ count: number;
2240
+ errorCount: number;
2241
+ avgMs: number | null;
2242
+ costMicroUsd: number;
2243
+ tokensIn: number;
2244
+ tokensOut: number;
2245
+ tokensCacheRead: number;
2246
+ };
2247
+
2248
+ export type StudioGraph = {
2249
+ generatedAt: string;
2250
+ nodes: StudioNode[];
2251
+ edges: StudioEdge[];
2252
+ agents: StudioAgentDetail[];
2253
+ skills: StudioSkillDetail[];
2254
+ toolGroups: StudioToolGroupDetail[];
2255
+ workers: StudioWorkerDetail[];
2256
+ /** Live config-integrity report (the same checker behind /debug/integrity). */
2257
+ report: SystemReport;
2258
+ };
2259
+
2260
+ export type StudioAgentDetail = {
2261
+ id: string;
2262
+ slug: string;
2263
+ name: string;
2264
+ model: string;
2265
+ role: string;
2266
+ enabled: boolean;
2267
+ isPersona: boolean;
2268
+ skillSlugs: string[];
2269
+ /** Skills attached but NOT resolved (missing or disabled) — surfaced honestly. */
2270
+ missingSkillSlugs: string[];
2271
+ delegateSlugs: string[];
2272
+ /** Tool groups granted to this agent. */
2273
+ toolGroupSlugs: string[];
2274
+ /** Granted groups that are missing or disabled — surfaced honestly. */
2275
+ missingToolGroupSlugs: string[];
2276
+ toolCount: number;
2277
+ params: { temperature?: number; max_tokens?: number };
2278
+ maxIterations?: number;
2279
+ /** Whether this is a manifest agent that can be reset to its canonical default. */
2280
+ resettable: boolean;
2281
+ /** The base system prompt (editable prose in Phase 2). */
2282
+ systemPrompt: string;
2283
+ /** The enabled, attached skills in composition order. */
2284
+ skillBlocks: ComposedSkillBlock[];
2285
+ /** The full assembled system prompt the model receives (base + skill blocks),
2286
+ * exactly as `composeSystemPromptWithSkills` builds it on a real turn. */
2287
+ composedPrompt: string;
2288
+ };
2289
+
2290
+ export type ComposedSkillBlock = { slug: string; name: string; instructions: string };
2291
+
2292
+ export type StudioSkillDetail = {
2293
+ id: string;
2294
+ slug: string;
2295
+ name: string;
2296
+ enabled: boolean;
2297
+ instructions: string;
2298
+ /** Fan-out: every agent that attaches this skill (the many-to-many). */
2299
+ usedByAgentSlugs: string[];
2300
+ };
2301
+
2302
+ export type StudioToolGroupDetail = {
2303
+ id: string;
2304
+ slug: string;
2305
+ name: string;
2306
+ enabled: boolean;
2307
+ toolSlugs: string[];
2308
+ /** Fan-out: every agent that grants this group. */
2309
+ usedByAgentSlugs: string[];
2310
+ };
2311
+
2312
+ export type StudioWorkerDetail = {
2313
+ id: string;
2314
+ kind: string;
2315
+ name: string;
2316
+ model: string;
2317
+ enabled: boolean;
2318
+ isDefault: boolean;
2319
+ /** Worker prose (registry): the chat-worker system prompt + the vision/document
2320
+ * extraction prompt, when present. */
2321
+ systemPrompt: string | null;
2322
+ extractionPrompt: string | null;
2323
+ issues: string[];
2324
+ };