running-process 4.5.2

Subprocess and PTY runtime for the running-process project
Documentation
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
// Proto3 schema for running-process daemon IPC.
//
// Convention: RequestType enum values are chosen so that each value matches
// the field number of the corresponding payload message inside DaemonRequest
// and DaemonResponse.  For example, REGISTER = 10 corresponds to
// `RegisterRequest register = 10;` in DaemonRequest and
// `RegisterResponse register = 10;` in DaemonResponse.

syntax = "proto3";
package running_process.daemon.v1;

// ---------------------------------------------------------------------------
// Enums
// ---------------------------------------------------------------------------

enum RequestType {
  REQUEST_TYPE_UNSPECIFIED     = 0;
  REQUEST_TYPE_REGISTER        = 10;
  REQUEST_TYPE_UNREGISTER      = 11;
  REQUEST_TYPE_SPAWN_DAEMON    = 12;
  REQUEST_TYPE_LIST_ACTIVE     = 20;
  REQUEST_TYPE_LIST_BY_ORIGINATOR = 21;
  REQUEST_TYPE_GET_PROCESS_TREE = 22;
  REQUEST_TYPE_KILL_ZOMBIES    = 30;
  REQUEST_TYPE_KILL_TREE       = 31;
  REQUEST_TYPE_PING            = 40;
  REQUEST_TYPE_SHUTDOWN        = 41;
  REQUEST_TYPE_STATUS          = 42;
  // Service supervision (runpm) — Phase 1 ships stubs that respond OK.
  REQUEST_TYPE_SERVICE_START     = 50;
  REQUEST_TYPE_SERVICE_STOP      = 51;
  REQUEST_TYPE_SERVICE_RESTART   = 52;
  REQUEST_TYPE_SERVICE_DELETE    = 53;
  REQUEST_TYPE_SERVICE_LIST      = 54;
  REQUEST_TYPE_SERVICE_DESCRIBE  = 55;
  REQUEST_TYPE_SERVICE_LOGS      = 56;
  REQUEST_TYPE_SERVICE_FLUSH     = 57;
  REQUEST_TYPE_SERVICE_SAVE      = 58;
  REQUEST_TYPE_SERVICE_RESURRECT = 59;
  // Detachable PTY sessions (issue #130 milestone 2).
  REQUEST_TYPE_SPAWN_PTY_SESSION     = 60;
  REQUEST_TYPE_ATTACH_PTY_SESSION    = 61;
  REQUEST_TYPE_DETACH_PTY_SESSION    = 62;
  REQUEST_TYPE_LIST_PTY_SESSIONS     = 63;
  REQUEST_TYPE_TERMINATE_PTY_SESSION = 64;
  // Detachable pipe-backed sessions (issue #130 milestone 3).
  // Pipe sessions own three independently-attachable streams (stdin is
  // write-only, exposed as WRITE_PIPE_STDIN; stdout and stderr are
  // streaming attaches via ATTACH_PIPE_STREAM with a stream selector).
  REQUEST_TYPE_SPAWN_PIPE_SESSION     = 65;
  REQUEST_TYPE_ATTACH_PIPE_STREAM     = 66;
  REQUEST_TYPE_DETACH_PIPE_STREAM     = 67;
  REQUEST_TYPE_LIST_PIPE_SESSIONS     = 68;
  REQUEST_TYPE_TERMINATE_PIPE_SESSION = 69;
  REQUEST_TYPE_WRITE_PIPE_STDIN       = 70;
  // Session backlog snapshot (#130 milestone 7 B4). Returns the current
  // ring-buffer contents for any daemon-owned session (PTY or pipe)
  // without consuming them — same buffer continues to fill, and a real
  // attach later still sees the same backlog.
  REQUEST_TYPE_GET_SESSION_BACKLOG    = 71;
  // Bulk session ops (#130 milestone 9 H4).
  REQUEST_TYPE_PURGE_EXITED_SESSIONS  = 72;
  REQUEST_TYPE_BULK_TERMINATE_SESSIONS = 73;
  // Resize a PTY session without attaching (#130 M5 follow-up).
  REQUEST_TYPE_RESIZE_PTY_SESSION     = 74;
  // Optional daemon-owned tee telemetry (#131).
  REQUEST_TYPE_REGISTER_SESSION_TEE   = 75;
  REQUEST_TYPE_UNREGISTER_SESSION_TEE = 76;
  REQUEST_TYPE_GET_SESSION_TEE_STATUS = 77;
  // Optional daemon-owned observer subscriptions (#221 Phase 2 / #429).
  REQUEST_TYPE_REGISTER_SESSION_OBSERVER   = 78;
  REQUEST_TYPE_UNREGISTER_SESSION_OBSERVER = 79;
  REQUEST_TYPE_GET_SESSION_OBSERVER_STATUS = 80;
}

enum StatusCode {
  STATUS_CODE_OK               = 0;
  STATUS_CODE_ERROR            = 1;
  STATUS_CODE_UNKNOWN_REQUEST  = 2;
  STATUS_CODE_INVALID_ARGUMENT = 3;
  STATUS_CODE_NOT_FOUND        = 4;
  STATUS_CODE_INTERNAL         = 5;
  STATUS_CODE_UNAVAILABLE      = 6;
  STATUS_CODE_VERSION_MISMATCH = 7;
  STATUS_CODE_RATE_LIMITED     = 8;
  STATUS_CODE_UNIMPLEMENTED    = 9;
  STATUS_CODE_ALREADY_ATTACHED = 10;
}

enum ProcessState {
  PROCESS_STATE_UNKNOWN = 0;
  PROCESS_STATE_ALIVE   = 1;
  PROCESS_STATE_DEAD    = 2;
  PROCESS_STATE_ZOMBIE  = 3;
}

enum GraphicsProtocol {
  GRAPHICS_PROTOCOL_UNSPECIFIED = 0;
  GRAPHICS_PROTOCOL_SIXEL       = 1;
  GRAPHICS_PROTOCOL_KITTY       = 2;
  GRAPHICS_PROTOCOL_ITERM2_FILE = 3;
}

enum CapabilityStatus {
  CAPABILITY_STATUS_UNSPECIFIED = 0;
  CAPABILITY_STATUS_SUPPORTED   = 1;
  CAPABILITY_STATUS_UNSUPPORTED = 2;
  CAPABILITY_STATUS_UNKNOWN     = 3;
  CAPABILITY_STATUS_BLOCKED     = 4;
}

enum EvidenceStrength {
  EVIDENCE_STRENGTH_UNSPECIFIED        = 0;
  EVIDENCE_STRENGTH_PROBE              = 1;
  EVIDENCE_STRENGTH_STRONG_HOST_SIGNAL = 2;
  EVIDENCE_STRENGTH_TERMINFO           = 3;
  EVIDENCE_STRENGTH_WEAK_ENV           = 4;
  EVIDENCE_STRENGTH_USER_OVERRIDE      = 5;
}

// ---------------------------------------------------------------------------
// Envelope messages
// ---------------------------------------------------------------------------

message DaemonRequest {
  uint64      id               = 1;
  RequestType type             = 2;
  uint32      protocol_version = 3;
  string      client_name      = 4;

  // Payload fields — field numbers match RequestType enum values.
  RegisterRequest         register          = 10;
  UnregisterRequest       unregister        = 11;
  SpawnDaemonRequest      spawn_daemon      = 12;
  ListActiveRequest       list_active       = 20;
  ListByOriginatorRequest list_by_originator = 21;
  GetProcessTreeRequest   get_process_tree  = 22;
  KillZombiesRequest      kill_zombies      = 30;
  KillTreeRequest         kill_tree         = 31;
  PingRequest             ping              = 40;
  ShutdownRequest         shutdown          = 41;
  StatusRequest           status            = 42;
  ServiceStartRequest     service_start     = 50;
  ServiceStopRequest      service_stop      = 51;
  ServiceRestartRequest   service_restart   = 52;
  ServiceDeleteRequest    service_delete    = 53;
  ServiceListRequest      service_list      = 54;
  ServiceDescribeRequest  service_describe  = 55;
  ServiceLogsRequest      service_logs      = 56;
  ServiceFlushRequest     service_flush     = 57;
  ServiceSaveRequest      service_save      = 58;
  ServiceResurrectRequest service_resurrect = 59;
  SpawnPtySessionRequest     spawn_pty_session     = 60;
  AttachPtySessionRequest    attach_pty_session    = 61;
  DetachPtySessionRequest    detach_pty_session    = 62;
  ListPtySessionsRequest     list_pty_sessions     = 63;
  TerminatePtySessionRequest terminate_pty_session = 64;
  SpawnPipeSessionRequest     spawn_pipe_session     = 65;
  AttachPipeStreamRequest     attach_pipe_stream     = 66;
  DetachPipeStreamRequest     detach_pipe_stream     = 67;
  ListPipeSessionsRequest     list_pipe_sessions     = 68;
  TerminatePipeSessionRequest terminate_pipe_session = 69;
  WritePipeStdinRequest       write_pipe_stdin       = 70;
  GetSessionBacklogRequest    get_session_backlog    = 71;
  PurgeExitedSessionsRequest  purge_exited_sessions  = 72;
  BulkTerminateSessionsRequest bulk_terminate_sessions = 73;
  ResizePtySessionRequest     resize_pty_session     = 74;
  RegisterSessionTeeRequest   register_session_tee   = 75;
  UnregisterSessionTeeRequest unregister_session_tee = 76;
  GetSessionTeeStatusRequest  get_session_tee_status = 77;
  RegisterSessionObserverRequest   register_session_observer   = 78;
  UnregisterSessionObserverRequest unregister_session_observer = 79;
  GetSessionObserverStatusRequest  get_session_observer_status = 80;
}

message DaemonResponse {
  uint64     request_id = 1;
  StatusCode code       = 2;
  string     message    = 3;

  // Payload fields — field numbers match RequestType enum values.
  RegisterResponse         register          = 10;
  UnregisterResponse       unregister        = 11;
  SpawnDaemonResponse      spawn_daemon      = 12;
  ListActiveResponse       list_active       = 20;
  ListByOriginatorResponse list_by_originator = 21;
  GetProcessTreeResponse   get_process_tree  = 22;
  KillZombiesResponse      kill_zombies      = 30;
  KillTreeResponse         kill_tree         = 31;
  PingResponse             ping              = 40;
  ShutdownResponse         shutdown          = 41;
  StatusResponse           status            = 42;
  ServiceStartResponse     service_start     = 50;
  ServiceStopResponse      service_stop      = 51;
  ServiceRestartResponse   service_restart   = 52;
  ServiceDeleteResponse    service_delete    = 53;
  ServiceListResponse      service_list      = 54;
  ServiceDescribeResponse  service_describe  = 55;
  ServiceLogsResponse      service_logs      = 56;
  ServiceFlushResponse     service_flush     = 57;
  ServiceSaveResponse      service_save      = 58;
  ServiceResurrectResponse service_resurrect = 59;
  SpawnPtySessionResponse     spawn_pty_session     = 60;
  AttachPtySessionResponse    attach_pty_session    = 61;
  DetachPtySessionResponse    detach_pty_session    = 62;
  ListPtySessionsResponse     list_pty_sessions     = 63;
  TerminatePtySessionResponse terminate_pty_session = 64;
  SpawnPipeSessionResponse     spawn_pipe_session     = 65;
  AttachPipeStreamResponse     attach_pipe_stream     = 66;
  DetachPipeStreamResponse     detach_pipe_stream     = 67;
  ListPipeSessionsResponse     list_pipe_sessions     = 68;
  TerminatePipeSessionResponse terminate_pipe_session = 69;
  WritePipeStdinResponse       write_pipe_stdin       = 70;
  GetSessionBacklogResponse    get_session_backlog    = 71;
  PurgeExitedSessionsResponse  purge_exited_sessions  = 72;
  BulkTerminateSessionsResponse bulk_terminate_sessions = 73;
  ResizePtySessionResponse     resize_pty_session     = 74;
  RegisterSessionTeeResponse   register_session_tee   = 75;
  UnregisterSessionTeeResponse unregister_session_tee = 76;
  GetSessionTeeStatusResponse  get_session_tee_status = 77;
  RegisterSessionObserverResponse   register_session_observer   = 78;
  UnregisterSessionObserverResponse unregister_session_observer = 79;
  GetSessionObserverStatusResponse  get_session_observer_status = 80;
}

// ---------------------------------------------------------------------------
// Payload messages
// ---------------------------------------------------------------------------

// Ordered key/value pair, used by env-carrying requests where the protobuf
// `map` type's unordered semantics would race case-insensitive dedup on
// Windows (see SpawnDaemonRequest.env).
message KeyValue {
  string key   = 1;
  string value = 2;
}

message TerminalGraphicsCapability {
  GraphicsProtocol protocol = 1;
  CapabilityStatus status   = 2;
  EvidenceStrength evidence = 3;
  string source             = 4;
  repeated string risks     = 5;
}

message TerminalGraphicsCapabilities {
  repeated TerminalGraphicsCapability protocols = 1;
  GraphicsProtocol preferred = 2;
}

message RegisterRequest {
  uint32 pid        = 1;
  double created_at = 2;
  string kind       = 3;
  string command    = 4;
  string cwd        = 5;
  string originator = 6;
  string containment = 7;
}

message RegisterResponse {}

message UnregisterRequest {
  uint32 pid = 1;
}

message UnregisterResponse {}

message SpawnDaemonRequest {
  string command = 1;
  string cwd = 2;
  // Environment variables to apply to the spawned subprocess. Default
  // behaviour (clear_inherited_env=false) is to LAYER these on top of
  // the daemon's own inherited env — the subprocess sees
  // the daemon environment plus these entries, with these entries winning ties.
  //
  // When clear_inherited_env=true, the subprocess sees ONLY these
  // entries, nothing inherited from the daemon. This mirrors Python's
  // `subprocess.Popen(env=…)` semantic: `env=None` (this field empty +
  // clear_inherited_env=false) inherits, `env=<dict>` (with
  // clear_inherited_env=true) replaces.
  //
  // Ordering: this is `repeated KeyValue`, not a `map`, because the
  // daemon needs to dedup case-insensitively on Windows where Rust's
  // `Command::env` collapses "PATH" and "Path" into one slot. The LAST
  // entry per case-folded key wins. Using a `map` here would lose the
  // ordering protobuf provides and race the dedup against HashMap
  // iteration order.
  //
  // Practical note for Windows callers: `cmd.exe` needs SystemRoot in
  // its env to load DLLs; when using replace mode on Windows you'll
  // typically want to copy SystemRoot (and any other essentials) into
  // this list yourself.
  repeated KeyValue env = 3;
  string originator = 4;
  bool clear_inherited_env = 5;
}

message SpawnDaemonResponse {
  uint32 pid = 1;
  double created_at = 2;
  string command = 3;
  string cwd = 4;
  string originator = 5;
  string containment = 6;
}

message ListActiveRequest {}

message ListActiveResponse {
  repeated TrackedProcess processes = 1;
}

message ListByOriginatorRequest {
  string tool = 1;
}

message ListByOriginatorResponse {
  repeated TrackedProcess processes = 1;
}

message GetProcessTreeRequest {
  uint32 pid = 1;
}

message GetProcessTreeResponse {
  string tree_display = 1;
}

message KillZombiesRequest {
  bool dry_run = 1;
}

message KillZombiesResponse {
  repeated ZombieReport zombies = 1;
}

message ZombieReport {
  uint32 pid     = 1;
  string command = 2;
  string reason  = 3;
  bool   killed  = 4;
}

message KillTreeRequest {
  uint32 pid             = 1;
  double timeout_seconds = 2;
}

message KillTreeResponse {
  uint32 processes_killed = 1;
}

message PingRequest {}

message PingResponse {
  int64 server_time_ms = 1;
}

message ShutdownRequest {
  bool   graceful        = 1;
  double timeout_seconds = 2;
}

message ShutdownResponse {}

message StatusRequest {}

message StatusResponse {
  string version                = 1;
  uint64 uptime_seconds         = 2;
  uint32 tracked_process_count  = 3;
  uint32 active_connections     = 4;
  string socket_path            = 5;
  string db_path                = 6;
  string scope                  = 7;
  string scope_hash             = 8;
  string scope_cwd              = 9;
  // ENOSPC emergency-reserve lifecycle state (#390): "armed",
  // "released" (deleted to recover from a full disk), or "degraded"
  // (pre-allocation failed at startup). Additive field; older clients
  // ignore it.
  string emergency_reserve      = 10;
}

message TrackedProcess {
  uint32       pid              = 1;
  double       created_at       = 2;
  string       kind             = 3;
  string       command          = 4;
  string       cwd              = 5;
  string       originator       = 6;
  string       containment      = 7;
  double       registered_at    = 8;
  double       uptime_seconds   = 9;
  bool         parent_alive     = 10;
  ProcessState state            = 11;
  double       last_validated_at = 12;
}

// ---------------------------------------------------------------------------
// Service supervision (runpm) — Phase 1
//
// All handlers in Phase 1 return STATUS_CODE_OK with empty/default payloads.
// Real lifecycle, restart policy, and persistence land in Phase 2.
// ---------------------------------------------------------------------------

message ServiceConfig {
  string              name           = 1;
  repeated string     cmd            = 2;
  string              cwd            = 3;
  map<string, string> env            = 4;
  bool                autorestart    = 5;
  uint32              max_restarts   = 6;
  uint32              restart_delay_ms = 7;
  uint32              kill_timeout_ms  = 8;
  uint32              min_uptime_ms    = 9;
}

message ServiceState {
  string         name             = 1;
  uint32         id               = 2;
  string         status           = 3;  // "online" | "stopped" | "errored" | "starting"
  uint32         pid              = 4;
  uint32         restart_count    = 5;
  double         last_started_at  = 6;
  double         last_exited_at   = 7;
  int32          last_exit_code   = 8;
  ServiceConfig  config           = 9;
}

message ServiceStartRequest {
  ServiceConfig config = 1;
}
message ServiceStartResponse {
  ServiceState service = 1;
}

message ServiceStopRequest {
  string target = 1;  // name | id | "all"
}
message ServiceStopResponse {
  uint32 stopped_count = 1;
}

message ServiceRestartRequest {
  string target = 1;
}
message ServiceRestartResponse {
  uint32 restarted_count = 1;
}

message ServiceDeleteRequest {
  string target = 1;
}
message ServiceDeleteResponse {
  uint32 deleted_count = 1;
}

message ServiceListRequest {}
message ServiceListResponse {
  repeated ServiceState services = 1;
}

message ServiceDescribeRequest {
  string target = 1;
}
message ServiceDescribeResponse {
  ServiceState service = 1;
}

message ServiceLogsRequest {
  string target = 1;  // name | id | "" for all
  uint32 lines  = 2;
  bool   follow = 3;
}
message ServiceLogsResponse {
  string log_text = 1;
}

message ServiceFlushRequest {
  string target = 1;
}
message ServiceFlushResponse {
  uint32 flushed_count = 1;
}

message ServiceSaveRequest {}
message ServiceSaveResponse {
  string snapshot_path = 1;
  uint32 service_count = 2;
}

message ServiceResurrectRequest {}
message ServiceResurrectResponse {
  uint32 restored_count = 1;
}

// ---------------------------------------------------------------------------
// Detachable PTY sessions (issue #130) — milestone 2 scaffold.
//
// Wire protocol for AttachPtySession is intentionally not finalized in this
// scaffold. The default plan is to multiplex PtyStreamFrame/PtyInputFrame
// over the same socket after the AttachPtySessionResponse is sent; the
// alternative is a per-attachment socket path returned in the response.
// The choice will be made in the milestone 2 implementation commit; for
// now the handler responds with STATUS_CODE_UNIMPLEMENTED.
//
// Design decision (see plan docs): the daemon owns the ConPTY / PTY
// master and the child process. Clients never receive raw OS handles —
// they receive bytes over IPC and submit bytes/resize/interrupt via RPC.
// This sidesteps DuplicateHandle and POSIX controlling-terminal handoff.
// ---------------------------------------------------------------------------

message SpawnPtySessionRequest {
  // Argv. argv[0] is the program; remaining are arguments.
  repeated string argv = 1;
  string cwd = 2;
  // Environment for the spawned child. Same layering semantics as
  // SpawnDaemonRequest.env: ordered repeated KeyValue with case-insensitive
  // last-wins dedup on Windows.
  repeated KeyValue env = 3;
  bool clear_inherited_env = 4;
  // Initial terminal size. 0 = use daemon defaults (24x80).
  uint32 rows = 5;
  uint32 cols = 6;
  // Originator tag, e.g. "clud". Stored in registry for discovery.
  string originator = 7;
}

message SpawnPtySessionResponse {
  // UUID v4 string. Used as the handle for subsequent Attach/Detach/Terminate.
  string session_id = 1;
  uint32 pid        = 2;
  double created_at = 3;
}

message AttachPtySessionRequest {
  string session_id = 1;
  // Client-side terminal dimensions; daemon resizes the PTY to match.
  uint32 rows = 2;
  uint32 cols = 3;
  // If true, evict any currently-attached client. Default false: a second
  // attach attempt fails with STATUS_CODE_ALREADY_ATTACHED.
  bool steal = 4;
  // Terminal capabilities for renegotiation; informational only for now.
  string term            = 5;
  bool   is_tty          = 6;
  TerminalGraphicsCapabilities graphics_capabilities = 7;
}

message AttachPtySessionResponse {
  // If the wire protocol ends up using a per-attachment socket, this is
  // the path / pipe name to connect to. Empty string means "multiplex on
  // the existing socket" (default plan).
  string stream_endpoint = 1;
  // Bytes the client missed while detached; included as a one-shot
  // backlog before the live stream starts. Bounded by daemon config.
  bytes  backlog         = 2;
  // True if the backlog ring buffer dropped bytes before this attach.
  // Client should render a "missed N bytes" indicator.
  bool   backlog_truncated = 3;
  uint64 bytes_missed     = 4;
}

message DetachPtySessionRequest {
  string session_id = 1;
}
message DetachPtySessionResponse {}

message ListPtySessionsRequest {
  // Optional originator filter; empty string returns all sessions in scope.
  string originator = 1;
}

message ListPtySessionsResponse {
  repeated PtySessionInfo sessions = 1;
}

message PtySessionInfo {
  string session_id = 1;
  uint32 pid        = 2;
  string command    = 3;
  string cwd        = 4;
  string originator = 5;
  double created_at = 6;
  bool   attached   = 7;
  // Populated only after the child has exited.
  bool   exited     = 8;
  int32  exit_code  = 9;
  double exited_at  = 10;
  uint32 rows       = 11;
  uint32 cols       = 12;
  // Which termination path the session took. UNSPECIFIED while the
  // session is live or for pre-#130 callers.
  TerminationOutcome termination_outcome = 13;
  // Whether the current attached client identified itself as a TTY.
  // Meaningful only when `attached=true`; set to the value of
  // AttachPtySessionRequest.is_tty at attach time. The daemon uses
  // this to skip resize/cursor-query side effects for non-TTY
  // clients (e.g. stream-JSON renderers). #130 M6 C9.
  bool   attached_is_tty = 14;
  // TERM string supplied by the attached client at attach time.
  // Informational; the daemon does not propagate this to the running
  // child's environment (you cannot change a live child's TERM).
  // Empty when no client is attached.
  string attached_term = 15;
  // Graphics capability metadata supplied by the currently attached
  // client. Missing metadata from older clients is treated as unknown.
  TerminalGraphicsCapabilities attached_graphics_capabilities = 16;
}

message TerminatePtySessionRequest {
  string session_id = 1;
  // Soft-then-hard schedule. Daemon sends SIGTERM/CTRL_BREAK_EVENT,
  // waits up to grace_ms, then escalates to SIGKILL / Job-Object kill.
  // Fire-and-forget from the client's perspective: response returns as
  // soon as the schedule is accepted, not when the child has exited.
  uint32 grace_ms = 2;
}

message TerminatePtySessionResponse {}

// Frame envelopes for the bidirectional attach stream. Not part of the
// request/response envelope above; sent after attach succeeds.
//
// Daemon -> client.
message PtyStreamFrame {
  oneof frame {
    // Raw bytes from the PTY master (output).
    bytes  output       = 1;
    // Child exited; final exit code. Always followed by stream close.
    int32  exit_code    = 2;
    // Daemon dropped this many bytes from the head of the ring buffer
    // before this point. Client should render a "missed N bytes" marker.
    uint64 missed_bytes = 3;
    // Error during streaming; daemon will close the stream after sending.
    string error        = 4;
    // Sent when the attached client was evicted via a steal from a peer.
    string stolen_by    = 5;
  }
}

// Client -> daemon.
message PtyInputFrame {
  oneof frame {
    // Raw bytes to write to the PTY master (stdin).
    bytes  input     = 1;
    // Terminal resize; daemon applies via portable-pty resize.
    PtyResize resize = 2;
    // Send SIGINT / CTRL_C_EVENT to the child process group.
    bool   interrupt = 3;
    // Clean detach. Daemon keeps the session alive but releases this
    // attachment.
    bool   detach    = 4;
  }
}

message PtyResize {
  uint32 rows = 1;
  uint32 cols = 2;
}

// ---------------------------------------------------------------------------
// Detachable pipe-backed sessions (issue #130 milestone 3).
//
// A pipe session is the daemon-owned analog of `subprocess.Popen` with
// stdin/stdout/stderr each independently attachable. Stdin is write-only
// (no streaming attach — clients call WritePipeStdin). Stdout and stderr
// use AttachPipeStream with a `stream` selector and receive PipeStreamFrame
// payloads on the same socket after the response.
//
// Design parity with PTY sessions (M2): daemon owns the child process and
// pipe handles; clients never receive raw OS handles. Ring buffer per
// stream with drop-oldest semantics. Single attachment per stream;
// `steal=true` evicts the existing attachment.
// ---------------------------------------------------------------------------

enum PipeStreamKind {
  PIPE_STREAM_KIND_UNSPECIFIED = 0;
  PIPE_STREAM_KIND_STDOUT      = 1;
  PIPE_STREAM_KIND_STDERR      = 2;
}

// How a session reached its terminal state. Reported on
// PtySessionInfo/PipeSessionInfo/GetSessionBacklogResponse once
// `exited=true`. Useful for understanding whether the soft signal
// worked or whether the daemon had to escalate to a hard kill.
enum TerminationOutcome {
  // Default for live sessions and pre-#130 callers.
  TERMINATION_OUTCOME_UNSPECIFIED = 0;
  // The child exited on its own. No terminate RPC fired before exit.
  TERMINATION_OUTCOME_NATURAL_EXIT = 1;
  // Terminate RPC fired; child exited within the grace window before
  // the daemon issued the hard kill. The soft signal worked.
  TERMINATION_OUTCOME_SOFT_EXIT    = 2;
  // Terminate RPC fired; child did not exit within the grace window;
  // the daemon's hard kill ran.
  TERMINATION_OUTCOME_HARD_KILLED  = 3;
}

message SpawnPipeSessionRequest {
  repeated string argv = 1;
  string cwd = 2;
  repeated KeyValue env = 3;
  bool clear_inherited_env = 4;
  string originator = 5;
  // Capture stderr separately (default) or merge into stdout.
  bool merge_stderr_into_stdout = 6;
}

message SpawnPipeSessionResponse {
  string session_id = 1;
  uint32 pid        = 2;
  double created_at = 3;
}

message AttachPipeStreamRequest {
  string         session_id = 1;
  PipeStreamKind stream     = 2;
  // If true, evict any currently-attached client on this stream.
  bool steal = 3;
}

message AttachPipeStreamResponse {
  // Initial backlog the client missed before this attach. Same semantics
  // as AttachPtySessionResponse: empty if nothing has been emitted yet.
  bytes  backlog          = 1;
  bool   backlog_truncated = 2;
  uint64 bytes_missed      = 3;
}

message DetachPipeStreamRequest {
  string         session_id = 1;
  PipeStreamKind stream     = 2;
}
message DetachPipeStreamResponse {}

message ListPipeSessionsRequest {
  string originator = 1;
}

message ListPipeSessionsResponse {
  repeated PipeSessionInfo sessions = 1;
}

message PipeSessionInfo {
  string session_id = 1;
  uint32 pid        = 2;
  string command    = 3;
  string cwd        = 4;
  string originator = 5;
  double created_at = 6;
  bool   stdout_attached = 7;
  bool   stderr_attached = 8;
  bool   exited     = 9;
  int32  exit_code  = 10;
  double exited_at  = 11;
  bool   merge_stderr_into_stdout = 12;
  TerminationOutcome termination_outcome = 13;
}

message TerminatePipeSessionRequest {
  string session_id = 1;
  uint32 grace_ms   = 2;
}
message TerminatePipeSessionResponse {}

message WritePipeStdinRequest {
  string session_id = 1;
  bytes  data       = 2;
  // If true, close stdin after writing the supplied bytes. Subsequent
  // writes to the same session return STATUS_CODE_INVALID_ARGUMENT.
  bool   close      = 3;
}
message WritePipeStdinResponse {
  uint64 bytes_written = 1;
}

message GetSessionBacklogRequest {
  string session_id = 1;
  // Pipe sessions only: which stream's backlog to return. Ignored for
  // PTY sessions (PTY has a single merged output buffer).
  PipeStreamKind pipe_stream = 2;
}

message GetSessionBacklogResponse {
  bytes  backlog          = 1;
  uint64 bytes_missed     = 2;
  // "pty" or "pipe", or empty if the session was not found.
  string session_kind     = 3;
  bool   exited           = 4;
  int32  exit_code        = 5;
  double exited_at        = 6;
  TerminationOutcome termination_outcome = 7;
}

message PurgeExitedSessionsRequest {
  // Filter by originator. Empty matches all.
  string originator = 1;
}
message PurgeExitedSessionsResponse {
  uint32 pty_purged  = 1;
  uint32 pipe_purged = 2;
}

message BulkTerminateSessionsRequest {
  // Terminate every session whose age is strictly greater than this
  // many seconds. Zero terminates everything in scope.
  uint64 older_than_secs = 1;
  // Filter by originator. Empty matches all.
  string originator      = 2;
  // Soft-signal grace window before hard kill.
  uint32 grace_ms        = 3;
}
message BulkTerminateSessionsResponse {
  uint32 pty_terminated  = 1;
  uint32 pipe_terminated = 2;
}

// Resize a PTY session without going through an attach. Useful for
// scripts that adjust the PTY dimensions between attaches, or for an
// orchestrator that wants to set the size once before any client
// connects. No-op on pipe sessions (no PTY to resize).
message ResizePtySessionRequest {
  string session_id = 1;
  uint32 rows       = 2;
  uint32 cols       = 3;
}
message ResizePtySessionResponse {}

// ---------------------------------------------------------------------------
// Optional session tee telemetry (#131).
// ---------------------------------------------------------------------------

enum TeeSessionKind {
  TEE_SESSION_KIND_UNSPECIFIED = 0;
  TEE_SESSION_KIND_PTY         = 1;
  TEE_SESSION_KIND_PIPE        = 2;
}

enum TeeStreamKind {
  TEE_STREAM_KIND_UNSPECIFIED = 0;
  TEE_STREAM_KIND_PTY_OUTPUT  = 1;
  TEE_STREAM_KIND_STDOUT      = 2;
  TEE_STREAM_KIND_STDERR      = 3;
  TEE_STREAM_KIND_STDIN       = 4;
}

enum TeeSinkKind {
  TEE_SINK_KIND_UNSPECIFIED = 0;
  // Daemon opens the path and owns the file descriptor until the tee is
  // removed or the session ends. Path bytes are OS-native, not UTF-8.
  TEE_SINK_KIND_FILE        = 1;
}

enum TeeFileMode {
  TEE_FILE_MODE_APPEND   = 0;
  TEE_FILE_MODE_TRUNCATE = 1;
}

enum TeeBackpressure {
  TEE_BACKPRESSURE_DROP_OLDEST = 0;
  TEE_BACKPRESSURE_BLOCK       = 1;
}

message RegisterSessionTeeRequest {
  string session_id          = 1;
  TeeSessionKind session_kind = 2;
  TeeStreamKind stream       = 3;
  TeeSinkKind sink_kind      = 4;
  // OS-native path bytes: Unix = OsStr bytes; Windows = little-endian UTF-16.
  bytes  file_path           = 5;
  TeeFileMode file_mode      = 6;
  // 0 means use the daemon default.
  uint32 queue_capacity      = 7;
  // false means write missed-byte markers, matching the Rust default.
  bool   suppress_missed_markers = 8;
  TeeBackpressure backpressure = 9;
}

message RegisterSessionTeeResponse {
  uint64 tee_handle = 1;
}

message UnregisterSessionTeeRequest {
  string session_id          = 1;
  TeeSessionKind session_kind = 2;
  uint64 tee_handle          = 3;
}
message UnregisterSessionTeeResponse {}

message GetSessionTeeStatusRequest {
  string session_id          = 1;
  TeeSessionKind session_kind = 2;
  uint64 tee_handle          = 3;
}

message GetSessionTeeStatusResponse {
  TeeStreamKind stream       = 1;
  uint64 missed_bytes        = 2;
  bool   disconnected        = 3;
}

// ---------------------------------------------------------------------------
// Optional session observer subscriptions (#221 Phase 2 / #429).
//
// Observer registrations live on the per-session struct, not on a per-client
// IPC connection. They survive the client's IPC connection going away. When a
// new client reconnects and re-registers it gets a fresh subscription — but
// events that arrived while no client was attached are NOT replayed. This is
// different from the PTY/pipe backlog (which is byte-stream-style); observer
// events are event-stream-style and are dropped (and counted) once the bounded
// sink fills, per the configured backpressure policy.
// ---------------------------------------------------------------------------

enum ObserverSessionKind {
  OBSERVER_SESSION_KIND_UNSPECIFIED = 0;
  OBSERVER_SESSION_KIND_PTY         = 1;
  OBSERVER_SESSION_KIND_PIPE        = 2;
}

// EventCategory values match `observer::EventCategory` integer ordering:
// 0=Lifecycle, 1=File, 2=Network, 3=Process. Phase 1 only honors Lifecycle;
// the others are accepted (so the client can request them ahead of Phase 3
// platform backends) but currently produce no events.
enum ObserverEventCategory {
  OBSERVER_EVENT_CATEGORY_LIFECYCLE = 0;
  OBSERVER_EVENT_CATEGORY_FILE      = 1;
  OBSERVER_EVENT_CATEGORY_NETWORK   = 2;
  OBSERVER_EVENT_CATEGORY_PROCESS   = 3;
}

// Backpressure for the bounded observer sink. Mirrors TeeBackpressure
// semantics: DropOldest never blocks the emitter, Block waits.
enum ObserverBackpressure {
  OBSERVER_BACKPRESSURE_DROP_OLDEST = 0;
  OBSERVER_BACKPRESSURE_BLOCK       = 1;
}

message RegisterSessionObserverRequest {
  string session_id                       = 1;
  ObserverSessionKind session_kind        = 2;
  // Requested categories. Each value is an ObserverEventCategory integer
  // (stored as repeated uint32 to match the Phase 1 in-process config which
  // accepts an arbitrary set). Empty = lifecycle only.
  repeated uint32 categories              = 3;
  // 0 means use the daemon default (1024).
  uint32 ring_capacity_events             = 4;
  ObserverBackpressure backpressure       = 5;
}

message RegisterSessionObserverResponse {
  // Server-assigned UUID for this subscription. Used by Unregister and
  // GetStatus. Not reused after Unregister.
  string subscriber_id = 1;
}

message UnregisterSessionObserverRequest {
  string session_id                = 1;
  ObserverSessionKind session_kind = 2;
  string subscriber_id             = 3;
}
message UnregisterSessionObserverResponse {}

message GetSessionObserverStatusRequest {
  string session_id                = 1;
  ObserverSessionKind session_kind = 2;
  string subscriber_id             = 3;
}

message GetSessionObserverStatusResponse {
  // Cumulative number of events that arrived while the bounded sink was
  // full. Increments only under DropOldest; Block backpressure never drops.
  uint64 missed_events = 1;
  // True if the downstream receiver has disconnected.
  bool   disconnected  = 2;
  // Cumulative number of events successfully delivered to this sink.
  uint64 delivered_events = 3;
}

// Daemon -> client stream frame for an attached stdout/stderr.
message PipeStreamFrame {
  oneof frame {
    bytes  bytes        = 1;
    // The child closed this stream (EOF). Terminal frame for the
    // attachment; the daemon will close the stream connection after.
    bool   eof          = 2;
    // The child has exited; the int payload is the exit code. Terminal
    // frame.
    int32  exit_code    = 3;
    uint64 missed_bytes = 4;
    string error        = 5;
    string stolen_by    = 6;
  }
}