forked from highflame-ai/codeoid
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtypes.ts
More file actions
1223 lines (1123 loc) · 39.9 KB
/
Copy pathtypes.ts
File metadata and controls
1223 lines (1123 loc) · 39.9 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
/**
* Codeoid Server Protocol v2
*
* Design principles:
* 1. Every message is self-contained, serializable JSON — no observables, no callbacks
* 2. Every message carries identity (who produced it) — auditable top to bottom
* 3. Discriminated unions with `kind` fields — frontends switch on kind, ignore unknown
* 4. Simple frontends (Telegram) use role + content string, rich frontends use parts[]
* 5. Tool calls are state machines — streaming → confirmation → executing → completed
* 6. Streaming via delta messages — reference a messageId, append content
* 7. Extensible — new roles, content parts, tool states added without breaking existing frontends
*
* Inspired by VS Code's IChatProgress union and tool invocation state machine.
* Adapted for network transport (JSON over WebSocket) and multi-frontend/multi-user.
*/
import type { Scope } from "./scopes.js";
/**
* Wire-protocol version. Bump on breaking changes (renamed/removed fields,
* renamed message kinds, altered semantics). Additive changes (new optional
* fields, new message kinds) do NOT require a bump — the "ignore unknown"
* discipline covers those.
*
* Native clients (e.g. the Rust Ratatui frontend) compare this against their
* own compiled-in version on `auth.ok` and warn the user if they've drifted.
*/
export const PROTOCOL_VERSION = 1;
// =============================================================================
// Session metadata
// =============================================================================
// Active-turn status is split into two sub-states so clients can show what
// the agent is actually doing: `thinking` (reasoning / generating text) vs
// `tool_running` (a tool is executing — clients surface the tool name).
export type SessionStatus =
| "idle"
| "thinking"
| "tool_running"
| "waiting_approval"
| "error";
/** True when the session is mid-turn (either reasoning or running a tool). */
export function isActiveStatus(s: SessionStatus): boolean {
return s === "thinking" || s === "tool_running";
}
/**
* Execution mode — controls tool approval and autonomous budgeting.
*
* - `guarded` (default): Read/Grep/Glob/memory are auto-approved; Write/Edit/Bash/Agent
* still ask. The name says it plainly — it AUTO-runs the safe reads but GUARDS the
* mutations. (≈ Claude Code's default mode.) Formerly named `auto-allow`.
* - `interactive`: every tool call asks for approval, including reads.
* - `autonomous`: every tool auto-approved until the turn budget (`maxTurns`) is exhausted;
* session then reverts to `guarded`. (≈ Claude Code's bypass-permissions mode.)
*/
export type SessionMode = "interactive" | "guarded" | "autonomous";
export interface SessionInfo {
id: string;
name: string;
workdir: string;
status: SessionStatus;
createdBy: string;
createdAt: string;
attachedClients: number;
/** Current execution mode (default "interactive"). */
mode?: SessionMode;
/** Remaining turns budget for autonomous mode (undefined = unbounded, 0 = exhausted). */
turnsRemaining?: number;
/** Files pinned to the session — prepended to every turn's prompt. */
pinnedFiles?: string[];
/** SPIFFE/WIMSE URI of the primary session agent (falls back to anonymous:session:<id>). */
agentUri?: string;
/** Active sub-agents for the identity chain display. */
subagents?: Subagent[];
/** Cumulative token + cost usage since the session started. */
usage?: SessionUsage;
/**
* Rotation telemetry — how many times the underlying Claude Code session
* has been rolled over to avoid context compaction. Only populated when
* auto-rotation is active or the user has manually rotated.
*/
rotation?: {
count: number;
/** Unix ms of last rotation, or null if never rotated. */
lastRotatedAt: number | null;
/** Backing Claude Code session id (opaque to UI, useful for debugging). */
claudeCodeSessionId?: string;
};
/**
* Number of user messages buffered in the streamInput queue, waiting for
* the SDK consumer to pick them up. > 0 means: user has sent faster than
* Claude can process — useful signal for mid-turn queueing UX.
*/
queuedMessages?: number;
/**
* Resolved full model id currently in use for this session. When unset,
* the SDK / Claude Code default applies. Frontends typically display
* the matching alias + label from the model catalog.
*/
model?: string;
/** Fallback model id used on 429/529 capacity errors. */
fallbackModel?: string;
}
/**
* Cumulative usage totals for a session. Aggregated from each SDK `result`
* message (one per turn). Frontends render this as a "$X · Yk in / Zk out"
* counter so the user sees what they're spending in near-realtime.
*
* Persistent: the daemon records one `TurnUsage` row per turn to SQLite so
* totals survive daemon restarts and can be queried after the fact.
*/
export interface SessionUsage {
/** Input tokens consumed across all turns. */
inputTokens: number;
/** Output tokens generated across all turns. */
outputTokens: number;
/** Tokens read from the prompt cache (cheap). */
cacheReadTokens: number;
/** Tokens written to the prompt cache (a premium on cache-misses). */
cacheCreationTokens: number;
/** Total cost in USD across all turns, as reported by the SDK. */
totalCostUsd: number;
/** Number of turns (round-trips) included in these totals. */
numTurns: number;
/** Wall-clock duration of agent work (sum of per-turn `duration_ms`). */
durationMs: number;
/** Most recent turns (newest first) — lightweight trend signal for UIs. */
recentTurns?: TurnUsage[];
/**
* Max PRIMARY-AGENT context size ever seen on a single turn — bloat canary.
* Computed as max(input + cache_read + cache_creation) across the primary
* agent's per-call usages within each turn (subagent calls excluded). This
* is the size the model actually processed; NOT a cumulative or billable
* figure. Capped at the model's context window on historical fallback.
*/
peakInputTokens?: number;
/**
* Most recent turn's PRIMARY-AGENT context size.
* = input + cache_read + cache_creation on the biggest primary call of the
* turn (subagents excluded). Matches Claude Code's canonical ctx-occupancy
* formula (`calculateContextPercentages`). Used as the numerator for the
* StatusBar's ctx%/window display — NOT billable input.
*/
lastTurnInputTokens?: number;
/** Most recent turn's output tokens. */
lastTurnOutputTokens?: number;
/** Most recent turn's cost (USD). */
lastTurnCostUsd?: number;
/** Most recent turn's cache-read ratio (cache_read / total_input). */
lastTurnCacheHitRate?: number;
/**
* Resolved model's context window in tokens — the denominator for
* ctx-occupancy displays. Derived from `SessionInfo.model` via the
* daemon's per-model catalog (`contextWindowForModel`). Switching
* models mid-session updates this on the next info_update broadcast.
*
* Optional for back-compat with daemons that pre-date this field;
* frontends should fall back to a conservative constant (200k) or
* skip the percentage when unset.
*/
contextWindow?: number;
}
/**
* Per-turn usage record — one row per SDK `result` event.
*
* Kept small + serializable so it fits cleanly in SessionInfo broadcasts.
* `totalInputTokens`, `billableInputTokens` and `cacheHitRate` are derived
* fields we compute once on write rather than re-computing in every
* frontend — keeps the StatusBar render cheap.
*
* Important Anthropic semantics (easy to get wrong):
* - `inputTokens` = NEW (uncached) input tokens only
* - `cacheReadTokens` = tokens served from prompt cache (billed 0.1x)
* - `cacheCreationTokens` = tokens written to cache (billed 1.25x)
* - Actual context size Claude processed = input + cacheRead + cacheCreation
*/
export interface TurnUsage {
/** 1-indexed turn number within the session. */
turnNumber: number;
/** Unix ms when the turn settled. */
createdAt: number;
/** New (uncached) input tokens for this turn. Does NOT include cache tokens. */
inputTokens: number;
/** Output tokens from the assistant. */
outputTokens: number;
/** Cache-read tokens (billed at ~10% of full input). */
cacheReadTokens: number;
/** Cache-write tokens (billed at ~125% of full input). */
cacheCreationTokens: number;
/** Total cost for the turn in USD, as reported by the SDK. */
totalCostUsd: number;
/** Wall-clock duration in ms (agent work, not network). */
durationMs: number;
/** Stop reason ("end_turn", "max_tokens", "tool_use", "error", …) if known. */
stopReason?: string;
/** Derived: total context size = inputTokens + cacheReadTokens + cacheCreationTokens. */
totalInputTokens: number;
/** Derived: full-price input = inputTokens + cacheCreationTokens (cache reads are ~free). */
billableInputTokens: number;
/** Derived: cacheReadTokens / totalInputTokens. 0-1. */
cacheHitRate: number;
/**
* Max single-call context size on the primary agent during this turn —
* `max(input + cache_read + cache_creation)` across the SDK's streamed
* per-call usage. Authoritative for "% of window" because
* `totalInputTokens` SUMS across the multiple internal Messages-API
* calls a tool-using turn makes, overstating single-shot context size.
*
* Optional for back-compat: rows persisted before the daemon began
* tracking this leave it `undefined`. Frontends fall back to
* `min(totalInputTokens, contextWindow)` (legacy behaviour) for those.
*/
primaryMaxCallInputTokens?: number;
}
export interface Subagent {
/** SDK-side agent id (opaque handle). */
agentId: string;
/** ZeroID WIMSE URI if registered, else undefined. */
wimseUri?: string;
/** Subagent type label (e.g. "general-purpose", "code-reviewer", "Explorer"). */
agentType: string;
/** Unix ms when the sub-agent started. */
spawnedAt: number;
/** True while the sub-agent is running; false after SubagentStop. */
active: boolean;
}
// =============================================================================
// Identity — WHO produced a message. On every message, always.
// =============================================================================
export type IdentityType = "human" | "agent" | "subagent" | "system";
export interface MessageIdentity {
/** ZeroID WIMSE URI (e.g. spiffe://zeroid.dev/personal/dev/agent/codeoid-session-abc) */
sub: string;
/** Human-readable display name */
name?: string;
/** What kind of entity produced this */
type: IdentityType;
}
/** System identity — used for daemon-generated messages */
export const SYSTEM_IDENTITY: MessageIdentity = {
sub: "system:codeoid",
name: "Codeoid",
type: "system",
};
// =============================================================================
// Message roles
// =============================================================================
/**
* Every message has a role. Simple frontends render based on role alone.
* Extensible — add new roles without breaking existing frontends.
*/
export type MessageRole =
| "user" // Human sent a prompt
| "assistant" // Agent's text response
| "thinking" // Agent's reasoning / extended thinking
| "tool_call" // Agent invoked a tool
| "tool_result" // Tool execution output
| "system" // Errors, retries, warnings
| "info"; // Informational (identity changes, session events)
// =============================================================================
// Content parts — rich, structured content within a message.
//
// Frontends that support rich rendering use parts[].
// Simple frontends (Telegram) fall back to the `content` string.
// Discriminated on `kind` — ignore unknown kinds gracefully.
// =============================================================================
export type ContentPart =
| TextPart
| CodePart
| FileRefPart
| DiffPart
| TreePart
| ButtonPart
| ProgressPart
| ImagePart
| AnchorPart
| TablePart;
/** Markdown or plain text */
export interface TextPart {
kind: "text";
text: string;
/** If true, text contains markdown. Default: true for assistant role. */
markdown?: boolean;
}
/** Fenced code block with optional language */
export interface CodePart {
kind: "code";
code: string;
language?: string;
/** Optional file path this code belongs to */
filePath?: string;
}
/** Reference to a file (clickable in rich frontends) */
export interface FileRefPart {
kind: "file_ref";
path: string;
/** Optional line range */
lines?: [start: number, end: number];
/** Change summary if this is a modified file */
change?: { added: number; removed: number };
}
/** File diff summary */
export interface DiffPart {
kind: "diff";
path: string;
added: number;
removed: number;
/** Original file URI (for multi-diff views) */
originalPath?: string;
}
/** File tree node */
export interface TreeNode {
label: string;
type: "file" | "directory";
path?: string;
children?: TreeNode[];
}
export interface TreePart {
kind: "tree";
label: string;
children: TreeNode[];
}
/** Clickable button / action */
export interface ButtonPart {
kind: "button";
label: string;
/** Action identifier — frontends handle based on this */
action: string;
/** Additional data for the action */
data?: Record<string, unknown>;
/** Visual style hint */
style?: "primary" | "secondary" | "danger";
}
/** Progress indicator */
export interface ProgressPart {
kind: "progress";
message: string;
/** 0-100 if deterministic, undefined if indeterminate */
percent?: number;
/** Elapsed time in milliseconds */
elapsedMs?: number;
}
/** Inline image */
export interface ImagePart {
kind: "image";
url: string;
alt?: string;
}
/** Hyperlink / anchor */
export interface AnchorPart {
kind: "anchor";
uri: string;
title: string;
}
/** Structured table (for tabular data without markdown) */
export interface TablePart {
kind: "table";
headers: string[];
rows: string[][];
}
// =============================================================================
// Tool invocation state machine
//
// Tool calls are NOT single events — they have a lifecycle.
// Each state transition is sent as a delta update referencing the tool's toolId.
//
// Lifecycle:
// streaming → waiting_confirmation → executing → completed
// → cancelled
//
// Inspired by VS Code's IChatToolInvocation.StateKind.
// =============================================================================
export type ToolPhase =
| "streaming" // LM is still generating the tool call input
| "waiting_confirmation" // Awaiting user approval
| "executing" // Tool is running
| "completed" // Tool finished (success or error)
| "cancelled"; // User denied or interrupted
export type ToolState =
| ToolStreamingState
| ToolWaitingConfirmationState
| ToolExecutingState
| ToolCompletedState
| ToolCancelledState;
export interface ToolStreamingState {
phase: "streaming";
/** Partial input as the LM generates it */
partialInput?: unknown;
}
export interface ToolWaitingConfirmationState {
phase: "waiting_confirmation";
/** Complete tool input */
input: unknown;
/** Human-readable description of what the tool will do */
description: string;
/** Unique ID for this confirmation — client responds with this */
approvalId: string;
}
export interface ToolExecutingState {
phase: "executing";
/** Progress message from the tool */
progress?: string;
/** Elapsed time in milliseconds */
elapsedMs?: number;
}
export interface ToolCompletedState {
phase: "completed";
success: boolean;
/** Tool output (may be truncated for large outputs) */
output?: string;
/** Elapsed time in milliseconds */
elapsedMs?: number;
/** How the tool was confirmed */
confirmedBy?: "user" | "auto" | "setting";
}
export interface ToolCancelledState {
phase: "cancelled";
reason: "denied" | "interrupted" | "timeout";
/** Optional explanation */
message?: string;
}
/** Tool call metadata on a session message */
export interface ToolInfo {
/** Unique ID for this tool invocation — correlate updates via this */
toolId: string;
/** Tool name (e.g. "Bash", "Read", "Edit") */
name: string;
/** Current state */
state: ToolState;
/**
* The original tool input as provided by the model. Lives on
* `ToolInfo` (not just on `WaitingConfirmation`) so it survives
* phase transitions — clients that want to render Edit-as-diff in
* the completed phase need it after approval, and we don't want to
* pay a round-trip to fetch it back.
*/
input?: unknown;
}
// =============================================================================
// Session messages — the core of the protocol.
// =============================================================================
/**
* A complete session message. Self-contained, serializable, auditable.
*
* Every message carries:
* - `role` — what kind of message (user, assistant, tool_call, etc.)
* - `content` — string fallback for simple frontends
* - `parts` — rich content for capable frontends
* - `identity` — who produced this message
* - `tool` — tool lifecycle (only for role=tool_call)
* - `messageId` — unique, for delta updates and cross-references
*/
export interface SessionMessage {
type: "session.message";
sessionId: string;
/** Unique message ID — used by deltas to reference this message */
messageId: string;
role: MessageRole;
/** Plain text content — always present, usable by any frontend */
content: string;
/** Rich content parts — optional, for frontends that support them */
parts?: ContentPart[];
/** Who produced this message */
identity: MessageIdentity;
/** Tool invocation metadata (only when role=tool_call) */
tool?: ToolInfo;
/** Extensible metadata — frontends ignore unknown keys */
metadata?: Record<string, unknown>;
timestamp: string;
}
/**
* Incremental update to an existing message.
*
* For streaming: the assistant's response arrives token by token.
* For tool lifecycle: tool state transitions (executing → completed).
*
* Frontends apply deltas to the message with matching messageId.
* If a frontend doesn't have the message (late attach), it can ignore deltas
* and rely on the scrollback replay to get the complete state.
*/
export interface SessionMessageDelta {
type: "session.message.delta";
sessionId: string;
/** References the original SessionMessage.messageId */
messageId: string;
/** Append to the content string */
contentAppend?: string;
/** Append new content parts */
partsAppend?: ContentPart[];
/** Replace content parts at a specific index */
partsUpdate?: { index: number; part: ContentPart }[];
/** Update tool state (state machine transition) */
toolStateUpdate?: ToolState;
timestamp: string;
}
// =============================================================================
// Client → Daemon messages
// =============================================================================
/** Liveness heartbeat — daemon replies with `response.ok`. */
export interface PingMsg extends BaseClientMsg {
type: "ping";
}
export type ClientMessage =
| PingMsg
| SessionCreateMsg
| SessionListMsg
| SessionAttachMsg
| SessionDetachMsg
| SessionSendMsg
| SessionInterruptMsg
| SessionApproveMsg
| SessionDestroyMsg
| SessionSetModeMsg
| SessionPinMsg
| SessionUnpinMsg
| SessionRotateMsg
| SessionSearchMsg
| SessionSetModelMsg
| SessionRenameMsg
| FsListMsg
| FsReadMsg
| FsBrowseDirMsg
| ClaudeConfigMsg
| ModelsListMsg
| SessionExportMsg
| SessionImportMsg;
interface BaseClientMsg {
/** Request ID for correlating responses */
id: string;
}
export interface SessionCreateMsg extends BaseClientMsg {
type: "session.create";
name: string;
workdir: string;
}
/**
* Rename a session. Daemon updates `SessionInfo.name` in-memory + in the
* transcript store and broadcasts `session.info_update` so every attached
* client refreshes its tab label. The sessionId is stable — callers can
* keep using it. Rejected with `invalid_request` if `name` is empty or
* whitespace-only.
*/
export interface SessionRenameMsg extends BaseClientMsg {
type: "session.rename";
sessionId: string;
name: string;
}
export interface SessionListMsg extends BaseClientMsg {
type: "session.list";
}
export interface SessionAttachMsg extends BaseClientMsg {
type: "session.attach";
sessionId: string;
}
export interface SessionDetachMsg extends BaseClientMsg {
type: "session.detach";
sessionId: string;
}
export interface SessionSendMsg extends BaseClientMsg {
type: "session.send";
sessionId: string;
text: string;
/**
* One-shot attachments for this turn only. Daemon resolves each path
* (relative to the session's workdir), reads and prepends the content
* to the effective prompt. Missing or oversized files are surfaced as
* inline error markers rather than silently dropped.
*/
attachments?: Attachment[];
/**
* Mid-turn priority hint (SDK semantics):
* - `now` — interrupt the agent's current turn and observe immediately
* - `next` — let the current turn finish, then pick this up
* - `later` — queue as a standard follow-up (default)
* Frontends that don't care pass nothing; FIFO stays the default.
*/
priority?: "now" | "next" | "later";
}
export interface Attachment {
/** File path, absolute or relative to the session workdir. */
path: string;
/**
* Optional inlined text content. When provided, daemon skips the file
* read and uses this directly — useful for paste-from-clipboard flows or
* remote editors that push the bytes over the wire.
*/
content?: string;
/**
* MIME type when the attachment carries non-text bytes (images, PDFs).
* Combined with `data`, lets a frontend push binary payloads that the
* daemon writes to a temp file under the session workdir so Claude's
* Read tool can pick them up.
*/
mimeType?: string;
/**
* Base64-encoded bytes. Mutually exclusive with `content` — when set,
* `mimeType` must also be set. The daemon decodes into a temp file and
* rewrites `path` to point at that file before handing it to Claude.
*/
data?: string;
}
export interface SessionInterruptMsg extends BaseClientMsg {
type: "session.interrupt";
sessionId: string;
}
export interface SessionApproveMsg extends BaseClientMsg {
type: "session.approve";
sessionId: string;
/** Correlates to ToolWaitingConfirmationState.approvalId */
approvalId: string;
approved: boolean;
/**
* Optional patch to merge into the original tool input before the SDK
* runs the tool's `call()`. Required for form-style tools like
* `AskUserQuestion` where the user's answers ARE the input the tool
* needs to produce its tool_result. For binary approvals (Bash, Edit,
* etc.) this is omitted and the daemon passes input through unchanged.
*
* Shape is tool-specific. For `AskUserQuestion`:
* `{ answers: { "<question text>": "<answer or comma-joined>" } }`
* The daemon shallow-merges this over the original `input` before
* returning `{ behavior: "allow", updatedInput: ... }` to the SDK.
*/
updatedInput?: Record<string, unknown>;
}
export interface SessionDestroyMsg extends BaseClientMsg {
type: "session.destroy";
sessionId: string;
}
export interface SessionSetModeMsg extends BaseClientMsg {
type: "session.set_mode";
sessionId: string;
mode: SessionMode;
/** Only meaningful for `autonomous`; undefined = unbounded. */
maxTurns?: number;
}
/**
* Manage the session's pinned-files list. Pinned files get prepended to
* every turn until unpinned — useful for keeping a spec document or
* acceptance criteria in Claude's attention across a long task.
*/
export interface SessionPinMsg extends BaseClientMsg {
type: "session.pin";
sessionId: string;
/** File path (absolute or relative to the session workdir). */
path: string;
}
export interface SessionUnpinMsg extends BaseClientMsg {
type: "session.unpin";
sessionId: string;
path: string;
}
/**
* Manually rotate the session's backing Claude Code context. Keeps the
* user-visible session id + scrollback + memory unchanged; starts a fresh
* Claude Code transcript. Rejected if the session has fewer turns than
* the configured min-turns-before-rotate.
*/
export interface SessionRotateMsg extends BaseClientMsg {
type: "session.rotate";
sessionId: string;
}
/**
* Switch the model (and optionally fallback) for a session. Accepts
* aliases (`opus`/`sonnet`/`haiku`) or full Anthropic model ids. Takes
* effect on the next send — the current streamInput loop is torn down so
* the new model is handed to the fresh `query()` invocation. Switching
* invalidates the prompt cache (Anthropic cache is per-model), so the
* first turn on the new model is a full re-cache cost.
*
* Passing `fallbackModel: null` clears any previous fallback; omitting
* the field leaves it unchanged.
*/
export interface SessionSetModelMsg extends BaseClientMsg {
type: "session.set_model";
sessionId: string;
model: string;
fallbackModel?: string | null;
}
/**
* Full-text + semantic search across ALL sessions in a workspace — the
* human-facing counterpart to Claude's `recall()` tool. Searches over
* every user message, Claude reply, tool call, tool result, and reasoning
* block stored in memory. Combines FTS5 BM25 (exact-keyword match) with
* vector similarity (semantic) + recency + session-name boost.
*
* Returns a ranked list of SESSIONS (not a flat list of episodes) with
* evidence snippets so frontends can render previews without a second
* round-trip. Pick a session → re-attach to jump into it.
*/
export interface SessionSearchMsg extends BaseClientMsg {
type: "session.search";
query: string;
/** Scope of the search. Default: `workspace` = sessions in the current workdir's workspace. */
scope?: "workspace" | "all";
/**
* Anchor workspace. When `scope="workspace"` and this is unset, the
* daemon infers from the requesting client's current focus — if the
* client doesn't have one either, it falls back to cross-workspace.
*/
workdir?: string;
/** Max results to return. Default 10. */
limit?: number;
}
/** Per-session hit returned by session.search. */
export interface SessionSearchHit {
sessionId: string;
sessionName: string;
workdir: string;
/** Number of matching episodes within the session. */
matchCount: number;
firstMatchAt: number;
lastMatchAt: number;
/** Aggregate rank score (higher = more relevant). */
aggregateScore: number;
/** Top evidence snippets from the session. */
snippets: SessionSearchSnippet[];
}
/** A single evidence snippet inside a SessionSearchHit. */
export interface SessionSearchSnippet {
episodeId: string;
kind: "user_turn" | "assistant_turn" | "tool_call" | "error";
toolName?: string;
summary: string;
/** Query-centered excerpt (~240 chars). */
excerpt: string;
createdAt: number;
/** Hybrid recall score (0..~1). */
score: number;
filePaths: string[];
}
// =============================================================================
// File system — read-only access scoped to a session's workdir.
//
// Path semantics: `path` is relative to `session.workdir`. Empty string,
// "." or "/" all mean "the workdir root". Daemon canonicalises and rejects
// any path that resolves outside the workdir (symlink escapes blocked).
// =============================================================================
export interface FsListMsg extends BaseClientMsg {
type: "fs.list";
sessionId: string;
/** Path relative to the session's workdir. */
path: string;
}
/**
* Session-less directory browse. Used by the new-session UI to let the
* user pick a workdir without typing a full path. Daemon resolves
* `path` (or HOME when omitted) against a configured root (HOME by
* default), canonicalises, and rejects paths that escape it.
*/
export interface FsBrowseDirMsg extends BaseClientMsg {
type: "fs.browse_dir";
/** Absolute path to browse. Defaults to the daemon user's HOME. */
path?: string;
}
/**
* Request a Claude Code configuration snapshot for the focused
* session — agents, skills, MCP servers, hooks. Read-only.
*/
export interface ClaudeConfigMsg extends BaseClientMsg {
type: "claude.config";
sessionId: string;
}
/**
* Ask the daemon for the model catalog the Claude Code backend actually
* supports (via the SDK's `supportedModels()`), rather than a hardcoded
* list that goes stale. Daemon-wide — no sessionId. Returns the cached
* live list, or a built-in fallback if no session has initialized yet.
*/
export interface ModelsListMsg extends BaseClientMsg {
type: "models.list";
}
/** One selectable model as reported by the Claude Code backend. */
export interface ModelInfo {
/** Value passed to `/model` and forwarded to the SDK (e.g. "opus[1m]"). */
value: string;
/** Human label (e.g. "Opus"). */
displayName: string;
/** Optional one-line description from the backend. */
description?: string;
/** True for the backend's recommended default. */
isDefault?: boolean;
}
/**
* Export a session as a `ShareBundle` JSON. Daemon resolves a workdir
* alias (from git remote when available), rewrites every absolute path
* to `${alias}/${relative}`, and returns either the full bundle inline
* (small sessions) or writes it to `~/.codeoid/exports/` and returns
* the on-disk path.
*/
export interface SessionExportMsg extends BaseClientMsg {
type: "session.export";
sessionId: string;
/** Slice memory episodes for this session into the bundle. Default true. */
includeMemory?: boolean;
/** Snapshot pinned files into the bundle. Default false (size). */
includePinnedFiles?: boolean;
/** Override the auto-resolved alias. Useful for non-git workdirs. */
aliasOverride?: string;
/** Force on-disk file output regardless of size. Default: file when > 5 MB. */
toFile?: boolean;
}
/**
* Import a session bundle into a fresh session id. The importer's
* `targetWorkdir` is the local path the bundle anchors to —
* `${alias}/...` paths get rewritten to this workdir on the way in.
*/
export interface SessionImportMsg extends BaseClientMsg {
type: "session.import";
/** Either inline bundle JSON (object) OR a path to a previously-saved file. */
source: { kind: "inline"; bundle: unknown } | { kind: "file"; path: string };
targetWorkdir: string;
/** Override the imported session's name. Default = original name. */
nameOverride?: string;
/** Materialise pinnedFiles into targetWorkdir. Default false. */
writePinnedFiles?: boolean;
}
export type ClaudeConfigScope = "global" | "workdir";
export interface ClaudeConfigAgent {
name: string;
description: string | null;
/** Absolute path to the source `*.md` file. */
path: string;
scope: ClaudeConfigScope;
/** Comma-separated tools list parsed from frontmatter, when set. */
tools?: string[];
}
export interface ClaudeConfigSkill {
name: string;
description: string | null;
path: string;
scope: ClaudeConfigScope;
}
export interface ClaudeConfigMcpServer {
name: string;
scope: ClaudeConfigScope;
/** Absolute path to the source file that declared it
* (`~/.claude.json`, `settings.json`, `.mcp.json`, …). */
path: string;
/** stdio command, when present. */
command: string | null;
args: string[];
/** Just the keys of the `env` block (values are secrets, never returned). */
envKeys: string[];
/** http URL for non-stdio servers. */
url: string | null;
/** Optional `type` field (e.g. "http"). */
type: string | null;
/** Just the keys of the `headers` block for http-type servers
* (values are bearer tokens / API keys, never returned). */
headerKeys?: string[];
/**
* Live connection status the SDK reported for this server name on the
* most recent `system/init` event for this session. `undefined` when the
* SDK hasn't started a turn yet (drawer opened before first send) or the
* server name didn't match any SDK-reported entry. The SDK uses values
* like `"connected"`, `"failed"`, `"pending"` — we surface the string
* verbatim so we don't lock in an enum.
*/
liveStatus?: string;
/**
* MCP tool names the SDK exposed in this session for this server,
* fully-qualified (`mcp__<server>__<tool>`). Empty array means the server
* connected but exposed no tools; `undefined` means we have no SDK-side
* data yet.
*/
liveTools?: string[];
}
export interface ClaudeConfigHook {
/** Hook event name (e.g. "PreToolUse", "PostToolUse"). */
event: string;
scope: ClaudeConfigScope;
/** Absolute path to the source `settings.json`. */
path: string;
/** Tool-name matcher pattern, when present. */
matcher: string | null;
/** Hook kind ("command" today; future-proof). */
kind: string;
/** The shell command to run. */
command: string;
}
export interface ClaudeConfigSnapshot {
agents: ClaudeConfigAgent[];
skills: ClaudeConfigSkill[];
mcpServers: ClaudeConfigMcpServer[];
hooks: ClaudeConfigHook[];
}
export interface FsReadMsg extends BaseClientMsg {
type: "fs.read";
sessionId: string;
path: string;
/** Hard cap in bytes; daemon also enforces an absolute ceiling. Default 1 MiB. */
maxBytes?: number;
}
export interface FsEntry {
/** Just the file or directory name (no path). */
name: string;
/** Path relative to the session's workdir. */
path: string;
kind: "file" | "directory";
/** Bytes for files; undefined for directories. */
size?: number;
/** Modified time as Unix ms. */
mtimeMs?: number;
/** True when this entry is a symlink (kind reflects the resolved target). */
isSymlink?: boolean;
}
export interface FsListResultMsg {
type: "fs.list.result";
requestId: string;
/** Echoed `path` (canonicalised, still relative to workdir). */
path: string;
entries: FsEntry[];
}
export interface FsReadResultMsg {
type: "fs.read.result";
requestId: string;
path: string;
/** UTF-8 text. Binary files come back base64-encoded with `encoding: "base64"`. */
content: string;
encoding: "utf-8" | "base64";
/** Total size on disk in bytes (may be larger than content if `truncated`). */