@crossworks/client-types 0.232.133 → 0.232.142

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.
@@ -0,0 +1,936 @@
1
+ /**
2
+ * @mantle/client-types · views
3
+ *
4
+ * Server-lib view/query DTOs — the shapes screens read, kept here so a
5
+ * client component never reaches into @server/*.
6
+ *
7
+ * Split out of the 2548-line index.ts on 2026-09-02 (audit, tier 3) with the
8
+ * contents unchanged. index.ts re-exports every one of these, so the package's
9
+ * public surface is byte-identical — only the file a symbol lives in moved.
10
+ */
11
+
12
+ import type { AuditSeverity, SystemReport } from '../types/integrity';
13
+ import type { TraceDetail } from '../traces-format';
14
+ import type { PersonaNoteDTO } from './agents';
15
+ import type { ContextSnapshot, ConversationAttachment } from './rows';
16
+
17
+ // ── Server-lib view/query DTOs (jackdaw split P0 follow-up: @server/* purge) ──
18
+ // Moved from server/web/lib/* and @mantle/content; the originals re-export
19
+ // these names so server import paths are unchanged.
20
+
21
+ /** Sort order for the pages list. 'edited' (last updated) is the default. */
22
+ export type PageSort = 'edited' | 'newest' | 'oldest' | 'title';
23
+
24
+ /** A node that links TO a given page — one inbound `references` edge, resolved
25
+ * to its source node. Powers the "Referenced by" panel. */
26
+ export type Backlink = {
27
+ id: string;
28
+ title: string;
29
+ /** The source node's type (wire truth: the db enum widens to string here). */
30
+ type: string;
31
+ icon: string | null;
32
+ };
33
+
34
+ export type CapacityZone = 'green' | 'watch' | 'split';
35
+
36
+ export type CapacityMetric = {
37
+ count: number;
38
+ watch: number;
39
+ split: number;
40
+ /** count / split — may exceed 1 when the split point is passed. */
41
+ ratio: number;
42
+ zone: CapacityZone;
43
+ };
44
+
45
+ export type BrainCapacity = {
46
+ docs: CapacityMetric;
47
+ chunkVectors: CapacityMetric;
48
+ /** Worst zone across both axes — the brain's headline state. */
49
+ zone: CapacityZone;
50
+ /** Worst-axis fill as an integer percentage of the split budget (may exceed 100). */
51
+ pctOfSplit: number;
52
+ };
53
+
54
+ export type AgentContext = {
55
+ agentId: string;
56
+ agentName: string | null;
57
+ agentSlug: string | null;
58
+ modelSlug: string;
59
+ lastTokensIn: number;
60
+ contextLimit: number | null;
61
+ /** Where contextLimit came from: live OpenRouter data, the static
62
+ * fallback, or unknown (slug not in either). Surfaced in the UI. */
63
+ contextSource: ContextSource;
64
+ pct: number | null;
65
+ lastRunAt: string;
66
+ };
67
+
68
+ export type SpendRange = 'day' | 'week' | 'month';
69
+
70
+ export type AgentSpend = {
71
+ agentId: string | null;
72
+ agentName: string | null;
73
+ agentSlug: string | null;
74
+ costMicroUsd: number;
75
+ tokensIn: number;
76
+ tokensOut: number;
77
+ cacheReadTokens: number;
78
+ runs: number;
79
+ };
80
+
81
+ export type ModelSpend = {
82
+ /** The OpenRouter model slug captured in trace_steps.meta.model. */
83
+ model: string;
84
+ costMicroUsd: number;
85
+ tokensIn: number;
86
+ tokensOut: number;
87
+ cacheReadTokens: number;
88
+ calls: number;
89
+ };
90
+
91
+ export type DailySpend = {
92
+ /** ISO date (YYYY-MM-DD) in the server's local timezone. */
93
+ day: string;
94
+ costMicroUsd: number;
95
+ tokensIn: number;
96
+ tokensOut: number;
97
+ cacheReadTokens: number;
98
+ runs: number;
99
+ };
100
+
101
+ export type RecentFailure = {
102
+ id: string;
103
+ kind: string;
104
+ startedAt: string;
105
+ error: string;
106
+ };
107
+
108
+ export type TopError = {
109
+ message: string;
110
+ count: number;
111
+ lastAt: string;
112
+ lastTraceId: string;
113
+ };
114
+
115
+ /** Per-tool tallies of calls the central validator flagged. Clean calls
116
+ * write no `arg_validation` meta at all, so these are problem counts,
117
+ * not rates — an empty result means nothing was flagged, not no calls. */
118
+ export type ToolValidationAgg = {
119
+ tool: string;
120
+ flaggedCalls: number;
121
+ withRepairs: number;
122
+ withUnknownKeys: number;
123
+ withViolations: number;
124
+ lastAt: string;
125
+ };
126
+
127
+ export type ToolValidationEvent = {
128
+ stepId: string;
129
+ traceId: string;
130
+ tool: string;
131
+ mode: string;
132
+ repairs: Array<{ key: string; kind: string; note: string }>;
133
+ unknownKeys: Array<{ key: string; suggestion: string | null }>;
134
+ violations: string[];
135
+ startedAt: string;
136
+ };
137
+
138
+ export type AgentActivityRow = {
139
+ id: string;
140
+ slug: string;
141
+ name: string;
142
+ role: string;
143
+ model: string;
144
+ priority: number;
145
+ enabled: boolean;
146
+ lastUsedAt: string | null;
147
+ usageCount: number;
148
+ };
149
+
150
+ export type ChatRow = {
151
+ id: string;
152
+ title: string | null;
153
+ username: string | null;
154
+ telegramChatId: string;
155
+ allowlistStatus: string;
156
+ totalTurns: number;
157
+ digested: number;
158
+ undigested: number;
159
+ lastActivity: string | null;
160
+ responderAgentId: string | null;
161
+ };
162
+
163
+ export type PersonaNotesRow = {
164
+ agentId: string;
165
+ agentName: string;
166
+ agentSlug: string;
167
+ notes: PersonaNoteDTO[];
168
+ };
169
+
170
+ export type ContentIndexCoverage = {
171
+ total: number;
172
+ indexed: number;
173
+ byType: Array<{ type: string; total: number; indexed: number }>;
174
+ };
175
+
176
+ /**
177
+ * Awareness of duplicate graph edges. Going forward the extractor rebuilds
178
+ * edges per node (idempotent), but content re-edited *before* that fix may
179
+ * carry historical duplicate `mentioned_in` / `references` rows. This surfaces
180
+ * the count + a few labelled samples so the operator knows to run
181
+ * `pnpm dedupe:edges`. Read-only — cleaning stays the deliberate CLI tool.
182
+ */
183
+ export type DuplicateEdgeStats = {
184
+ groups: number; // logical edges with >1 row
185
+ redundant: number; // rows that could be removed (sum of count-1)
186
+ samples: { relation: string; label: string; count: number }[];
187
+ };
188
+
189
+ /** One responder turn: the question, the retrieval snapshot the turn's
190
+ * 'load_context' trace step persisted (null for pre-instrumentation turns),
191
+ * and the outbound reply. See ContextSnapshot in @mantle/runtime/agent. */
192
+ /** Mirrors @mantle/tracing `ContextSource`. */
193
+ export type ContextSource = 'live' | 'fallback' | 'unknown';
194
+
195
+ export type ContextTurnRow = {
196
+ traceId: string;
197
+ startedAt: string;
198
+ status: string;
199
+ surface: string | null;
200
+ agentSlug: string | null;
201
+ model: string | null;
202
+ durationMs: number | null;
203
+ question: string | null;
204
+ snapshot: ContextSnapshot | null;
205
+ response: string | null;
206
+ };
207
+
208
+ export type DigestRow = {
209
+ id: string;
210
+ title: string;
211
+ createdAt: string;
212
+ /** All fields below are pulled out of nodes.data (jsonb). */
213
+ chatId: string;
214
+ telegramChatId: string | null;
215
+ periodStart: string;
216
+ periodEnd: string;
217
+ sourceTurnCount: number;
218
+ model: string;
219
+ agent: string;
220
+ summary: string;
221
+ topic: string | null;
222
+ topicSlug: string | null;
223
+ };
224
+
225
+ export type FactRow = {
226
+ id: string;
227
+ content: string;
228
+ kind: string;
229
+ confidence: number;
230
+ entityName: string | null;
231
+ entityKind: string | null;
232
+ sourceNodeId: string | null;
233
+ sourceTitle: string | null;
234
+ createdAt: string;
235
+ };
236
+
237
+ export type TopicRow = {
238
+ topic: string;
239
+ topicSlug: string;
240
+ digestCount: number;
241
+ turnCount: number;
242
+ firstSeen: string;
243
+ lastSeen: string;
244
+ };
245
+
246
+ /** One key/count bucket in the corpus histograms below. */
247
+ export type Bucket = { key: string; count: number };
248
+
249
+ export type BrainCounts = {
250
+ nodesTotal: number;
251
+ nodesByType: Bucket[];
252
+ factsTotal: number;
253
+ factsByKind: Bucket[];
254
+ entitiesTotal: number;
255
+ entitiesByKind: Bucket[];
256
+ edgesTotal: number;
257
+ edgesByRelation: Bucket[];
258
+ };
259
+
260
+ /** A health check, not a fixer. Counts active edges that share the same
261
+ * (source, target, relation) — i.e. duplicates. The extractor's
262
+ * delete-then-rebuild discipline (see architecture §9k) means this should
263
+ * stay 0; a non-zero value flags a regression in edge writing. The remedy is
264
+ * the one-shot `pnpm dedupe:edges --apply`, NOT a recurring auto-clean (which
265
+ * would mask the regression). */
266
+ export type GraphIntegrity = {
267
+ /** Distinct (source, target, relation) groups with more than one row. */
268
+ duplicateEdgeGroups: number;
269
+ /** Total redundant rows across those groups (Σ count-1) — how many
270
+ * `dedupe:edges --apply` would remove. */
271
+ redundantEdgeRows: number;
272
+ };
273
+
274
+ export type VectorCounts = {
275
+ nodesIndexed: number;
276
+ nodesTotal: number;
277
+ factsIndexed: number;
278
+ factsTotal: number;
279
+ entitiesIndexed: number;
280
+ entitiesTotal: number;
281
+ /** The headline: total embedded vectors across nodes + facts + entities. */
282
+ vectorsTotal: number;
283
+ /** Global content-addressed embedding cache (not owner-scoped). */
284
+ embeddingCacheRows: number;
285
+ };
286
+
287
+ export type EmailStats = {
288
+ total: number;
289
+ unread: number;
290
+ withAttachments: number;
291
+ byAccount: { accountId: string; address: string; total: number; unread: number }[];
292
+ latestSync: {
293
+ accountId: string;
294
+ address: string;
295
+ status: string;
296
+ finishedAt: string | null;
297
+ ingested: number;
298
+ scanned: number;
299
+ error: string | null;
300
+ }[];
301
+ };
302
+
303
+ export type HeartbeatStats = {
304
+ byStatus: Bucket[];
305
+ recentFiresByDisposition: Bucket[];
306
+ };
307
+
308
+ export type TelegramStats = {
309
+ messagesTotal: number;
310
+ unprocessed: number;
311
+ chatsByStatus: Bucket[];
312
+ };
313
+
314
+ export type IngestDay = {
315
+ day: string; // YYYY-MM-DD
316
+ total: number;
317
+ byType: Record<string, number>;
318
+ };
319
+
320
+ /** One model as shown in the explorer. Normalised fields are best-effort
321
+ * (absent when the provider's API doesn't return them); `raw` is always the
322
+ * untouched object the API gave us. */
323
+ export type ExplorerModel = {
324
+ /** Provider model id / slug (e.g. 'anthropic/claude-sonnet-4.6'). */
325
+ id: string;
326
+ /** Friendly display name if the API provides one. */
327
+ name?: string;
328
+ description?: string;
329
+ /** Total context window in tokens. */
330
+ contextTokens?: number;
331
+ /** Max output/completion tokens, when stated separately. */
332
+ maxOutputTokens?: number;
333
+ /** USD per 1M input (prompt) tokens. 0 means free; undefined means unknown. */
334
+ inputPricePerM?: number;
335
+ /** USD per 1M output (completion) tokens. */
336
+ outputPricePerM?: number;
337
+ /** Other priced dimensions the API exposes, surfaced verbatim. */
338
+ extraPricing?: { label: string; value: string }[];
339
+ /** e.g. 'text+image→text'. */
340
+ modality?: string;
341
+ /** Coarse type: chat | embedding | image | tts | stt | rerank | other. */
342
+ kind?: string;
343
+ /** Release/creation time as ISO, when provided. */
344
+ created?: string;
345
+ /** The provider's untouched model object. */
346
+ raw: unknown;
347
+ };
348
+
349
+ export type ModelSort = 'name' | 'context' | 'input' | 'output' | 'created';
350
+
351
+ export type StudioNode = {
352
+ /** Stable canvas id, namespaced by kind: `agent:<slug>` / `skill:<slug>`. */
353
+ id: string;
354
+ kind: StudioNodeKind;
355
+ slug: string;
356
+ label: string;
357
+ /** Secondary line — model for agents, tool-count for skills. */
358
+ sublabel: string;
359
+ enabled: boolean;
360
+ isPersona: boolean;
361
+ /** Node-local referential problems (dangling tool/skill/delegate, disabled). */
362
+ issues: string[];
363
+ };
364
+
365
+ export type StudioEdge = {
366
+ id: string;
367
+ source: string;
368
+ target: string;
369
+ kind: 'skill' | 'delegate' | 'group';
370
+ };
371
+
372
+ export type NodeBiographyView = {
373
+ node: {
374
+ id: string;
375
+ type: string;
376
+ title: string;
377
+ path: string;
378
+ tags: string[];
379
+ createdAt: string;
380
+ updatedAt: string;
381
+ /** First N chars of the summary the extractor wrote — null if
382
+ * the extractor hasn't run (or refused to). */
383
+ summary: string | null;
384
+ /** True when the node has an embedding vector — second half of
385
+ * the "is this node ready for retrieval?" check. */
386
+ hasEmbedding: boolean;
387
+ /** Bytes of the content field (text-shaped nodes) or 0
388
+ * otherwise. Useful for "did extractor skip because body too
389
+ * short?" debugging. */
390
+ contentChars: number;
391
+ /** First 4KB of content. Lets the biography page show a quick
392
+ * preview of what was actually saved. */
393
+ contentPreview: string | null;
394
+ /** The data jsonb truncated and key-summarised so we don't blow
395
+ * up the page rendering a 1MB blob inline. */
396
+ dataKeys: string[];
397
+ };
398
+ /** Traces in chronological order (oldest first). Operators read
399
+ * these top-to-bottom as a story: ingest → extractor → ... */
400
+ traces: TraceDetail[];
401
+ stats: {
402
+ totalTraces: number;
403
+ totalCostMicroUsd: number;
404
+ totalTokensIn: number;
405
+ totalTokensOut: number;
406
+ /** ISO timestamp of the earliest trace touching this node, or
407
+ * the node's own createdAt if there are no traces. */
408
+ firstSeen: string;
409
+ /** ISO timestamp of the most recent trace. Equal to firstSeen
410
+ * when there's only one. */
411
+ lastTouched: string;
412
+ /** Counts by kind + status for the header chips. */
413
+ byKind: Record<string, number>;
414
+ byStatus: Record<string, number>;
415
+ };
416
+ };
417
+
418
+ export type AssistantAgentOption = {
419
+ id: string;
420
+ slug: string;
421
+ name: string;
422
+ role: string;
423
+ model: string;
424
+ };
425
+
426
+ export type AssistantTimelineRow = {
427
+ id: string;
428
+ direction: 'inbound' | 'outbound';
429
+ text: string;
430
+ model: string | null;
431
+ /** Transport the turn arrived/left on — drives the channel badge in the UI.
432
+ * 'web' for native /assistant turns; 'telegram' (etc.) for turns that came
433
+ * in on another surface and now show in the unified stream. */
434
+ channel: string;
435
+ /** Execution state (migration 0105). 'complete' for every historical/inbound
436
+ * row; an outbound row is 'pending' while the durable runner works and
437
+ * 'failed' if it errored — so a reload mid-turn renders a live "thinking…"
438
+ * bubble (or the error) instead of nothing. See docs/live-turn-streaming.md. */
439
+ status: 'pending' | 'complete' | 'failed';
440
+ /** Human-readable failure reason for a 'failed' turn; null otherwise. */
441
+ error: string | null;
442
+ /** Persisted media (images, voice notes, docs) so the turn renders its
443
+ * attachments on load — no bytes, just node/file references. */
444
+ attachments: ConversationAttachment[];
445
+ /** Persisted thought trail (grounded action labels), present on an outbound
446
+ * row when the brain has trail-persistence on — lets the "Thought process"
447
+ * record survive a reload. Undefined when not persisted. */
448
+ thoughts?: Array<{ kind: string; label: string; elapsedMs?: number }>;
449
+ /** Deterministic tool-outcome tally for the turn — the runtime's own
450
+ * ledger, persisted at finalize. Drives the "N tool calls · M failed"
451
+ * footer so the record is independent of the reply's claims. */
452
+ toolStats?: ToolOutcomeStatsRow;
453
+ /** True when this row belongs to a superseded (replaced) turn pair — the
454
+ * user cancelled the turn mid-stream and re-sent original + correction as
455
+ * one combined turn (data.superseded_by). The pair stays in the transcript,
456
+ * rendered dimmed with a "replaced" tag; prompt history and digests skip it. */
457
+ superseded?: boolean;
458
+ createdAt: string;
459
+ };
460
+
461
+ export type TestApiKeyResult = {
462
+ ok: boolean;
463
+ /** One-line summary for the UI — e.g. '13 models accessible' or
464
+ * 'OpenAI rejected the key (401)'. */
465
+ message: string;
466
+ /** Provider label for the result line. Empty when we can't resolve the
467
+ * provider from the key's service. */
468
+ provider: string;
469
+ /** Which adapter ran the probe ('openai-tts', 'anthropic-chat', …). */
470
+ adapter: string;
471
+ /** Number of models accessible to this key, if discovery succeeded. */
472
+ modelsFound?: number;
473
+ };
474
+
475
+ export type ComposeStatus = {
476
+ state: ComposeState;
477
+ /** The updater's last refresh outcome verbatim (e.g. 'refreshed',
478
+ * 'modified', 'no-baseline', 'unavailable'), for the details view. */
479
+ refresh: string | null;
480
+ /** The CLIENT stack's compose (v0.200 split). 'absent' state = a
481
+ * server-only box (no docker-compose.client.yml — nothing to drift). */
482
+ client: { state: ComposeState | 'absent'; refresh: string | null };
483
+ /** The updater sidecar's own script (v0.206+). Before the self-refresh
484
+ * landed this was the silent failure: a stale script rolled the server
485
+ * stack, reported ok, and skipped the client stack with no error anywhere.
486
+ * 'unknown' on any box still running that script — it reports no sha. */
487
+ updater: { state: UpdaterScriptState; refresh: string | null };
488
+ /** The front door (v0.232.126+): infra/caddy/Caddyfile is release-owned and
489
+ * refreshed by the updater like compose. 'unknown' on a box whose updater
490
+ * predates the field. A 'modified' Caddyfile means release-level front-door
491
+ * changes are not arriving there; box routes belong in conf.d/. */
492
+ caddy: { state: ComposeState; refresh: string | null };
493
+ /** The operator scripts (v0.232.137+): db-dump, db-restore, sanity,
494
+ * compose-adopt, uninstall and the install.sh configurator, fingerprinted
495
+ * as ONE set. Nothing refreshed these before, so a box ran the copies it
496
+ * was installed with forever — jason-prod applied a compose binding
497
+ * infra/caddy/{shapes,conf.d} with a compose-adopt.sh that knew about
498
+ * neither. Classified like the updater script rather than like compose: an
499
+ * absent baseline reads 'stale' because the refresh adopts it by itself,
500
+ * and only a baseline that EXISTS and disagrees ('modified') needs a
501
+ * human. 'unknown' on a box whose updater predates the field. */
502
+ scripts: { state: UpdaterScriptState; refresh: string | null };
503
+ checkedAt: string | null;
504
+ };
505
+
506
+ export type UpdateCheck = {
507
+ currentVersion: string;
508
+ latest: ReleaseInfo | null;
509
+ updateAvailable: boolean;
510
+ checkedAt: string;
511
+ /** Set when the check itself failed (network, rate limit, no releases yet). */
512
+ error: string | null;
513
+ /** The owner-UI (jackdaw) release stream — versioned separately since the
514
+ * repo split. `latest` is jackdaw's newest release; `pairedTag` is the
515
+ * client tag THIS server release was tested with (from the release-pair
516
+ * file baked into the image). The server cannot know which client build a
517
+ * browser is running, so "is an interface update available" is computed by
518
+ * the client itself against its own APP_VERSION. Absent on servers that
519
+ * predate the field. */
520
+ client?: {
521
+ latest: ReleaseInfo | null;
522
+ pairedTag: string | null;
523
+ error: string | null;
524
+ } | null;
525
+ };
526
+
527
+ export type UpdaterStatus = {
528
+ phase: UpdaterPhase;
529
+ target: string;
530
+ startedAt: string | null;
531
+ finishedAt: string | null;
532
+ ok: boolean | null;
533
+ error: string | null;
534
+ };
535
+
536
+ export interface TailnetStatus {
537
+ available: true;
538
+ /** tailscaled backend state: "Running" when connected; "NeedsLogin",
539
+ * "Stopped", "Starting" otherwise. */
540
+ backendState: string;
541
+ /** This node's MagicDNS name + hostname (how peers reach US). */
542
+ self: { dnsName: string; hostName: string; online: boolean } | null;
543
+ /** The tailnet domain, e.g. "tail1234.ts.net". */
544
+ magicDNSSuffix: string | null;
545
+ peers: TailnetPeer[];
546
+ }
547
+
548
+ export interface TailnetUnavailable {
549
+ available: false;
550
+ /** Human-readable why — shown in the status tile. */
551
+ reason: string;
552
+ }
553
+
554
+ export type TailnetResult = TailnetStatus | TailnetUnavailable;
555
+
556
+ export type TailscaleConfigSummary = {
557
+ hostname: string;
558
+ masked: string;
559
+ lastActivatedAt: Date | null;
560
+ };
561
+
562
+ export type SystemHealth = {
563
+ ts: string;
564
+ scope: 'container' | 'host';
565
+ host: {
566
+ cpuLoadPct: number | null;
567
+ mem: { usedBytes: number; totalBytes: number; usedPct: number } | null;
568
+ disk: DiskInfo | null;
569
+ uptimeSec: number;
570
+ heapUsedBytes: number;
571
+ rssBytes: number;
572
+ loadAvg: number[];
573
+ cpuCores: number;
574
+ };
575
+ postgres: {
576
+ up: boolean;
577
+ dbSizeBytes: number | null;
578
+ connections: number | null;
579
+ cacheHitPct: number | null;
580
+ topTables: { name: string; bytes: number }[];
581
+ };
582
+ storage: {
583
+ minioUp: boolean | null;
584
+ attachmentBytes: number | null;
585
+ filesDisk: DiskInfo | null;
586
+ };
587
+ /** Tier-2 document parser fallback (.odt / .pptx / .doc / .rtf / .epub /
588
+ * …) — sibling docker service. `up: false` means the fallback path
589
+ * degrades cleanly to `no_text_layer` on every new ingest of those
590
+ * formats; in-process parsers (pdf/docx/xlsx/text) keep working. */
591
+ tika: {
592
+ up: boolean;
593
+ version: string | null;
594
+ };
595
+ /** The browser sidecar (browserless/chromium) — the Pages → PDF export
596
+ * engine, a sibling docker service like Tika. `up: false` means PDF
597
+ * downloads 503 until it's back (Markdown/Word unaffected); `up: null`
598
+ * means BROWSER_WS_ENDPOINT isn't configured (e.g. detached dev). */
599
+ browser: {
600
+ up: boolean | null;
601
+ version: string | null;
602
+ };
603
+ /** The configured embedding server. For the `local` provider this is the
604
+ * self-hosted Ollama/LM Studio/TEI on MANTLE_LOCAL_EMBEDDING_URL (the
605
+ * bundled `ollama` compose service in prod). `up: true` means it's
606
+ * reachable AND the configured model is loaded — the only state in which
607
+ * ingest can actually embed. `up: null` = a remote/cloud embedder
608
+ * (openrouter/openai/google), which isn't pingable from here without a key,
609
+ * so it's surfaced as "remote" rather than a misleading red dot. */
610
+ embedder: {
611
+ up: boolean | null;
612
+ provider: string | null;
613
+ model: string | null;
614
+ detail: string | null;
615
+ /** Where the embedder runs: a self-hosted server ('local') or a cloud
616
+ * provider ('remote'). Shown on the dashboard pill label. */
617
+ scope: 'remote' | 'local' | null;
618
+ };
619
+ /** CLI sandboxes supervisor (sandboxd) — profile-gated like the tailnet,
620
+ * so `up: null` (muted pill) is the resting state on a box without the
621
+ * `sandboxes` compose profile. `up: true` requires sandboxd answering
622
+ * (its own /healthz additionally verifies docker); counts and the disk
623
+ * budget come from its live listing. */
624
+ sandboxes: {
625
+ up: boolean | null;
626
+ total: number | null;
627
+ running: number | null;
628
+ disk: { usedBytes: number | null; budgetBytes: number } | null;
629
+ };
630
+ /** Media sidecar (yt-dlp + ffmpeg) — the video_ingest fetch/transcode
631
+ * engine, profile-gated like sandboxes, so `up: null` (muted pill) is the
632
+ * resting state on a box without the `media` compose profile. Versions
633
+ * come from its /healthz — that is what makes a stale or failed yt-dlp
634
+ * self-update VISIBLE instead of silently breaking downloads. */
635
+ media: {
636
+ up: boolean | null;
637
+ ytDlpVersion: string | null;
638
+ ffmpegVersion: string | null;
639
+ /** null on images built before the CAD tier (v0.232.92) — DWF renders
640
+ * fall back to embedded thumbnails without it. */
641
+ ezdwfVersion: string | null;
642
+ /** null on images built before the DWG tier (v0.232.99) — with either of
643
+ * these missing the UI should show "DWG tier missing" (DWG parsing and
644
+ * rendering both need the sidecar; absence means the whole format). */
645
+ dwg2dxfVersion: string | null;
646
+ ezdxfVersion: string | null;
647
+ };
648
+ /** Tailscale / local network — the optional tailnet that lets a cloud VPS
649
+ * reach a LAN model box by MagicDNS name. Profile-gated and off by default
650
+ * in dev, so `up: null` (a muted/disabled pill) is the normal resting state;
651
+ * `up: true` only when tailscaled reports backendState 'Running'. */
652
+ network: {
653
+ up: boolean | null;
654
+ detail: string | null;
655
+ };
656
+ degraded: string[];
657
+ };
658
+
659
+ export type ProvisionResult = {
660
+ createdWorkers: { kind: string; name: string; provider: string; model: string }[];
661
+ createdAgent: { slug: string; name: string } | null;
662
+ /** Capabilities skipped because the optional key wasn't provided. */
663
+ skipped: string[];
664
+ /** Specialist agents seeded alongside the persona (Pages, Ledger, Remy,
665
+ * Researcher, Coder) and wired into the assistant's delegate_to. Names of the
666
+ * ones that seeded successfully; a seed that throws is logged + omitted (it
667
+ * never aborts onboarding — the persona is what matters). */
668
+ seededSpecialists: string[];
669
+ };
670
+
671
+ export type HeartbeatFireSummary = {
672
+ id: string;
673
+ firedAt: string;
674
+ traceId: string | null;
675
+ disposition: string;
676
+ stateBefore: Record<string, unknown> | null;
677
+ stateAfter: Record<string, unknown> | null;
678
+ replyText: string | null;
679
+ replySurfaceRef: Record<string, unknown> | null;
680
+ errorMessage: string | null;
681
+ };
682
+
683
+ export type AgentTelegramBinding = {
684
+ accountId: string;
685
+ botUsername: string;
686
+ enabled: boolean;
687
+ lastPollAt: string | null;
688
+ lastPollError: string | null;
689
+ };
690
+
691
+ export type AgentTelegramChat = {
692
+ id: string;
693
+ telegramChatId: string;
694
+ label: string;
695
+ status: 'pending' | 'allowed' | 'denied';
696
+ lastMessageAt: string | null;
697
+ };
698
+
699
+ export type DiffStatus =
700
+ /** Live matches the template (for tracked fields). */
701
+ | 'ok'
702
+ /** In the template, absent (or disabled) in the brain — a capability not landed. */
703
+ | 'missing'
704
+ /** In the brain, not in the template — operator-added, informational. */
705
+ | 'extra'
706
+ /** Present in both, but a tracked field diverges. */
707
+ | 'modified';
708
+
709
+ export type FieldDiff = {
710
+ /** 'toolGroupSlugs' | 'skillSlugs' | 'delegate_to' | 'instructions' |
711
+ * 'toolSlugs' | 'model' | 'systemPrompt' | 'enabled' */
712
+ field: string;
713
+ /** The template value — what an "adopt" would write. */
714
+ manifest: string | string[] | null;
715
+ /** The live value in the brain. */
716
+ live: string | string[] | null;
717
+ /** Set fields only: members in `live` but not `manifest` (operator-added). */
718
+ added?: string[];
719
+ /** Set fields only: members in `manifest` but not `live` (not landed). */
720
+ removed?: string[];
721
+ /** Informational-only diff (e.g. a specialist prompt) — shown, not weighted. */
722
+ info?: boolean;
723
+ };
724
+
725
+ export type EntityDiff = {
726
+ kind: EntityKind;
727
+ /** Agent/skill/group slug, or the worker kind. */
728
+ slug: string;
729
+ name: string;
730
+ status: DiffStatus;
731
+ severity: AuditSeverity;
732
+ /** One-line human summary of the difference. */
733
+ summary: string;
734
+ /** Tracked fields that differ (empty when status is 'ok'). */
735
+ fields: FieldDiff[];
736
+ /** Can the operator "Adopt from template" this item? True for missing/modified
737
+ * (apply the manifest version); false for ok (nothing to do) and extra
738
+ * (operator-added — adopting would mean deleting, which we never do). */
739
+ adoptable: boolean;
740
+ };
741
+
742
+ export type ConfigDiffReport = {
743
+ generatedAt: string;
744
+ /** The shipped template version (APP_VERSION). */
745
+ appVersion: string;
746
+ /** The version the brain was last auto-reconciled to (null if never). */
747
+ lastReconciledVersion: string | null;
748
+ entities: EntityDiff[];
749
+ counts: { ok: number; missing: number; extra: number; modified: number };
750
+ };
751
+
752
+ export type AdoptKind = 'persona' | 'agent' | 'skill' | 'tool-group' | 'worker';
753
+
754
+ // ── Server-lib view/query DTOs (jackdaw split P0 follow-up: @server/* purge) ──
755
+ // Moved from server/web/lib/* and @mantle/content; the originals re-export
756
+ // these names so server import paths are unchanged.
757
+
758
+ export type UpdaterPhase =
759
+ 'idle' | 'pulling' | 'rolling' | 'done' | 'error' | 'unconfigured' | 'requested';
760
+
761
+ /** The updater SCRIPT's own currency. Deliberately not `ComposeState`: the
762
+ * script has no `no-baseline` standoff (it self-adopts, having no supported
763
+ * box-local variation), so a missing baseline is not a state an operator can
764
+ * act on — the only actionable state is `modified`. */
765
+ export type UpdaterScriptState =
766
+ | 'in-sync' // box script == this release's canonical
767
+ | 'stale' // differs — self-refreshes on the next successful update
768
+ | 'modified' // differs from its baseline: hand-edited, refresh refused
769
+ | 'unknown'; // no stack.json, or a pre-v0.206 updater that reports no sha
770
+
771
+ export type ComposeState =
772
+ | 'in-sync' // box compose == this release's canonical
773
+ | 'stale' // pristine (== baseline) but not this release's — refresh hasn't run
774
+ | 'modified' // hand-edited canonical file — auto-refresh disabled, needs adoption
775
+ | 'no-baseline' // pre-adoption box — run scripts/compose-adopt.sh once
776
+ | 'unknown'; // no stack.json (old updater.sh / no sidecar / dev)
777
+
778
+ export type ReleaseInfo = {
779
+ /** Tag as published, e.g. "v0.20.67". */
780
+ tag: string;
781
+ /** Bare version, e.g. "0.20.67". */
782
+ version: string;
783
+ name: string;
784
+ url: string;
785
+ publishedAt: string | null;
786
+ };
787
+
788
+ export type DiskInfo = { usedBytes: number; totalBytes: number; usedPct: number; mount: string };
789
+
790
+ export type EntityKind = 'persona' | 'agent' | 'skill' | 'tool-group' | 'worker';
791
+
792
+ export type StudioNodeKind = 'agent' | 'skill' | 'group';
793
+
794
+ /** One peer on the tailnet (another device sharing your tailnet). */
795
+ export interface TailnetPeer {
796
+ /** MagicDNS name, trailing dot stripped — e.g. "gemma-box.tail1234.ts.net".
797
+ * This is what you'd put in a route base URL: http://<dnsName>:<port>/v1 */
798
+ dnsName: string;
799
+ /** Short hostname — e.g. "gemma-box". */
800
+ hostName: string;
801
+ /** Tailscale IPs (100.x.y.z / fd7a:…). Surfaced for reference; prefer names. */
802
+ ips: string[];
803
+ online: boolean;
804
+ /** OS string tailscaled reports (linux / windows / macOS …), best-effort. */
805
+ os: string | null;
806
+ }
807
+
808
+ export type ToolOutcomeStatsRow = {
809
+ calls: number;
810
+ succeeded: number;
811
+ failed: number;
812
+ skipped: number;
813
+ /** Confirm-gated calls parked behind operator approval — not yet run. */
814
+ queued: number;
815
+ failures: Array<{ slug: string; error: string }>;
816
+ };
817
+
818
+ // ── Server-lib view/query DTOs (jackdaw split P0 follow-up: @server/* purge) ──
819
+ // Moved from server/web/lib/* and @mantle/content; the originals re-export
820
+ // these names so server import paths are unchanged.
821
+
822
+ export type CacheHitStats = {
823
+ hits: number;
824
+ misses: number;
825
+ apiCalls: number;
826
+ };
827
+
828
+ export type DuplicateSuppression = {
829
+ /** Model slug captured in trace_steps.meta.model at suppression time. */
830
+ model: string;
831
+ /** How many duplicate tool_use blocks were suppressed in the window. */
832
+ count: number;
833
+ /** Distinct tool slugs the duplicates targeted (top 5, comma-separated). */
834
+ topSlugs: string;
835
+ /** Most recent suppression, ISO string. */
836
+ lastAt: string;
837
+ };
838
+
839
+ export type FactCostCapStats = {
840
+ /** Extractor model slug captured in trace_steps.meta.model. */
841
+ model: string;
842
+ /** How many process_facts steps dropped facts to the cap in the window. */
843
+ runs: number;
844
+ /** Total facts discarded across those runs (sum of meta.dropped). */
845
+ factsDropped: number;
846
+ /** Most recent occurrence, ISO string. */
847
+ lastAt: string;
848
+ };
849
+
850
+ export type Traffic = {
851
+ count: number;
852
+ errorCount: number;
853
+ avgMs: number | null;
854
+ costMicroUsd: number;
855
+ tokensIn: number;
856
+ tokensOut: number;
857
+ tokensCacheRead: number;
858
+ };
859
+
860
+ export type StudioGraph = {
861
+ generatedAt: string;
862
+ nodes: StudioNode[];
863
+ edges: StudioEdge[];
864
+ agents: StudioAgentDetail[];
865
+ skills: StudioSkillDetail[];
866
+ toolGroups: StudioToolGroupDetail[];
867
+ workers: StudioWorkerDetail[];
868
+ /** Live config-integrity report (the same checker behind /debug/integrity). */
869
+ report: SystemReport;
870
+ };
871
+
872
+ export type StudioAgentDetail = {
873
+ id: string;
874
+ slug: string;
875
+ name: string;
876
+ model: string;
877
+ role: string;
878
+ enabled: boolean;
879
+ isPersona: boolean;
880
+ skillSlugs: string[];
881
+ /** Skills attached but NOT resolved (missing or disabled) — surfaced honestly. */
882
+ missingSkillSlugs: string[];
883
+ delegateSlugs: string[];
884
+ /** Tool groups granted to this agent. */
885
+ toolGroupSlugs: string[];
886
+ /** Granted groups that are missing or disabled — surfaced honestly. */
887
+ missingToolGroupSlugs: string[];
888
+ toolCount: number;
889
+ params: { temperature?: number; max_tokens?: number };
890
+ maxIterations?: number;
891
+ /** Whether this is a manifest agent that can be reset to its canonical default. */
892
+ resettable: boolean;
893
+ /** The base system prompt (editable prose in Phase 2). */
894
+ systemPrompt: string;
895
+ /** The enabled, attached skills in composition order. */
896
+ skillBlocks: ComposedSkillBlock[];
897
+ /** The full assembled system prompt the model receives (base + skill blocks),
898
+ * exactly as `composeSystemPromptWithSkills` builds it on a real turn. */
899
+ composedPrompt: string;
900
+ };
901
+
902
+ export type ComposedSkillBlock = { slug: string; name: string; instructions: string };
903
+
904
+ export type StudioSkillDetail = {
905
+ id: string;
906
+ slug: string;
907
+ name: string;
908
+ enabled: boolean;
909
+ instructions: string;
910
+ /** Fan-out: every agent that attaches this skill (the many-to-many). */
911
+ usedByAgentSlugs: string[];
912
+ };
913
+
914
+ export type StudioToolGroupDetail = {
915
+ id: string;
916
+ slug: string;
917
+ name: string;
918
+ enabled: boolean;
919
+ toolSlugs: string[];
920
+ /** Fan-out: every agent that grants this group. */
921
+ usedByAgentSlugs: string[];
922
+ };
923
+
924
+ export type StudioWorkerDetail = {
925
+ id: string;
926
+ kind: string;
927
+ name: string;
928
+ model: string;
929
+ enabled: boolean;
930
+ isDefault: boolean;
931
+ /** Worker prose (registry): the chat-worker system prompt + the vision/document
932
+ * extraction prompt, when present. */
933
+ systemPrompt: string | null;
934
+ extractionPrompt: string | null;
935
+ issues: string[];
936
+ };