@crossworks/client-types 0.232.133 → 0.232.140

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