Repository navigation
Expand file tree
/
Copy pathtypes.ts
More file actions
2745 lines (2621 loc) · 110 KB
/
Copy pathtypes.ts
File metadata and controls
2745 lines (2621 loc) · 110 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// TypeScript mirrors of the OpenCompany operator API payloads.
// Kept in sync with src/runtime/types.rs, src/server/operator.rs, and
// src/feedback/{types,service}.rs.
/**
* Where a company's manifest was seeded from — the source template's stable
* identity, recorded once at launch. Mirrors `TemplateProvenance` in
* `src/ports/types.rs`. Absent for a company provisioned from a raw manifest.
*/
export interface TemplateProvenance {
/** The template's stable id — the source directory slug. */
source_id: string;
/** The template's version, when the source exposes one. */
version?: string | null;
/** The source directory the company was launched from, when recorded. */
path?: string | null;
}
/** `GET /api/v1/companies` and `GET /api/v1/companies/{id}`. */
export interface CompanyStatus {
id: string;
name: string;
/** e.g. "running", "paused", "suspended", "archived". */
lifecycle: string;
pending_approvals: number;
/**
* The source-template provenance recorded at launch (issue #85). Absent for
* a company provisioned from a raw manifest rather than a template.
*/
template_provenance?: TemplateProvenance | null;
/**
* Whether the governance kill switch is engaged (issue #86): new effects
* outside the `Other` group are being denied.
*
* Deliberately independent of `lifecycle`, which stays `"running"` — chat
* still works while a company is stopped. Anything showing company state must
* read this too, or it will report a stopped company as perfectly healthy.
*/
emergency_paused?: boolean;
}
/**
* `GET /api/v1/companies/provisioning` — the sign-in mode a company provisioned
* on this host right now would land in, so the create/reset dialog can collect
* the right identity field before it builds a manifest. Mirrors
* `ProvisioningInfoDto` in `src/server/provision.rs`.
*/
export interface ProvisioningInfo {
/** The effective sign-in mode: `wallet`, `email`, or `none`. */
auth_mode: "wallet" | "email" | "none";
/** Whether provisioning requires at least one `[users].wallets` address. */
wallets_required: boolean;
}
/** What kind of processing step this is (drives the timeline icon). */
export type TurnStepKind = "tool_call" | "thinking" | "note";
/**
* How a processing step ended.
*
* `awaiting_approval` (#411) is **not** a failure: the call was gated and is
* waiting on a person. It used to arrive as `error`, which made the one step an
* operator could act on look like the one thing that had crashed. Anything
* counting failures must key on `error` alone — see {@link isFailedStep}.
*/
export type TurnStepStatus = "ok" | "error" | "running" | "awaiting_approval";
/**
* Why a step did not succeed, in the failure's own terms (#411). Mirrors
* `TurnStepFailure` in `src/ports/types.rs`.
*
* Rendered by lookup, never by reading the prose in `result` — the whole point
* is that the console switches on a known state instead of pattern-matching a
* sentence. Absent on a success, on a step still `running`, and on a parked
* one (its status already says what it is).
*/
export type TurnStepFailure =
| "declined"
| "blocked_by_policy"
| "unauthorized"
| "missing_permission"
| "missing_app"
| "not_found"
| "unsupported"
| "timeout"
| "unavailable"
| "failed";
/**
* One visible step in an agent turn's processing timeline. Mirrors `TurnStep`
* in `src/ports/types.rs`. The host folds and scrubs these from the turn's
* progress stream: nothing here carries raw tool output or call ids, and
* arguments reach `detail` only through the host-side redactor an approval card
* already uses (#372).
*/
export interface TurnStep {
kind: TurnStepKind;
status: TurnStepStatus;
label: string;
/**
* **What the step was doing** — its arguments, redacted host-side and
* bounded (#411). This is what tells two calls to the same tool apart.
*/
detail?: string;
/**
* **What came back** — a shape summary (`"12 items"`), an intrinsic tool's
* own message, or a failure's plain-language cause (#411). Never a remote
* body's content.
*/
result?: string;
/** The typed reason the step did not succeed (#411). */
failure?: TurnStepFailure;
/** The result was cut before the agent could read all of it (#410). */
truncated?: boolean;
/** How long a tool call took, in milliseconds, when known. */
elapsedMs?: number;
}
/**
* Short, operator-facing copy for each {@link TurnStepFailure}, plus the word
* for a parked step.
*
* One table, used by both render surfaces (the chat bubble's step timeline and
* the task Attempts tab), so a failure cannot be named two different things
* depending on where you happen to be looking at it.
*/
export const STEP_FAILURE_LABEL: Record<TurnStepFailure, string> = {
declined: "Declined",
blocked_by_policy: "Blocked by policy",
unauthorized: "Unauthorized",
missing_permission: "Missing permission",
missing_app: "App unavailable",
not_found: "Not found",
unsupported: "Not supported",
timeout: "Timed out",
unavailable: "Service unavailable",
failed: "Failed",
};
/** The word for a step waiting on a person. Not a failure. */
export const AWAITING_APPROVAL_LABEL = "Awaiting approval";
/**
* Whether a step counts as **failed**.
*
* The single place the question is answered on the client, mirroring
* `TurnStepStatus::is_failure` on the host. A parked step is deliberately not a
* failure (#411).
*/
export function isFailedStep(status: TurnStepStatus | undefined): boolean {
return status === "error";
}
/** One addressable object produced by a chat-started agent turn. */
export interface ChatOutput {
kind: "workspace-node" | "artifact";
targetId: string;
/** Bounded, redacted label supplied by the host. */
title: string;
/** Required for artifact links; absent for workspace nodes. */
taskId?: string;
/** Exact artifact revision produced by the turn. */
version?: number;
}
/** One channel reply from a cycle. */
export interface OutboundMessage {
channel: string;
text: string;
/**
* The visible processing steps behind this reply (tool calls, thinking runs,
* surfaced MCP failures). Omitted by the host when empty — a memory-served or
* tool-less answer carries no steps, which is the tell that distinguishes it
* from a tool-backed one.
*/
steps?: TurnStep[];
/** Channel-specific reply addressing. Absent on operator messages. */
replyTo?: ReplyTo;
/**
* The board card this turn opened, when it opened one (issue #246). Drives
* the reply bubble's "card opened" chip. Absent when the turn opened nothing
* — which is every reply the host sent before this field existed.
*
* Only the *first* card of a turn that opened several: the journal field this
* is persisted into is a single id, so the claim is incomplete but never
* wrong. The bubble's `steps` timeline still shows every spawn.
*/
taskId?: string;
/** Workspace nodes and artifacts produced by this reply's turn. */
outputs?: ChatOutput[];
/**
* Who this reply names, as the host resolved them (issue #1645).
*
* Absent when it names nobody, and on a host that predates the field.
* When present the renderer can chip the resolved spans immediately
* rather than waiting for the history-rehydration path - the live POST
* response delivers the same mentions `chat/history` will later return.
*/
mentions?: ChatMentionDto[];
/**
* The durable id this reply was journaled under (issue #364) — the id
* `chat/history` will return for it. Absent on a reply the host could not
* journal, and on a host that predates the field; either way the console
* treats the bubble as un-threadable rather than inventing an id for it.
*/
messageId?: string;
/**
* Whether {@link message} is the exact, user-facing sentence for a
* fail-closed turn (keys rework, issue #2306, round-2 review KR-L2-03) —
* a pinned provider switched off or removed, a broken company default, or
* no model chosen at all. Absent (or `false`) means `text` already carries
* whatever prose applies — the generic retry line, or a provider/network
* failure's own classified sentence — and the console renders that as
* always.
*
* Field names match `docs/key-reworks/in-use-guards.md` §5 — see
* `src/lib/turn-failure.ts`'s own module doc for the full explanation.
*/
userFacing?: boolean;
/** One of the codes `src/lib/turn-failure.ts`'s `TURN_FAILURE_CODES` names, when {@link userFacing} is true. */
code?: string;
/** The X9 sentence itself, with display names — present only when {@link userFacing} is true. */
message?: string;
/** The agent this failure's pair names, when the code names one — never this reply's own author (see `channel`), which is a different question. */
pairAgentId?: string;
/** The provider slug the failure names, when there is one. */
providerSlug?: string;
}
/** Channel-specific reply addressing. Mirrors `ReplyTo` in `src/ports/types.rs`. */
export interface ReplyTo {
/** The chat/thread id to deliver back to. */
chatId: string;
}
/**
* `GET {scope}/desks` — one desk (group chat). Mirrors `DeskDto` in
* `src/server/operator.rs`. The `id` doubles as the chat thread id; `members[0]`
* is the desk's lead.
*/
export interface DeskDto {
id: string;
name: string;
description?: string;
/** Effective members: manifest members unioned with overlay additions. */
members: string[];
/**
* The subset of `members` added through the operator overlay (issue #72).
* Only these can be removed at runtime; manifest members are part of the
* company blueprint. Omitted (undefined) when there are none.
*/
overlayMembers?: string[];
/**
* How this desk's unmentioned messages find their answerer (issue #1835):
* `"auto"` is a channel with **no lead** — `members[0]` carries no rank, and
* the host picks a best-fit member per message — so every lead affordance
* (crown, badge, Make lead) is suppressed for it. Omitted means `"lead"`,
* which is every manifest desk and every desk created before the field
* existed.
*/
responder?: "lead" | "auto";
/**
* Whether the whole desk was operator-created (an overlay desk) rather than
* declared in the manifest blueprint. The console offers a delete action only
* for these. Omitted (undefined/false) for blueprint desks.
*/
overlayCreated?: boolean;
/**
* How this desk paces the episodes it opens — the numbers in force, not the
* editable block (see {@link DeskRoutingDto}). Absent on a host predating
* desk routing, and on a leadless or single-member desk that runs none.
*/
routing?: DeskRoutingSummaryDto;
}
/**
* How a desk routes and paces the episodes it opens (`[group_chat.routing]`).
*
* Mirrors `DeskRoutingDto` in `src/server/operator.rs`. The `declared` /
* `effective` split is the reason this is its own payload rather than a field
* on {@link DeskDto}: a `roundWidth: 2` is either an operator's decision or the
* library default, and the two behave differently the moment the manifest
* changes — so one number cannot tell the editor which it is looking at.
*/
export interface DeskRoutingDto {
deskId: string;
/** Where the block in force came from. */
source: DeskRoutingSource;
/** The block as authored. Snake_case: this field **is** the manifest block. */
declared: DeskRoutingDeclared;
/** What the runtime will use, every default resolved. */
effective: DeskRoutingEffective;
/**
* Every seat the router may pick, with the other desks each one also sits
* on — a shared seat is the one whose turn can be delayed by another desk's
* round, which is worth seeing before widening a round to include it.
*/
candidates: DeskRoutingCandidateDto[];
}
/** Where a desk's routing block in force came from. */
export type DeskRoutingSource = "overlay" | "manifest" | "default";
/**
* The `[group_chat.routing]` block as authored — snake_case, because it is the
* TOML block verbatim and a camelCase twin would be a second shape to keep in
* step with it. Every key absent means "not said", which is distinct from any
* value it could hold.
*/
export interface DeskRoutingDeclared {
round_width?: number;
choice_option_limit?: number;
minimum_confidence?: number;
high_impact_minimum_confidence?: number;
clarification_threshold?: number;
high_impact_threshold?: number;
max_rounds?: number;
turn_timeout_secs?: number;
referral?: {
enabled?: boolean;
max_hops?: number;
reach?: string;
returns?: boolean;
};
}
/** The routing numbers the runtime will actually use, defaults resolved. */
export interface DeskRoutingEffective {
roundWidth: number;
choiceOptionLimit: number;
maxRounds: number;
turnTimeoutSecs: number;
/** Which router picks the seats: the System One (`jev`) router, or the
* lead/mention fallback the host uses without one. */
router: RoutingRouter;
minimumConfidence?: number;
highImpactMinimumConfidence?: number;
clarificationThreshold?: number;
highImpactThreshold?: number;
referral?: {
enabled: boolean;
maxHops: number;
reach?: string;
returns: boolean;
};
}
/** One seat the router may pick, and where else it sits. */
export interface DeskRoutingCandidateDto {
agentId: string;
label: string;
role: string;
/** The other desks this agent is also a member of. Empty for a seat that is
* this desk's alone. */
sharedWith: string[];
}
/**
* The compact routing summary a desk carries on the list (`GET {scope}/desks`),
* so the room can label a round without a second read. The full editable block
* is {@link DeskRoutingDto}. Optional on {@link DeskDto}: a host predating it
* omits it, and the room then labels nothing.
*/
export interface DeskRoutingSummaryDto {
source: DeskRoutingSource;
roundWidth: number;
choiceOptionLimit: number;
maxRounds: number;
turnTimeoutSecs: number;
router: RoutingRouter;
}
/** Which router chose the seats of a round. */
export type RoutingRouter = "jev" | "fallback" | "explicit";
/**
* How a message was routed to seats — the plan a round opened with, or the
* plan a broadcast resolved to. Mirrors `RoutingPlanDto` in
* `src/server/operator.rs`, which is tinyhivemind's `RoutingPlan` on the wire.
*/
export type RoutingPlanDto =
| { kind: "one"; primaryId: string }
| { kind: "hive"; primaryId: string; invitedIds: string[] }
| { kind: "clarify"; question?: string }
/**
* The deterministic destination and why the router did not decide. The
* host names the seat it fell back to (`primaryId`) beside the reason —
* the seat is what a comms edge or a round lane needs; optional only for
* a host predating the field.
*/
| { kind: "fallback"; primaryId?: string; reason: string };
/**
* The one speech act a seat ends its turn with. Mirrors
* `tinyhivemind::speech::Utterance`'s four kinds — `read` is a query, not an
* utterance, and never reaches the journal.
*/
export type UtteranceKind = "post" | "broadcast" | "dm" | "complete_episode";
/**
* What a journaled reply was, inside the episode that produced it. Mirrors the
* `episode` field of `ChatHistoryMessageDto` in `src/server/operator.rs`.
*
* Optional everywhere it appears: a reply outside an episode — a DM, `#general`,
* a workflow relay, every row from a host predating episodes — carries none,
* and renders exactly as it always has.
*/
export interface MessageEpisodeDto {
/** The episode this reply was committed into. */
id: string;
/** The round it was committed in — the driver's revision when it landed. */
revision: number;
kind: UtteranceKind;
/** A `dm`'s recipients, by agent id. */
to?: string[];
/** How a `broadcast` was routed onward, when the host recorded it. */
routedBy?: { plan: RoutingPlanDto; router: RoutingRouter };
}
/**
* `GET {scope}/episodes?desk&status&limit` — one episode a desk ran or is
* running. Mirrors `EpisodeDto` in `src/server/operator.rs`.
*/
export interface EpisodeDto {
id: string;
/** The desk it ran on. */
chatId: string;
/** The journal sequence of the message that opened it. */
openedBySeq: number;
/** The thread root inside that desk, when the message was in a thread. */
parentId?: string;
participants: string[];
plan: RoutingPlanDto;
/** The driver's current revision — how many rounds have committed. */
revision: number;
status: "open" | "completed";
openedAtMillis: number;
completedAtMillis?: number;
completedBy?: string;
reason?: EpisodeCompletionReason;
}
/** Why an episode closed. Widened by the console to a string on read, so a
* word from a newer host is not a type error. */
export type EpisodeCompletionReason =
| "complete_episode"
| "round_cap"
| "timeout"
| "failed"
| "membership_changed";
/**
* `GET {scope}/operator-channel` — the identity of the company's
* always-present, durable Operator feed (issue #1757 rework): a read-only
* "what happened" feed aggregating workflow-run reports and the owner/
* no-mailbox fallback. Its own surface, not a desk — the console pins it
* below a divider in the chat rail instead of folding it into `GET
* {scope}/desks`. Mirrors `OperatorChannelDto` in `src/server/operator.rs`.
*/
export interface OperatorChannelDto {
/** The channel id — the `desk` query param `chat/history` reads through. */
id: string;
/** Always "Operator" — the console's pinned-row label. */
name: string;
/** The channel's purpose line, shown under the name in the pinned row. */
description: string;
}
/**
* Body for `POST {scope}/desks` — create a desk. `name` is required; `id` is
* derived from the name when omitted; `members` are optional roster teammate
* ids (the first becomes the lead).
*/
export interface CreateDeskInput {
name: string;
description?: string;
id?: string;
members?: string[];
/**
* How the desk routes its unmentioned messages (issue #1835). Absent means
* `"lead"` — today's model, what the org chart's create sends. `"auto"`
* creates a leadless channel whose answerer is picked per message.
*/
responder?: "lead" | "auto";
}
/**
* `GET {scope}/chat/history` — one persisted transcript message. Mirrors
* `ChatHistoryMessageDto` in `src/server/operator.rs`. Shares its filter +
* projection logic with the GraphQL `Chat.history` resolver, so the two can
* never disagree about a desk's history (issue #65).
*/
/**
* Where a message came from when another desk caused it (tinyhivemind P15).
*
* A crossing referral runs a turn on a desk the asker is not a member of, so
* the message needs to say so on its face — otherwise a turn that exists only
* because engineering asked reads as design's own idea.
*
* The labels are **captured with the row**, never resolved at render, for the
* reason `SessionAuthor` captures its own: a desk renamed later must not
* rewrite what the transcript said at the time.
*/
/** One line of a crossing between a desk and somebody who does not work on it. */
export interface ReferralLineDto {
authorId: string;
/** Empty for this desk's own agent, whom the console already names. */
authorLabel: string;
text: string;
/** True for the question going out, false for the answer coming back. */
outbound: boolean;
}
/**
* A crossing folded onto the report that brought it home.
*
* The relayed rows are dropped from the transcript — an agent who does not work
* on this desk did not speak on it — so without this the operator could see that
* a question crossed and never what was said either way. `lines.length` is the
* count the collapsed label shows.
*/
/**
* One agent-to-agent exchange on this desk, folded onto the `ask` row that
* opened it.
*
* The same shape and the same `ReferralLineDto` rows as
* {@link ReferralConversationDto}: to a reader both are an exchange somebody
* on this desk had that the desk's own transcript cannot show. The difference
* is where the rows are — a crossing's are dropped host-side, these live in
* the pair channel the two seats wrote to.
*/
export interface AgentConversationDto {
/**
* The `ask` row it is rooted at: its identity.
*
* Two seats can hold several exchanges inside one episode and they share a
* channel — `pair_conversation` is deterministic, so each is `dm:<a>+<b>`.
* Without this they are indistinguishable: same asker, same askee, same
* channel, and a reader sees the same line twice with nothing to tell them
* apart. It is also the only safe React key for the same reason.
*/
root: number;
askerId: string;
askeeId: string;
/** The channel the exchange is written to (`dm:{a}+{b}`). */
conversationId: string;
/** Whether it has ended. A live exchange is worded in the present tense. */
concluded: boolean;
/** Whether it ended by running out of turns rather than by concluding. */
forced: boolean;
lines: ReferralLineDto[];
}
export interface ReferralConversationDto {
askerId: string;
otherId: string;
otherDeskId: string;
otherDeskName: string;
/** Whether a person was asked rather than a desk — `@name` vs `#desk`. */
direct: boolean;
/**
* Whether this desk was **asked** rather than doing the asking.
*
* Every other field is named from the asking desk's side, because that was
* the only desk a crossing used to be folded onto. Optional because a host
* predating it omits it, and `undefined` means the asking side — which is
* what every older row is.
*/
inbound?: boolean;
lines: ReferralLineDto[];
}
/**
* One line of an agent's session: a chat row plus where it was said.
*
* The session view is one continuous stream across every channel an agent can
* read, so a row that does not say which channel it came from is unreadable —
* two teammates answering in two desks would interleave with nothing to tell
* them apart. Everything else is a plain {@link ChatHistoryMessageDto}, which
* is what lets `fromHistory` map it and the room's own components render it,
* referral collapses and utterance chips included.
*/
export interface AgentSessionMessageDto extends ChatHistoryMessageDto {
/** The channel as the rail names it — `#Brand`, `#general`, `dm`. */
sessionChannel: string;
/** The desk id behind that label, so a row can link to its conversation. */
sessionChannelId: string;
/**
* **What the agent was told to call this row's author.**
*
* Not {@link ChatHistoryMessageDto.author}, and not a substitute for it. That
* one is the display name a *person* reads, walking the ladder chosen name →
* a name derived from the login identity → `"someone"`. This one is what the
* runtime puts in the cue line the model is handed, and it is a **stable id**
* — the signed-in user's id, or `"operator"` for a machine credential.
*
* The two differ on purpose. An agent's byline becomes a per-line attribution
* prefix, so it has to be unique and unforgeable; a display name is neither,
* and a person who set theirs to a teammate's id could otherwise have their
* lines prefixed as if that teammate had said them. A person reading a
* transcript needs the opposite — a name, not a key.
*
* Only the raw view reads it, and only because it claims to show the string
* the model received. Optional: a host predating the field omits it, and the
* honest fallback there is the display name with no claim attached.
*/
cueAuthor?: string;
/**
* **The text the model was actually handed for this row** — before the
* host's move-marker rewrite turned it into operator-facing prose (e.g.
* `!support #topic ^3` becoming a sentence).
*
* Same reasoning as {@link cueAuthor}, for the other half of the cue line:
* the raw view claims to show the string the model received, and
* {@link ChatHistoryMessageDto.text} has already been rewritten for a
* person to read. Equal to `text` on every row the rewrite did not touch.
* Optional for the same reason `cueAuthor` is — a host predating the field
* omits it, and the honest fallback is the rendered text with no claim
* attached.
*/
cueText?: string;
/**
* **The openhuman session this agent's turns belong to** —
* `{company}:{agentId}`.
*
* Minted host-side by `openhuman_session_key` (`src/harness/session_key.rs`),
* the one function that names a session, and the same string stamped onto the
* live session's `event_context`. Never rebuilt here: a `${company}:${id}`
* in TypeScript would be a second spelling of a session's name, and a second
* spelling is one that can drift from the one the runtime actually answers to.
*
* Carried per row rather than in an envelope because the route answers a bare
* array and every caller indexes it; see the Rust DTO for the full reasoning.
* Every row of one response carries the same value. Optional: a host
* predating the field omits it, and the honest thing then is to show no
* session name rather than a guessed one.
*/
openhumanSessionKey?: string;
}
export interface ReferredFromDto {
deskId: string;
deskName: string;
askerId: string;
askerLabel: string;
/** The asking message, so the chip can link straight to it. */
sequence: number;
/** Whether a person was asked rather than a desk. */
direct?: boolean;
/**
* Which leg of the referral this message is: the outbound ask, or the answer
* arriving home.
*
* The host says it because only the host can. Both legs are agent-authored
* lines on a desk, so `from`, `byPerson` and the author all read identically
* on each — a console that guesses from those gets every return wrong, which
* is exactly what it did before this field existed.
*
* Optional: a host that predates it says nothing, and the chip then falls
* back to "asked", which is what every marker written before the return leg
* shipped actually was.
*/
direction?: "asked" | "answered";
}
export interface ChatHistoryMessageDto {
id: string;
channel: string;
author: string;
text: string;
/**
* **The body as the model wrote it** — {@link text} before the host rewrote
* the room's grammar into operator-facing prose.
*
* Absent when the two are equal, which is every row carrying no move, and
* absent from a host predating the field. Read it wherever the *moves* are
* the point rather than the prose: the episode fold counts
* `!propose`/`!support`/`^N`, and reading {@link text} there is why a
* deliberation panel never survived a refresh.
*/
cueText?: string;
referredFrom?: ReferredFromDto;
referralConversation?: ReferralConversationDto;
agentConversations?: AgentConversationDto[];
/**
* What this reply was inside the episode that produced it — its round, its
* speech act, and for a `dm` who it went to. Absent for every reply outside
* an episode and on a host predating episodes; such a row renders exactly as
* before.
*/
episode?: MessageEpisodeDto;
/**
* Who may read this line, by agent id, when the host narrowed it — a `dm`
* inside a desk. Absent means everyone on the desk. An operator reads every
* line regardless: audience is a coordination device, not access control.
*/
audience?: string[];
atMillis: number;
mine: boolean;
/**
* Whether a **person** typed this line rather than the runtime (issue #1734).
*
* `mine` answers a different question — "did *you* write it" — and is relative
* to the reader, so a colleague's own message is `mine: false` and arrives on
* the company side of the transcript beside the agent replies. Nothing here
* can separate the two without this field, and the obvious substitute is a
* trap: the offline echo brain names its own outbound channel `operator`,
* exactly as an operator message does, so `channel === "operator"` matches
* both.
*
* Optional because a host predating it omits it. `undefined` means "cannot
* say", and the honest rendering of that is today's behaviour — never a
* confident "the runtime wrote this".
*/
byPerson?: boolean;
/**
* The scrubbed processing steps behind a company reply, so a rehydrated
* transcript renders the same timeline the live turn showed. Omitted when
* empty (operator messages, tool-less replies).
*/
steps?: TurnStep[];
/**
* The board card this reply is about (issue #246) — the card the turn opened,
* or the dispatched card it ran for (#185). Projected from the same shared
* `MessageView` field the GraphQL `Chat.history` resolver reads, so the chip
* renders identically whichever surface hydrated the transcript.
*/
taskId?: string;
/** Live output buttons to restore when this transcript is rehydrated. */
outputs?: ChatOutput[];
/**
* The message this one replies to (issue #364), by that message's own `id`.
* Absent for a message posted straight into the channel — which is every
* message journaled before threads were persisted.
*/
parentId?: string;
/**
* Who reacted to this message with what (issue #364), one row per person per
* emoji. Absent when nobody has, and on a host that predates the field.
*/
reactions?: ChatReactionDto[];
/**
* Files attached to this message (issue #1682), each a reference into the
* company workspace with the store-computed name / mime / size. Absent when
* the message carries none — which is every reply, every system pill, and
* every operator message journaled before the field existed — and on a host
* that predates it.
*/
attachments?: AttachmentDto[];
/**
* Who this message names, in reading order. Absent when it names nobody, and
* on a host that predates the field.
*/
mentions?: ChatMentionDto[];
/**
* Whether {@link message} is the exact, user-facing sentence for a
* fail-closed turn (keys rework, issue #2306, round-2 review KR-L2-03) —
* see {@link OutboundMessage.userFacing}'s doc; this is the same shape,
* rehydrated.
*/
userFacing?: boolean;
/** One of the codes `src/lib/turn-failure.ts`'s `TURN_FAILURE_CODES` names, when {@link userFacing} is true. */
code?: string;
/** The X9 sentence itself, with display names — present only when {@link userFacing} is true. */
message?: string;
/** The agent this failure's pair names, when the code names one. */
pairAgentId?: string;
/** The provider slug the failure names, when there is one. */
providerSlug?: string;
}
/**
* One file attached to a message (issue #1682). Mirrors `ChatAttachmentDto` in
* `src/server/operator.rs`. Every field is store-authored metadata; the bytes
* are fetched separately through the hardened `…/workspace/blob/{nodeId}` route.
*/
export interface AttachmentDto {
/** The workspace node id the payload is stored under — handed to the blob
* route to download or preview it. */
nodeId: string;
/** The stored file's display name. */
name: string;
/** The stored payload's media type, so the console decides download-vs-
* preview without fetching the bytes. */
mime: string;
/** The stored payload's exact length in bytes. */
size: number;
}
/**
* What a mention points at. Mirrors the Rust `MentionTarget`.
*
* `everyone` is a scope rather than an actor, which is why this is a union and
* not an `{ kind, id }` pair.
*/
export type MentionTarget =
| { kind: "agent"; id: string }
| { kind: "user"; id: string }
| { kind: "desk"; id: string }
| { kind: "everyone" };
/**
* One mention as the composer *sends* it. Mirrors the Rust `Mention` input.
*
* Distinct from {@link ChatMentionDto}, which is what comes back: outgoing
* carries the target the picker resolved, incoming carries the label the host
* resolved it to. A client never sends a label and never receives a target.
*/
export interface ChatMentionInput {
target: MentionTarget;
/** The literal span typed, `@` included. */
text: string;
/** UTF-8 byte offset of `text` in the message body. */
offset: number;
}
/** One mention. Mirrors `ChatMentionDto` in `src/server/operator.rs`. */
export interface ChatMentionDto {
/** The literal span the author typed, `@` included. */
text: string;
/** Byte offset of `text` in the message body. */
offset: number;
/** Who was named, as a display label — never a raw user id. */
label: string;
/** Whether the reading viewer is the one named (or was named by @everyone). */
mine: boolean;
/** Whether this mention renders but pinged nobody. */
quiet?: boolean;
}
/** One person's reaction. Mirrors `ChatReactionDto` in `src/server/operator.rs`. */
export interface ChatReactionDto {
emoji: string;
/** Who reacted, as a display label — never a raw user id. */
by: string;
/** Whether the reading viewer is the one who reacted. */
mine: boolean;
}
/** Response of `/chat` and approval-resolution routes. */
export interface ChatResponse {
responses: OutboundMessage[];
/**
* On a resolve: how many OTHER decisions the turn behind that approval is
* still blocked on (issue #561). `0` means this decision released it.
*
* Absent on every other answer, and on a host that predates the field — which
* the console reads as "cannot tell", and words its confirmation without a
* claim about what happens next rather than guessing the optimistic one.
*/
stillAwaiting?: number;
/**
* The durable id the operator's own message was journaled under (issue #364)
* — the id `chat/history` will return for it. Absent on a host that predates
* the field, which the console reads as "this message cannot be threaded or
* reacted to" rather than guessing an id.
*/
messageId?: string;
/**
* The durable turn row this message opened (issue #983) — pollable at
* `GET {scope}/runs/{turnId}`. Additive: absent on a host that predates the
* field, which the console reads as "this turn cannot be watched", falling
* back to re-reading history.
*/
turnId?: string;
/**
* On a resolve: which end state it reached (#1449).
*
* The Approvals page resolves **without** `detach`, so it never sees a
* {@link ResolveReceipt} — this is the only shape that can tell it its click
* was refused. Absent on every other answer, and on a host that predates the
* field.
*/
outcome?: ResolveOutcome;
/**
* Set when a thread reply was intercepted as review feedback on an
* `in_review` dispatch card and re-dispatched it instead of answering with
* `responses` here. The re-run's own reply still arrives later on the event
* stream and in `chat/history` — this only tells the console not to read an
* empty `responses` as "the turn produced nothing." Absent on every other
* answer, and on a host that predates the field.
*/
reviewFeedbackApplied?: boolean;
/**
* On a resolve: every approval it settled, when it settled more than the one
* addressed. A blocker answered here fans its verdict to its whole
* root-cause group, so the queue owes the siblings the same removal it gives
* the card that was clicked. Absent on every other answer, and on a host that
* predates the field.
*/
settledIds?: string[];
}
/**
* Where a reviewed card lands: `done` on approve, `in_progress` on revise —
* or `in_review`, unchanged, on a revise whose note was blank. The host
* treats an empty note as nothing to re-run on and leaves the card where it
* was rather than dispatching an identical attempt a second time.
*/
export type ChatReviewColumn = "done" | "in_progress" | "in_review";
/**
* The card a thread review verdict left behind, so the console can reconcile
* its optimistic move. Mirrors `ChatReviewReceipt` in `src/server/operator.rs`.
*/
export interface ChatReviewReceipt {
/** The reviewed card's id. */
taskId: string;
/** The column it landed in — see {@link ChatReviewColumn}. */
column: ChatReviewColumn;
}
/**
* The answer to a **detached** chat post (issue #983): the turn has been
* accepted, journaled and given an id, and that is all it claims. The reply
* arrives afterwards on the event stream's `agent_reply` frame, and durably in
* `chat/history`.
*
* `detached` is a constant `true` and exists to be *present*: a newer console
* pointed at a host that predates the field sends `detach`, the host ignores it,
* and the full synchronous body comes back. So the console can only tell the two
* apart by what arrived — never by what it asked for.
*/
export interface DetachedChatResponse {
/**
* The turn's durable row, to poll. Optional for the same reason
* `ChatResponse.turnId` is: a run store that refused a row does not get to
* refuse the turn, so a detached turn can exist unwatched.
*/
turnId?: string;
/**
* The durable id of the operator's own message. Never optional here — since
* #983 the append happens at accept time, so it is already a fact when this
* body is written. That is what lets the console reconcile its optimistic
* bubble immediately instead of waiting for the turn to settle.
*/
messageId: string;
detached: true;
}
/** What `POST {scope}/chat` can answer with — settled, or accepted (#983). */
export type ChatPostResult = ChatResponse | DetachedChatResponse;
/**
* Which shape came back, decided on the **response**, never on the request.
*
* Reads `detached` as a presence check rather than trusting `detach` was
* honoured: an older host silently ignores the field and answers synchronously,
* and a console that assumed otherwise would sit waiting for a reply it was
* already holding.
*/
export function isDetachedChat(answer: ChatPostResult): answer is DetachedChatResponse {
return (answer as DetachedChatResponse).detached === true;
}
/** One parked approval from `/approvals`. */
export interface ApprovalSummary {
id: string;
/** The parked effect's dotted kind, e.g. "payment.send". */
kind: string;
amount_usd: number | null;
/**
* Epoch-millis the effect was parked — stamped in the same turn that composed
* its arguments, so it dates the **payload**, not the queue (#1024).
*/
at_millis: number;
/**
* Epoch-millis this approval default-denies if nobody decides it (#971) —
* `at_millis` plus the company's approval deadline
* (`[policy].approval_ttl_hours`, 24 hours by default).
*
* **Never recompute it.** The host projects it from the gate that actually
* enforces the deadline; a console that added its own 24 hours to
* `at_millis` would show a deadline nothing enforces, and an operator would
* act on "in 3h" and be refused.
*
* Optional because a host may predate the field. Absent means "this host
* does not report deadlines" — render the card exactly as before rather
* than guessing one.
*/
expires_at_millis?: number | null;
/**
* The host's consequence group for the parked effect (#1024).
*
* Derived server-side from the tool **and its arguments**, so a
* `composio_execute` carrying `GMAIL_SEND_EMAIL` arrives as `"send"` rather
* than as the catch-all its tool name alone implies. It cannot be computed
* here: for a harness tool call `kind` is the tool name, so a console keying
* on `kind` would miss exactly the outbound sends this marks.
*
* Optional, and that is how an old host degrades: no field, no age label,
* exactly the pre-#1024 card.
*/
group?: "spend" | "send" | "sign" | "publish" | "hire" | "identity" | "other";
/**
* Which board task **owns** this approval (#333, resolved by #1891).
*
* Three states, deliberately: `{link: "task"}` is owned by that card,
* `{link: "unlinked"}` is owned by no card (a workflow delivery, an
* operator-chat turn, a scheduler tick), and *absent* means the park predates