zshrs 0.10.2

The first compiled Unix shell — bytecode VM, worker pool, AOP intercept, SQLite caching
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
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
Yeah — the daemon eliminates an entire architectural layer. Walk through what dies:

  compinit-related infrastructure that becomes obsolete:

  ┌─────────────────────────────┬────────────────────────────────┬────────────────────────────────┐
  │            Layer            │           Cost today           │          After daemon          │
  ├─────────────────────────────┼────────────────────────────────┼────────────────────────────────┤
  │ compinit cold               │ 200ms-2s, scales with fpath    │ 0 — never called               │
  │                             │ size                           │                                │
  ├─────────────────────────────┼────────────────────────────────┼────────────────────────────────┤
  │ .zcompdump (cache file)     │ 50-200ms warm-load +           │ gone — replaced by daemon's    │
  │                             │ corruption risk                │ rkyv                           │
  ├─────────────────────────────┼────────────────────────────────┼────────────────────────────────┤
  │ zcompile'd .zcompdump.zwc   │ extra 20-50ms loading bytecode │ gone                           │
  ├─────────────────────────────┼────────────────────────────────┼────────────────────────────────┤
  │ compdef foo bar calls       │ stored in stash, walked at     │ daemon owns dispatch table     │
  │                             │ lookup                         │ directly                       │
  ├─────────────────────────────┼────────────────────────────────┼────────────────────────────────┤
  │ _main_complete walk         │ O(N) function dispatch         │ O(1) hash lookup via daemon    │
  ├─────────────────────────────┼────────────────────────────────┼────────────────────────────────┤
  │ compinit -i / -u security   │ runs once per shell            │ runs once daemon-side, cached  │
  │ check                       │                                │                                │
  ├─────────────────────────────┼────────────────────────────────┼────────────────────────────────┤
  │ zinit's compdef-replay      │ replays compdef calls from     │ redundant — daemon has them    │
  │                             │ cache                          │                                │
  ├─────────────────────────────┼────────────────────────────────┼────────────────────────────────┤
  │ autoload -Uz declarations   │ parsed every shell             │ declared once in daemon        │
  ├─────────────────────────────┼────────────────────────────────┼────────────────────────────────┤
  │ bashcompinit glue           │ another 20-50ms                │ redundant                      │
  └─────────────────────────────┴────────────────────────────────┴────────────────────────────────┘

  For your 17k-completion setup the cold-completion-lookup math:

  - Today (no cache): parse ~17k files = 3-5s cold. 100% CPU spike on every shell.
  - Today (.zcompdump warm): load cache = 300-800ms. Corrupts every few weeks, requires manual rm
  ~/.zcompdump*.
  - Daemon + rkyv: mmap a pre-built byte-aligned table, single hash lookup per completion query =
  ~50ns to ~5μs depending on access pattern.

  That's not a 10× speedup. It's 10⁴ to 10⁶× on cold-start completion lookup. And the whole "is my
  .zcompdump stale" class of bugs disappears because fsnotify is the source of truth — when a file in
  fpath changes, the daemon rebuilds the affected slice and notifies subscribers. No polling, no cron,
   no manual rm.

  The correctness win is bigger than the speed win, actually. Every zsh user has hit the "completion
  is stale, restart shell, still stale, rm ~/.zcompdump, restart again" loop. With fsnotify as the
  single source of truth, that bug class can't exist. Same pattern as IDEs that watch the filesystem
  and update language-server state — the user never has to think about cache invalidation.

Right — thin client = pure consumer of pre-warmed state. Let me itemize what the client actually
  does vs what the daemon does:

  Thin client responsibilities (everything fast):

  ┌──────────────────────────────────────────┬──────────────────────┐
  │                Operation                 │         Cost         │
  ├──────────────────────────────────────────┼──────────────────────┤
  │ Connect to daemon (Unix socket)          │ <1ms                 │
  ├──────────────────────────────────────────┼──────────────────────┤
  │ mmap rkyv completion table               │ ~10μs (zero-copy)    │
  ├──────────────────────────────────────────┼──────────────────────┤
  │ mmap history index                       │ ~10μs                │
  ├──────────────────────────────────────────┼──────────────────────┤
  │ mmap alias / function table              │ ~10μs                │
  ├──────────────────────────────────────────┼──────────────────────┤
  │ Render prompt from cached theme segments │ ~1ms                 │
  ├──────────────────────────────────────────┼──────────────────────┤
  │ Capture keystrokes → forward to daemon   │ <100μs round trip    │
  ├──────────────────────────────────────────┼──────────────────────┤
  │ Display output (write to terminal)       │ terminal-bound       │
  ├──────────────────────────────────────────┼──────────────────────┤
  │ Subscribe to fsnotify-pushed events      │ ~free, async channel │
  └──────────────────────────────────────────┴──────────────────────┘

  Total cold-start: <10ms. No work needed beyond connecting and mmap-ing.

  Daemon responsibilities (all the heavy state):

  - fpath scan + completion function parse (one-shot at boot, fsnotify-incremental after)
  - compdef registry (single source, hash-indexed)
  - History database (SQLite FTS, indexed, queryable)
  - Plugin lifecycle (lazy-load, dependency graph, hooks)
  - Syntax-highlighting parse trees (tree-sitter, cached per-buffer)
  - Autosuggestion engine (frecency-ranked history search)
  - Theme resolution (segment computation, color mixing)
  - Alias / function expansion table
  - Variable scope graphs
  - Job orchestration / status (long-running tasks)

  This is exactly the architecture every modern multi-process system uses:

  ┌────────────────┬───────────────────────────────────────────┬──────────────────────────────────┐
  │     System     │               Daemon (fat)                │           Thin client            │
  ├────────────────┼───────────────────────────────────────────┼──────────────────────────────────┤
  │ X11 / Wayland  │ display server (compositing, input        │ each app (just renders pixels    │
  │                │ routing, window management)               │ into a buffer)                   │
  ├────────────────┼───────────────────────────────────────────┼──────────────────────────────────┤
  │ LSP            │ language server (parsing, type-checking,  │ editor (just shows completions)  │
  │                │ completion logic)                         │                                  │
  ├────────────────┼───────────────────────────────────────────┼──────────────────────────────────┤
  │ Postgres /     │ server (query plan, indexes, locks,       │ psql / clients (formats results) │
  │ MySQL          │ transactions)                             │                                  │
  ├────────────────┼───────────────────────────────────────────┼──────────────────────────────────┤
  │ Docker         │ dockerd (image cache, container           │ docker CLI (~3MB binary, just    │
  │                │ lifecycle, network)                       │ talks to dockerd)                │
  ├────────────────┼───────────────────────────────────────────┼──────────────────────────────────┤
  │ K8s            │ API server + etcd + controllers           │ kubectl                          │
  ├────────────────┼───────────────────────────────────────────┼──────────────────────────────────┤
  │ systemd        │ systemd PID 1 (service graph, sockets,    │ systemctl (just queries)         │
  │                │ cgroups)                                  │                                  │
  ├────────────────┼───────────────────────────────────────────┼──────────────────────────────────┤
  │ zshrs (your    │ daemon (state, caches, plugins, jobs)     │ shell client (mmap + render)     │
  │ design)        │                                           │                                  │
  └────────────────┴───────────────────────────────────────────┴──────────────────────────────────┘


## v1 Locked Design

Everything below is the sole-source-of-truth for the v1 implementation. Cross-references the existing memory file `cache_architecture_rkyv.md`; this section captures the additional decisions made in the design pass and consolidates them in one place so impl can start from a single doc.

### The 90/10 work split

90% of all shell-internal work lives in the daemon. 10% in the client. The daemon is fat: full thread pool, hundreds of concurrent requests in flight, long-running jobs, push notifications, cross-shell routing, federation. The client is paper-thin: tty IO, process bookkeeping, fork+exec, and direct mmap reads.

**Daemon owns:**

- All compilation: fpath functions, `.zshrc`, plugins, user scripts → bytecode.
- All persistence: rkyv shard writes, `catalog.db` hydration, `history.db` append.
- All search and walking: history FTS, frecency ranking, fuzzy matching, completion enumeration, tree-sitter parsing.
- **All filesystem enumeration:** `$PATH` dir scans, `$FPATH` dir scans, plugin tree walks, completion file discovery, theme file reads, all `find`/`glob`/`readdir` over shell-internal directories.
- **All starting-state preparation:** `$PATH`/`$FPATH`/`$MANPATH` resolution, command hash table, autoload function table, alias table, shell-options state, keybindings, theme templates — all pre-resolved daemon-side and served at boot.
- All long-running work: `zjob` supervision, plugin install/update, daemon ticker (rotation, vacuum, integrity scan).
- All routing: cross-shell pub/sub, shell registry (`zls`), session tracking (cwd shadow, override generation counter).
- The single fsnotify watcher across the machine.
- All authority decisions: cross-uid dispatch validation, federation auth, integrity checks.

**Client owns:**

- tty IO: keystroke read, screen paint.
- Process attributes: `$$`, `$!`, `$?`, traps, signal handlers, fd table, positional params, locals.
- `fork+exec` for user commands.
- IPC client to daemon.
- Direct mmap indexed reads (and nothing else against the cache).
- Tiny in-memory overlay hash for in-session monkey-patches.

### NO WALKING IN CLIENTS

Absolute rule. Two surfaces this covers:

**1. No client-side data-structure traversal.** Client-side cache access is `hash(key) → mmap_index → value`. ONE indirection. No probing chains, no traversal, no iteration over rkyv internal structures. Mechanism: rkyv shards use **perfect hashing** (PHF, generated daemon-side at compile time). Every key has a unique slot. Client lookup = compute hash, mask to slot, single mmap dereference. ~150-200ns end-to-end.

**2. No client-side filesystem walks for shell-internal purposes.** Clients never `find`, `glob`, `readdir`, or stat-loop over `$PATH`, `$FPATH`, plugin trees, completion directories, or any other shell-internal source. The daemon walks everything once and serves the precomputed results (see "Starting state served by daemon" below). Even `hash -r` becomes an IPC to the daemon, not a client-side rebuild of the command hash table.

When iteration is required (e.g., `${(k)_comps}`, `for cmd in $(hash); do …`), the daemon either:

1. Precomputes a sorted/deduped flat key array, stores it in the shard. Client receives the slice directly; the only iteration is over a flat array, not a data-structure walk.
2. Serves the iteration over IPC: `{"op":"keys","param":"_comps"}` → daemon walks its own state, returns flat list to client.

When logic is required (matching, ranking, filtering, dedup of overlay-vs-rkyv): IPC the daemon. Client never runs the logic.

Exception (intentional): `fork+exec`'d user commands (`find`, `ls`, `rg`, `grep`) walk filesystems normally — those are user code, not shell-internal walks.

### Daemon = sole writer

No client ever writes to:

- Any rkyv shard
- `catalog.db`
- `history.db`
- `daemon.pid`
- `daemon.sock`
- `zshrs.log`

Client mutations land only in:

- Process state (PWD, env, `$$`, fds, signals, traps, locals, positional params).
- Per-client overlay hash (interactive `compdef`, `alias`, `function`, monkey-patched globals, autoloaded function bodies).

Overlay dies on `exec zshrs`. Daemon never sees overlay state unless the client packages it into an IPC request (e.g., `complete` ops include `overlay_gen` and the daemon can ask for the delta).

### Snapshot-at-boot + overlay

Each client boots, mmaps the daemon's then-current shard set, and runs against that snapshot for its lifetime. Daemon-side rebuilds (fsnotify-driven) become visible to that client only after `exec zshrs`. New shells started after a rebuild see the new image immediately.

Atomic-rename per shard with strict ordering — shard rename FIRST, then `index.rkyv` update — prevents torn reads. Existing client mmaps stay valid via kernel inode-pinning (deleted-but-mapped pages stay alive until last unmap on Linux and macOS).

### Cache layout (locked)

```
~/.cache/zshrs/
├── index.rkyv                          ← top-level fq_name → (shard_id, generation, byte_offset)
├── images/
│   ├── {hash8}-system.rkyv             ← system / shipped completions
│   ├── {hash8}-completions-corpus.rkyv ← zsh-more-completions
│   ├── {hash8}-zpwr.rkyv               ← zpwr functions / completions
│   ├── {hash8}-zshrc.rkyv              ← user .zshrc bytecode (per-user)
│   ├── {hash8}-plugin-{name}.rkyv      ← per zinit / oh-my-zsh plugin
│   ├── {hash8}-script-{slug}.rkyv      ← per `zshrs FILE` invocation
│   └── {name}.rkyv.lock                ← per-shard advisory flock
├── catalog.db                          ← daemon-only writer; queryable mirror
├── history.db                          ← daemon-only writer; SQLite FTS
├── zshrs.log                           ← tracing output, daemon-rotated, 10 MB cap
├── daemon.sock                         ← Unix socket for IPC
└── daemon.pid                          ← singleton flock + daemon process ID
```

### catalog.db schema (daemon-only writer)

```sql
plugins         (name, version, source, installed_at, enabled)
plugin_deps     (plugin, dep, constraint)
entries         (fq_name, plugin_id, kind, image_path, byte_offset, source_loc, bytecode BLOB)
hooks           (kind, name, fq_name)
entry_stats     (fq_name, last_called_at, call_count, total_ns)
compiled_files  (path PRIMARY KEY, kind TEXT, mtime, inode, hash, bytecode BLOB,
                 last_used_at, use_count, bytes_in, bytes_out, sensitive BOOLEAN,
                 parent_paths TEXT)
                 -- kind ∈ {'script', 'source', 'zshrc', 'plugin_init', 'autoload'}
                 -- one unified table for any file that gets parsed and bytecoded by the daemon,
                 -- looked up by absolute path. covers `zshrs FILE`, `source FILE`, .zshrc,
                 -- plugin entry-point files, autoloaded function files. parent_paths is a
                 -- JSON array of files that transitively included this one (for invalidation
                 -- chain when an upstream source dependency changes).
```

`bytecode` BLOB columns make `catalog.db` a self-contained mirror of all compiled state. Two-way reconstruction: rkyv shards ↔ catalog.db can each rebuild from the other. catalog.db is queryable and joinable; rkyv is hot-path zero-copy. Hot lookups never hit SQLite — clients only mmap rkyv.

`scripts` table powers `zshrs FILE`: client stat()s the file, sends `load_script` IPC keyed by `(path, mtime, inode)`, daemon returns hit-from-cache or compiles-and-stores. Same pattern unifies `zshrs FILE`, `source ~/zpwr/init.sh`, and `.zshrc` cold-start: stat + IPC + mmap + replay env-mutation log, regardless of source-file size.

### Special parameters served by daemon

These zsh global associative parameters are daemon-prepared, perfect-hash-indexed in rkyv, and exposed to clients with overlay-on-mmap semantics:

- `_comps` — completion handler dispatch table
- `_services` — service-name aliases
- `_patcomps` — pattern-matched completions
- `_describe_handlers` — completion description providers

Read: client computes `hash(key)` → mmap index → value. ONE indirection. Overlay hash checked first; on miss, fall through to mmap. No walking.

Write: insert into client-local overlay hash. rkyv image is read-only.

Iterate: client receives the daemon-precomputed flat key array (mmap'd slice) plus overlay keys. Iteration is flat-array iteration only; no data-structure walks.

Plugin compat falls out: zinit's `_comps[foo]=_my_handler` direct assignment lands in overlay; `${(k)_comps}` iterates the daemon-flat-array merged with overlay; `compdef foo bar` writes to overlay. zinit's compdef-replay is harmless redundancy.

For legacy tooling that introspects `.zcompdump` directly (some plugin patterns, backup scripts, p10k cache-staleness probes, parallel zsh sessions sharing the cache), the daemon can synthesize a valid `.zcompdump` file on demand from its canonical state. Triggered by `zcache export zcompdump [path]` or the `export_zcompdump` IPC op. The synthesized file is byte-compatible with what `compinit` would have produced, so legacy consumers don't notice the difference. Not generated automatically — opt-in only, on user request. (`.zcompdump.zwc` is not emitted; legacy tooling will regenerate it if it wants it.)

### Starting state served by daemon (PATH, FPATH, hash tables, etc.)

Daemon parses the user's `.zshrc` AND every plugin it sources (zinit-loaded plugins, oh-my-zsh-loaded plugins, manually-sourced files), evaluates them in an analysis pass, and consolidates the resulting state effects into starting-point caches that all clients consume. The no-walking rule is total: anything the daemon can pre-walk and serve, it does. Clients never `find`, never `glob` over fpath, never enumerate PATH directories, never scan plugin trees.

Plugin discovery happens at the same time as `.zshrc` analysis: daemon walks the user's `.zshrc`, sees zinit/OMZ/source calls, descends into each referenced plugin, parses + bytecode-compiles per-plugin shards (`{hash8}-plugin-{name}.rkyv`), captures every state contribution (alias declarations, function definitions, fpath additions, `compdef` calls, `zstyle` declarations, `bindkey` calls, `setopt` calls, env exports), and folds them into the consolidated starting-point caches below. Each plugin's compiled bytecode lives in its own shard for cache-locality and per-plugin invalidation; the *state effects* of all plugins fold into the unified per-user boot-state image.

Pre-walked state delivered at boot:

| State | Mechanism |
|-------|-----------|
| `$PATH` | Daemon evaluates user dotfiles, resolves `PATH=` / `path+=` / plugin contributions, serves final string |
| Command hash table | Daemon walks every directory in `$PATH`, builds perfect-hash `command_name → absolute_path` table in rkyv. Client `which`/`command -v` = single lookup. `hash -r` = IPC to daemon |
| `$FPATH` | Resolved list of autoload directories, served as a flat array |
| Autoload function table | Daemon walks every `$FPATH` directory, populates `function_name → (shard_id, byte_offset)` in `index.rkyv`. Client `autoload` = hash lookup, never `find`/`glob` |
| `$MANPATH`, `$INFOPATH`, `$CDPATH`, `$LD_LIBRARY_PATH` | Same model — resolved values served at boot |
| **Named-directory hash (`hash -d`)** | Daemon parses all `hash -d name=/path` from `.zshrc` and plugins, serves resolved `name → path` perfect-hash table. `~name` expansion = single lookup. Interactive `hash -d` writes to overlay; `zsync up named_dir` promotes |
| Completion staleness metadata | Daemon scans completion file mtimes; results live in `entries.source_loc` + `entry_stats` |
| Theme initial state | Daemon resolves PROMPT segments, RPROMPT, color palette once; serves the final templates |
| Initial alias table | Daemon parses user `.zshrc` alias declarations, serves resolved table. Interactive `alias foo=bar` writes go to overlay |
| Global aliases (`alias -g`) and suffix aliases (`alias -s`) | Pre-resolved by daemon, served as separate perfect-hash tables |
| Initial shell-options state | `setopt`/`unsetopt` calls in `.zshrc` pre-resolved; client boots with final option mask |
| Initial keybinding table | `bindkey` declarations pre-resolved; client boots with final binding map |
| Loaded modules state | `zmodload` declarations pre-resolved; daemon ensures required modules are available, serves initial loaded-module set |
| Initial environment (`env`) | Exported vars (`export FOO=bar`) from `.zshrc` and plugins pre-resolved; client boots with canonical env, mutations go to overlay |
| Initial shell parameters (`params`) | Non-exported shell-level vars (`FOO=bar`, `typeset -A MAP=(...)`, `typeset -a ARR=(...)`) pre-resolved by daemon into a typed table (scalar / array / assoc); same overlay + `zsync up` / `eval $(zcache export params)` machinery as everything else |
| `zstyle` registry | All `zstyle` declarations from `.zshrc` and plugins pre-resolved into a daemon-served context-pattern → key-value table |

The mechanism is uniform: daemon parses and evaluates user dotfiles in an analysis pass, captures deterministic state effects, serializes into the user's boot-state shard. Client at boot mmaps the shard, applies state to its process, and is fully initialized. No client-side filesystem walks for shell-internal purposes.

**Determinism boundary:** non-deterministic `.zshrc` fragments — anything that calls `$(date)`, reads `/dev/urandom`, conditionally branches on `$$` or `$RANDOM`, depends on per-shell state — are detected during the analysis pass and emitted as a small per-shell replay log. Client executes those fragments locally at boot. The vast majority of `.zshrc` content is deterministic and gets pre-resolved on the daemon side.

**What's NOT covered by this rule:** `fork+exec`'d user commands (`find`, `ls`, `rg`, `grep`, etc.) are user code, not shell-internal walks. Those run normally in the client. The no-walking rule applies only to shell-internal directory enumeration (PATH/FPATH scans, completion file lookups, autoload resolution, plugin discovery, `hash` table population, theme file reads).

Result: a 172k-line `zpwr` `.zshrc` should cost client cold-start no more than the IPC + mmap + state-apply pass — measured in milliseconds, not seconds. Per-client init cost is independent of `.zshrc` size or fpath cardinality.

**Walk lifecycle — first init + cache bust only.** The daemon walks `$PATH` / `$FPATH` / plugin trees / source-statement targets exactly twice in its life:

1. **First init.** Cold cache, no rkyv shards present (or daemon spawning into an empty `~/.cache/zshrs/`). Init is multi-pass and order-sensitive — `$PATH` and `$FPATH` don't exist as final values until the user's dotfiles are parsed:
   - **Pass 1 — parse system + user dotfiles.** `/etc/zshenv` → `~/.zshenv` → `/etc/zprofile` → `~/.zprofile` → `/etc/zshrc` → `~/.zshrc` → `/etc/zlogin` → `~/.zlogin`, in zsh's standard order. Follow `source` / `.` statements transitively into plugins, env files, tokens.sh, anything reachable. Compile each parsed file into `compiled_files`.
   - **Pass 2 — evaluate state mutations deterministically.** Replay every `path+=`, `fpath+=`, `PATH=`, `FPATH=`, `export VAR=`, `alias`, `setopt`, `bindkey`, `compdef`, `zstyle`, `hash -d`, `zmodload`, `typeset` declaration in source-order against an in-memory state model. Captures the resolved final value of every shell parameter that boots into a new shell.
   - **Pass 3 — walk now-resolved $PATH / $FPATH / plugin tree directories.** With `$PATH` and `$FPATH` finalized, daemon walks each directory to build the command-hash table (`command_name → executable_path`) and the autoload-table (`function_name → file_path`). Plugin trees referenced from `.zshrc` get walked here too.
   - **Pass 4 — serialize.** Build perfect-hash tables, write rkyv shards, atomic-rename, write `index.rkyv`, hydrate `catalog.db` `entries` + `compiled_files` + `entry_stats`. Register fsnotify watches on every directory and file involved.

   One-time cost. Runs in the background while clients fall back to source-interp until the first shard atomic-renames.

2. **Cache bust.** User-explicit `zcache clean` / `zcache clean shards` / `zcache rebuild` / version-migration / corruption-recovery triggers a re-walk of the affected scope. Same multi-pass ordering applies whenever the bust changes a source file that contributes to `$PATH` / `$FPATH` resolution: parse first, evaluate, then walk. `zcache clean shard <name>` re-walks just that shard's source root (no need to re-parse dotfiles if `$PATH` / `$FPATH` haven't changed); `zcache rebuild` walks everything from Pass 1.

**Steady state: fsnotify only, with delta-walks for newly-introduced directories.** Between first init and cache bust, the daemon never re-enumerates a directory it already knows about. fsnotify watches every directory and file the daemon registered during the last walk; events fire on create / modify / delete / rename and the daemon updates exactly the affected entries. The only walks that happen in steady state are walks of *newly-introduced* directories — when a parsed dotfile change adds a new path to `$PATH` / `$FPATH` that wasn't watched before:

- File created in a watched `$FPATH` dir → daemon parses + bytecode-compiles → inserts into autoload-table → atomic-renames the affected shard → bumps generation → optional push to subscribers.
- File deleted → daemon removes the entry from `entries` + autoload-table + relevant shard → atomic-rename → bump.
- File modified → daemon re-parses just that one file → updates one row in `entries` → updates one slot in the affected shard's perfect-hash table → atomic-rename → bump.
- File renamed → treated as delete-old + create-new in the same atomic update.
- **`.zshrc` modified to add `path+=(/opt/foo/bin)`** → daemon detects `$PATH` resolution changed → walks ONLY `/opt/foo/bin` (the delta), inserts new commands into command-hash → registers fsnotify watch on `/opt/foo/bin` → atomic-rename → bump. Same for `fpath+=`. The delta-walk is bounded to the new directory; existing directories are not re-enumerated.
- **`.zshrc` modified to remove a path entry** → daemon detects removal → drops command-hash entries that came from the removed directory → unregisters its fsnotify watch → atomic-rename → bump. No full rewalk.
- **Client `zsync up path`** with new directory → same delta-walk path: daemon walks just the new directory, updates canonical, registers watch.

**No polling, no periodic rescan, no cron.** Per the hard invariants. fsnotify is the source of truth for "what changed since the last walk." If fsnotify drops an event (kernel queue overflow on Linux, FSEvents coalescing on macOS), `zcache verify` catches the drift on next user invocation and recommends `zcache rebuild` for the affected shard. Drift is rare; recovery is one verb.

**Why this matters at zpwr scale.** Walking 1.6 M LOC across 579 files takes seconds, not milliseconds, even on fast SSDs. Doing this on every shell launch (zsh's status quo) is a non-starter. Doing it once at first init and incrementally thereafter via fsnotify means cold-start cost is paid in full only once — first daemon spawn ever — and amortizes to zero across the next 478 k commands the user types.

### Source / dot interception and file registry

The `source` and `.` builtins are daemon-aware. Every call routes through the daemon's `compiled_files` registry:

```
client: source /Users/wizard/.zpwr/local/.tokens.sh
    │
    ↓  IPC: {"op":"source_resolve","path":"/Users/wizard/.zpwr/local/.tokens.sh","mtime":N,"inode":M}
    │
daemon: lookup compiled_files WHERE path = …
    HIT (mtime+inode match):    return shard_path + generation
    MISS:                        parse + bytecode-compile + insert + return
    STALE (mtime/inode differ):  rebuild + atomic-rename + return new generation
    │
    ↓
client: mmap shard, replay env-mutation log + execute function definitions in current process
```

**Effect:** every file ever sourced from any zshrs shell ends up in the daemon's registry, fsnotify-watched, bytecode-cached. Subsequent `source` of the same file is mmap + replay, not parse. The `.zshrc` analysis pass follows `source` / `.` calls transitively at compile-time, so the entire transitive-closure of files reachable from `.zshrc` is registered before the first interactive shell boots — `parent_paths` records the inclusion chain so an edit to a deeply-nested sourced file invalidates all the bytecode that depended on it.

**Concrete example.** User has in `.zshrc`:
```sh
source ~/.zpwr/local/.tokens.sh    # API keys, passwords, env exports
source ~/.zpwr/init.sh             # main zpwr init (172k LOC across sourced submodules)
source ~/.config/work/aliases.sh   # work-specific aliases
```

Daemon analysis pass walks all three transitively, parses each, bytecode-compiles each, registers them in `compiled_files` as `kind='source'`. fsnotify watches each. Edit `.tokens.sh` → daemon rebuilds just that one entry → next shell boot sees the change. Edit `~/.zpwr/init.sh` → daemon rebuilds it and any submodule it transitively sources.

**Sensitive content.** Files like `.tokens.sh` typically contain secrets (API keys, passwords, `export AWS_SECRET_ACCESS_KEY=…`). The daemon caches their bytecode, which means the bytecode shard contains the secret in plaintext-equivalent form. Two enforcement points:

1. `~/.cache/zshrs/` directory permission is `0700` (user-only). Files inside are `0600`. Set at daemon startup, verified by `zcache verify` on every integrity scan. Any drift triggers a `WARN` in `zshrs.log` and a refusal to attach for non-owner clients.
2. `compiled_files.sensitive` flag is set when daemon detects likely-secret content (heuristic: file path matches `*tokens*`, `*secret*`, `*credentials*`, `*.env*`, or content contains `AWS_SECRET`, `API_KEY=`, `PASSWORD=`, etc.). When set: the file's bytecode shard is written with `O_NOFOLLOW`, mmap'd `MAP_PRIVATE` only, and excluded from `zcache export --all` archive output unless `--include-sensitive` is passed. `zcache view` and `zcache export` for these targets refuse to print contents to terminal unless `--show-sensitive` is passed (just a one-flag opt-in; no friction).

User opts into this caching by sourcing the file from `.zshrc` — the assumption is the user already trusts `~/.cache/zshrs/` with the same threat model as `~/.zpwr/local/.tokens.sh` itself. If they don't, the workaround is `[[ -o interactive ]] && source …` guarded so daemon analysis skips it (see "Determinism boundary" — daemon emits per-shell replay for conditional sources rather than baking them).

### Compat surface and zpwr-scale validation target

Daemon design is validated against the `~/.zpwr` codebase as the bedrock real-world load — anything that can't handle this is a design failure. Concrete numbers from the user's machine:

| Measurement | Value |
|---|---|
| `.zshrc` line count | 889 |
| `~/.zpwr` total shell LOC (`.zsh` + `.sh`) | ~1.6M |
| `~/.zpwr` shell file count | 579 |
| Existing `.zwc` files in `~/.zpwr` | 40 |
| `~/.zpwr/local/.zcompdump-zpwr-MenkeTechnologies` | 753 KB |
| Same, zcompiled (`.zwc`) | 1.8 MB |
| `~/.zpwr/local/.zpwr-MenkeTechnologies-history` | 25 MB (478k commands) |
| `~/.zpwr/local/.tokens.sh` (sensitive, pre-zwc'd) | 12 KB |
| `~/.zpwr/local/.common_aliases` | 92 KB |

The daemon must absorb all of this on first cold start (one-shot, then incremental on fsnotify-detected changes). Subsequent shell boots see <10 ms client cold-start regardless of zpwr scale.

**Plugin manager interop.** zpwr's `.zshrc` switches between plugin managers via `$ZPWR_PLUGIN_MANAGER` and ships with built-in support for zinit, antigen, zplug, antibody, oh-my-zsh, and direct git-clone. zshrs daemon supersedes all of these architecturally — the daemon IS the plugin manager (parses, caches, lifecycle, dependency graph). But for `.zshrc` files in the wild, the daemon supports the syntax of the major managers as input to its analysis pass:

| Manager | Compat surface | Status |
|---|---|---|
| **zinit** | `zinit ice …; zinit load|light|snippet …; zinit cdreplay; zinit creinstall` — full ice-modifier grammar; turbo-mode hints recognized but no longer needed (daemon has zero cold-start cost so deferral is meaningless) | Required — primary daily-driver target |
| **oh-my-zsh** | `ZSH_THEME=…; plugins=(…); source $ZSH/oh-my-zsh.sh; antigen-bundle …` — recognize plugin array, apply plugins from `$ZSH_CUSTOM/plugins/` and OMZ tree | Required — zpwr uses OMZ libs/plugins/comps as fallback |
| **antigen** | `antigen bundle …; antigen apply` | Best-effort |
| **zplug** | `zplug "user/repo"; zplug load` | Best-effort |
| **antibody** | `antibody bundle <<<…` | Best-effort |
| **sheldon** | TOML-config-driven | Best-effort |
| **znap / zgenom / zcomet / zr** | various | Best-effort |
| **direct `git clone` + `source`** | base case | Always works (handled by source-interception) |

Daemon doesn't *run* zinit's Ruby/zsh code. It parses zinit declarations to extract: which plugins to load, which ice modifiers (lazy-load, install-time hooks, alternate paths) to honor, what lifecycle behavior the user expects. The daemon then implements that behavior natively. zinit-the-zsh-plugin can be removed from the user's `.zshrc` once daemon-handled compat is verified, but doesn't have to be — having it source unmodified is fine; daemon analysis just notices the work has already been done.

**`.zwc` files: invisible to scans, importable on demand, encouraged to delete.** During every automatic discovery pass the daemon performs — `$FPATH` enumeration, plugin tree walks, source-statement following, fsnotify watches, autoload table population — `.zwc` files are filtered out and treated as if they don't exist. The daemon only sees the source files (`.zsh`, `.sh`). `.zwc` files in `~/.zpwr` (or anywhere else) sit on disk untouched and unread by any auto-driven code path. They never feed into the cold-start, never participate in fsnotify, never count toward "what's in fpath."

Once a user is on zshrs, every `.zwc` and `.zcompdump-*` file is **dead disk litter that contributes zero speed** — no longer doing any work, just consuming bytes and confusing future debugging.

`.zwc`'s entire reason to exist is "skip the source-parse step on cold load" — but zshrs clients never parse source on cold load. The daemon parsed it once, serialized into rkyv, and clients mmap the rkyv shard. The cold path is mmap + index + execute. There's no parse step for `.zwc` to short-circuit, so even if zshrs *did* read `.zwc`, it would be slower than the rkyv path (`.zwc` deserialization → AST → re-execute is more work than mmap → indexed bytecode → execute). `.zcompdump` has the same problem at one layer up (compinit-cache vs daemon-served `_comps`).

zshrs encourages cleanup:

```
zcache clean zwc                    # find and delete every .zwc inside known scope (fpath, plugin trees,
                                    # source-statement targets, autoload paths, ~/.zpwr, ~/.zsh, etc.)
zcache clean zwc --dry-run          # report what would be deleted, delete nothing
zcache clean zcompdump              # find and delete every .zcompdump* file (default scope: $HOME, $ZDOTDIR,
                                    # $ZPWR_LOCAL, $XDG_CACHE_HOME)
zcache clean legacy                 # zwc + zcompdump together; the standard "I am fully on zshrs" cleanup
zcache verify                       # already exists; reports .zwc/.zcompdump presence as a WARN with the
                                    # cleanup hint, since their existence implies stale legacy artifacts
```

Out-of-scope dirs are never touched (no recursive `find ~ -name '*.zwc'`). Daemon walks only the directories it already knows about from the user's `.zshrc` analysis. `--dry-run` shows the full list before commit. No confirmation prompt at delete time (per CLAUDE.md "no friction"). `zcache verify` runs as part of `zcache info` and flags litter every time the user looks at cache state, gently surfacing the cleanup verb without nagging.

Once cleaned, the user's daily-driver disk footprint for shell artifacts is just `~/.cache/zshrs/` — single directory, daemon-owned, queryable, exportable. No more 40 `.zwc` files scattered through `~/.zpwr`, no more 1.8 MB `.zcompdump.zwc` orphan, no more `~/.zcompdump*` accumulating across versions.

**Why no auto-import:** `.zwc` (and `.zcompdump`) files can be arbitrarily stale relative to the source they were compiled from. Picking them up automatically would let stale bytecode bleed into the daemon's canonical view, masking real source changes and producing the same "completion is stale, restart shell, still stale" failure mode that motivates the daemon architecture in the first place. The daemon's source-file-is-authoritative rule means it always parses fresh from `.zsh` / `.sh` and never trusts a pre-compiled artifact unless the user explicitly says so.

**On-demand import (user-invoked only):**

```
zcache import zwc <path>            # ingest a single .zwc — daemon validates against the adjacent
                                    # source file (mtime+hash); skips with WARN if stale; on match,
                                    # uses the .zwc to skip the source-parse pass for that file
zcache import zwc --tree <dir>      # walk a directory, import every .zwc that has a fresh adjacent
                                    # source file; stale ones reported and skipped
zcache import zcompdump <path>      # ingest a .zcompdump as compdef seed (validated against current
                                    # fpath; entries pointing at non-existent functions are dropped)
```

Both verbs are explicit user choice, never run automatically. Both validate freshness before merging:

- `.zwc` ingest: daemon stats the adjacent `.zsh` / `.sh` source; if mtime newer than `.zwc`, skip with `WARN: stale .zwc, will reparse from source`. If fresh, ingest the bytecode equivalence and skip the parse step for that file.
- `.zcompdump` ingest: daemon walks every entry; if the referenced function file doesn't exist or has a newer mtime, drop it; only fresh entries are merged.

Conflict resolution: incoming entries that disagree with daemon's current canonical state report a merge plan; `--force` overrides, default is skip-with-WARN.

This preserves the optionality (user can opt in to skip parse work for a known-fresh prebuilt cache) without the auto-staleness trap.

**History migration.** zpwr's 25 MB / 478 k-command history file (`~/.zpwr/local/.zpwr-MenkeTechnologies-history`) ingests once into the daemon's `history.db` via `zcache import history <path>`. The legacy zsh `HISTFILE=` setting becomes a no-op once migrated — daemon owns the canonical store. Backwards-export to legacy zsh format available via `zcache export history --format zsh-histfile` for round-trip.

**`compinit`'s `.zcompdump-…` files.** Daemon ignores existing `.zcompdump` on the first run (the rkyv corpus is canonical), but `zcache import zcompdump <path>` provides a one-shot ingest path. After migration, the legacy `.zcompdump` files remain on disk untouched until the user removes them.

**Acceptance against zpwr.** Cold-cache full-corpus build for `~/.zpwr` (1.6 M LOC, 579 files): target <60 s clean compile. Warm-cache cold-start of a new shell: target <10 ms. After migration, every existing user workflow that worked under zsh+zpwr+zinit+p10k must work under zshrs+daemon, with the speedups described in the comparison table at the top of this doc.

### Promoting client-local changes to daemon canonical

Clients can push their local overlay state up to the daemon to become the new starting-state for **future** shells. This makes the overlay a staging area for what may eventually become canonical, with the user explicitly deciding what gets promoted.

**Direction of effect:**

- Pushing client's `$PATH` modification → daemon updates canonical PATH → command hash table rebuilt daemon-side → next shell boots with new PATH already populated.
- The pushing shell itself already has the new PATH via its overlay; the push is for the benefit of future shells.
- Existing other shells stay on their boot-time snapshot unless they explicitly opt in to canonical-change events via subscription.

**Mechanism:** new IPC op + event + builtin family:

- IPC op: `{"op":"push_canonical","args":{"subsystem":"path","value":["/usr/bin","/usr/local/bin","/opt/foo/bin",…]}}`
- IPC event: `{"event":"canonical_changed","subsystem":"path","generation":N}` — fired to subscribers after daemon commits.
- Builtin: `zsync` family.

**`zsync` builtin (added to the z\* family):**

```
zsync up path                       # push current $PATH to daemon canonical
zsync up fpath                      # push current $FPATH
zsync up named_dir <name…>          # push named-directory entries (hash -d)
zsync up named_dir --all
zsync up alias <name…>              # push specific alias(es)
zsync up alias --all                # push all aliases from overlay
zsync up function <name…>           # push function definition to daemon
zsync up compdef <name…>            # push compdef registration
zsync up env <var…>                 # push exported env var(s)
zsync up params <var…>              # push non-exported shell parameter(s)
zsync up zstyle <pattern…>          # push zstyle declarations
zsync up zstyle --all
zsync up bindkey <key…>             # push keybindings
zsync up bindkey --all
zsync up setopt <option…>           # push shell options
zsync up zmodload <module…>         # push module load declarations
zsync up --all                      # promote everything in overlay to canonical
zsync diff                          # show overlay-vs-canonical for all subsystems
zsync diff <subsystem>              # focused diff
zsync watch <subsystem…>            # subscribe to canonical_changed events for these subsystems
zsync pull <subsystem>              # explicit pull: refresh local state from daemon canonical (opt-in mid-session refresh; breaks snapshot rule on user request)
```

**Visibility semantics for currently-running shells:**

- Snapshot-at-boot is the default. Other running shells see their boot-time canonical, not the new one.
- `zsync watch path` subscribes to `canonical_changed.path` events. On match, the shell can react: log a notice, prompt the user, or call `zsync pull path` to opt in to the new state mid-session.
- Without subscription, running shells stay frozen until `exec zshrs` or explicit `zsync pull`.

**Daemon-side commit flow on `push_canonical`:**

1. Validate the pushed value (sane format, dirs exist for PATH/FPATH, no duplicate keys, etc.).
2. Update canonical state (in-memory + persisted to a daemon-managed config shard).
3. Rebuild any derived hashtable that depends on the changed subsystem (e.g., command hash table for PATH change).
4. Atomic-rename the affected shard, bump generation, update `index.rkyv`.
5. Emit `canonical_changed` event to subscribers.

**What this gets you:** explicit control over what becomes "the way it is" for future shells. `path+=(/opt/foo/bin); zsync up path` is a session-action that takes effect for the next 100 tmux panes the user opens. Without `zsync up`, the path mod dies with this shell.

### Universal cache dump / view / export

For debugging, backup, migration, and integration with legacy tooling, the daemon can serialize **any** of its caches in multiple formats. Two user-facing verbs:

- `zcache view <target>` — pretty-print to stdout (default format = human-readable text; `--format json|yaml|disasm|…` for structured)
- `zcache export <target> [--out <path>]` — write to file (default format = native rkyv binary; `--format` for alternatives)

Both are thin IPC wrappers over daemon ops `view_cache` / `export_cache`. Daemon does the serialization work; client only paints stdout or writes to disk.

**Targets** (what can be dumped):

| Target | Description |
|--------|-------------|
| `path` | Canonical `$PATH` |
| `fpath` | Canonical `$FPATH` |
| `manpath`, `infopath`, `cdpath`, `ld_library_path` | Resolved values |
| `named_dir` | `hash -d` table |
| `command_hash` | Command name → executable path table |
| `autoload_table` | Function name → file path table |
| `aliases` | Alias table |
| `galiases` / `saliases` | Global aliases (`alias -g`) / suffix aliases (`alias -s`) |
| `functions [<name>]` | All function bytecode, or one named function (+ disassembly with `--format disasm`) |
| `compdef` / `_comps` | Completion handler dispatch table |
| `_services`, `_patcomps`, `_describe_handlers` | Sub-handler tables |
| `zstyle` | zstyle context-pattern → key-value registry |
| `bindkey` | Keybinding map |
| `setopt` | Option mask |
| `zmodload` | Loaded module set |
| `env` | Exported env vars (visible to subprocesses) |
| `params` | Non-exported shell parameters — scalar, array, assoc — this-shell-only |
| `theme` | Resolved theme templates (PROMPT, RPROMPT, palette) |
| `history` | Command history (with `--filter` for FTS query, `--range` for time range) |
| `entry_stats` | Frecency / call counts / total time |
| `subscriptions` | Active pub/sub subscriptions (this shell or `--all`) |
| `shells` | Live shell registry (same data as `zls`) |
| `plugins` | Installed plugins, deps, versions, enabled state |
| `shard <name>` | Specific rkyv shard contents |
| `index` | `index.rkyv` lookup table |
| `catalog` | Full `catalog.db` dump |
| `script <path>` | Bytecode for a cached `zshrs FILE` script |
| `sourced <path>` | Bytecode for a single sourced file (with `--all` for registry of every file ever sourced) |
| `compiled_files` | Full compiled_files table — every file the daemon has bytecode-cached, with kind, mtime, hash, sensitive flag, parent_paths |
| `zcompdump` | Synthetic `.zcompdump` for legacy tools (only valid as export, not view) |
| `daemon_state` | Full daemon state for debugging (sizes, queues, lock states, in-flight jobs) |

**Formats** (`--format <fmt>`):

| Format | Use | Valid targets |
|--------|-----|---------------|
| `sh` (default for `export` on shell-state targets) | Eval-compatible zsh script: `eval $(zcache export <target>)` resets overlay to canonical. Includes wipe prefix unless `--additive` | path/fpath/manpath/named_dir/aliases/galiases/saliases/functions/_comps/_services/_patcomps/_describe_handlers/zstyle/bindkey/setopt/zmodload/env/params/theme/command_hash/autoload_table |
| `text` (default for `view`) | Human-readable pretty-print | All targets |
| `json` | Machine-readable structured | All targets |
| `yaml` | Human + machine readable | All targets |
| `native` | rkyv zero-copy binary | All targets (default for `export` on binary-only targets: shard/index/catalog) |
| `sql` | SQL INSERT statements | catalog/entries/entry_stats/plugins/history |
| `csv` | Tabular | history/entry_stats/shells/plugins |
| `zcompdump` | Legacy zsh compinit format (byte-compatible) | compdef/_comps/_services/_patcomps/_describe_handlers (combined) |
| `disasm` | Disassembled bytecode (mnemonic + operands) | function/script/shard |

**Examples:**

```
zcache view path                          # pretty-print resolved $PATH, one dir per line with exists/missing status
zcache view command_hash --filter 'git*'  # commands matching glob, with executable paths
zcache view function _git --format disasm # disassembled bytecode for _git
zcache view history --filter 'cargo' --range 7d  # last 7 days of cargo commands
zcache view subscriptions --all           # every active subscription across every shell

zcache export path --format sh            # path=(...) suitable for sourcing
zcache export catalog --out ~/backup.db   # full catalog backup
zcache export shard zpwr --format json    # zpwr shard as JSON
zcache export zcompdump                   # legacy .zcompdump for plugin compat
zcache export daemon_state --format yaml  # full daemon state for bug reports
zcache export --all --out ~/zshrs-backup.tar.zst  # snapshot every cache target into one archive
```

**Import** (one-shot, limited):

```
zcache import zcompdump ~/.zcompdump      # ingest legacy compinit cache (migration assist)
zcache import catalog ~/backup.db         # restore catalog (loses entry_stats unless backup includes them)
zcache import shard <name> /path/to.rkyv  # restore specific shard
zcache import --all ~/zshrs-backup.tar.zst  # restore from full snapshot
```

Imports validate format + version before merging. Conflicts (incoming entry differs from current canonical) report a merge plan and require `--force` to override.

This makes `~/.cache/zshrs/` fully introspectable and portable. Every byte of canonical state can be exported in a format suited to the consumer — text for humans, JSON for scripts, sh for replayable backups, zcompdump for legacy compat, native rkyv for binary-fast portability. Diagnosing a misbehaving completion, comparing two users' caches, sharing a daemon-built fpath with a colleague, and migrating from zsh+zinit are all `zcache export` + `zcache import` operations.

### IPC wire format

Length-prefixed JSON over `~/.cache/zshrs/daemon.sock`. Each frame:

```
[4 bytes: u32 BE length] [length bytes: UTF-8 JSON]
```

Message envelope:

| Direction | Required keys | Notes |
|-----------|---------------|-------|
| client → daemon (handshake) | `hello: {version, client_pid, tty, cwd, argv0}` | First message after connect |
| daemon → client (handshake) | `welcome: {version, client_id, session_id, daemon_pid, daemon_uptime_ms}` | Or `err` on version mismatch |
| client → daemon (request)   | `id: u64`, `op: str`, `args: {…}` | `id` is monotonic per-connection |
| daemon → client (response)  | `id: u64`, `ok: bool`, payload-or-`err` | `id` echoes the request |
| daemon → client (async)     | `event: str`, payload | No `id`, fire-and-forget |

Conventions:

- All timestamps suffixed `_ns`, integer ns since epoch.
- All sizes suffixed `_bytes` or `_size`.
- Error shape: `{"err":{"code":"shard_locked","msg":"human-readable"}}` paired with `"ok":false`.
- Unknown op: `{"err":{"code":"unknown_op","msg":"unsupported by daemon vN"}}`.

Hot-path escape hatch: if JSON parse cost shows up in flamegraphs for `highlight` or `suggest` (per-keystroke ops), those opcodes can migrate to msgpack or fixed-layout binary while the rest of the protocol stays JSON for `socat`-style debuggability.

### Operation table (client → daemon)

| Op | Purpose |
|-----|---------|
| `info` | Daemon stats, shard info, in-flight jobs |
| `rebuild` | Enqueue compile job (full corpus or per-shard) |
| `clean` | Unlink + re-derive (per shard or whole corpus) |
| `verify` | Integrity scan on shards + catalog |
| `compact` | Vacuum catalog.db, dedup shards |
| `fpath_changed` | New paths added in user `.zshrc` |
| `stats_flush` | Batched runtime stats deltas merged into `entry_stats` |
| `subscribe_shard` | Push notification on shard update |
| `history_append` | Add command to `history.db` |
| `history_query` | FTS search; powers Ctrl-R, fc -l |
| `complete` | Tab completion enumeration (daemon eval, client paint) |
| `suggest` | Inline autosuggest from history frecency |
| `highlight` | Syntax-highlight current buffer |
| `keys` | Get key list for daemon-served special parameter (`_comps`, `_services`, etc.) |
| `load_script` | Cold-load `zshrs FILE`; returns shard path or inline bytecode |
| `source_resolve` | Resolve `source FILE` / `. FILE` to cached bytecode; daemon parses + caches on miss, returns shard path + generation |
| `push_canonical` | Promote client overlay state for a subsystem (path/fpath/alias/named_dir/etc.) into daemon canonical for future shells |
| `pull_canonical` | Client opt-in: re-fetch canonical state for a subsystem mid-session |
| `diff_canonical` | Get overlay-vs-canonical diff for inspection |
| `export_zcompdump` | Emit a synthetic `.zcompdump` from canonical state for legacy tooling (no `.zwc` emission) |
| `export_catalog` | Dump `catalog.db` to a portable file |
| `export_shard` | Dump a specific rkyv shard to a portable file |
| `import_zcompdump` | Ingest a legacy `.zcompdump` for migration assist |
| `register` | Implicit on connect; also tag/cwd updates |
| `list_shells` | Powers `zls` |
| `ping` | Liveness + roundtrip latency probe |
| `tag` / `untag` | Self-tag for routing |
| `send` | `zsend` dispatch (single, broadcast, by tag, by user) |
| `notify` | `znotify` OSC-9 / status-line message |
| `subscribe` / `unsubscribe` | `zsubscribe` glob pub/sub |
| `daemon` | Daemon control (status, stop, restart) |

### Async event types (daemon → client)

| Event | Trigger |
|-------|---------|
| `shard_updated` | Daemon swapped shard with newer generation |
| `rebuild_complete` | Async compile job finished |
| `canonical_changed` | Daemon canonical state for a subsystem (path / fpath / alias / named_dir / zstyle / bindkey / setopt / zmodload) was promoted by some client; subscribers can `zsync pull` if they want to track it mid-session |
| `match` | Pub/sub pattern matched |
| `cmd:execute` | `zsend` dispatch arrived |
| `notify` | `znotify` arrived |
| `daemon_shutdown` | Daemon going down (graceful, with grace period) |

### z\* builtin family (locked, no shadowing of zsh)

Every custom builtin uses `z` prefix. Build-time anti-collision check vs upstream zsh's z-namespace.

**zsh-owned z\* builtins (DO NOT shadow):**

```
zmv        zparseopts    zformat      zstat        zstyle      zprof
zcompile   zargs         zcurses      zsystem      ztie        zuntie
zselect    zsocket       zftp         zpty         zed         zcalc
zregexparse  zutil       zmodload     zle
```

**zshrs-owned z\* builtins** — all are length-prefixed JSON IPC wrappers around the daemon. ZERO local logic, ZERO background threads, ZERO polling, ZERO state in clients:

```
# Cache management
zcache                              # alias for `zcache info`
zcache info                         # daemon stats: shard sizes, entry counts, in-flight jobs
zcache jobs                         # list active compile jobs
zcache clean [--wait]               # regenerable only (preserves entry_stats)
zcache clean --all [--wait]         # everything (no prompt)
zcache clean shards [--wait]
zcache clean shard <name> [--wait]
zcache clean catalog [--wait]       # preserves entry_stats via dump+reimport
zcache clean catalog --no-stats     # loses entry_stats
zcache clean index [--wait]
zcache clean stats
zcache clean log
zcache rebuild [--wait]
zcache rebuild shard <name> [--wait]
zcache rebuild --parallel N
zcache verify                       # integrity scan + PRAGMA integrity_check on catalog.db
zcache compact [--wait]             # vacuum + dedup

# Universal cache dump/export/view — every named target is its own subcommand,
# accepting common flags: [--format <fmt>] [--filter <pat>] [--out <path>] [--all]
zcache view   <target> [flags]      # pretty-print to stdout (default --format text)
zcache export <target> [flags]      # serialize as eval-compatible zsh script to stdout (default --format sh)
zcache import <target> <path>       # ingest external file (stale-validated; --force to override)
zcache import zwc <path>            # on-demand .zwc ingest; daemon validates adjacent source freshness
zcache import zwc --tree <dir>      # walk dir, import every .zwc with fresh adjacent source
zcache import zcompdump <path>      # on-demand .zcompdump ingest; entries validated against current fpath
zcache list                         # list every supported export target

# CRITICAL: `zcache export` default output is eval-compatible. The canonical reset pattern is:
#     eval $(zcache export <target>)
# This restores the target's full canonical state into the current shell with no parser, no
# special importer, no intermediate format. Use case: get back to canonical starting state
# after session experimentation, without exec'ing a new shell (preserves $$, fds, cwd, history,
# job table). Whatever zsh syntax recreates the state, that's what `zcache export` emits.
#
# Default semantics include a wipe prefix so eval truly RESETS overlay back to canonical:
#     zcache export aliases        # emits: unalias -m '*'  followed by  alias foo='bar'  ...
#     zcache export _comps         # emits: unset _comps    followed by  typeset -gA _comps; _comps[git]=_git ...
#     zcache export bindkey        # emits: bindkey -d      followed by  bindkey '^A' beginning-of-line ...
# To suppress the wipe and emit additive-only:
#     zcache export aliases --additive
#
# Examples:
#     eval $(zcache export aliases)        # reset alias table to canonical
#     eval $(zcache export path)           # reset $PATH to canonical
#     eval $(zcache export named_dir)      # reset hash -d entries to canonical
#     eval $(zcache export functions)      # redefine every canonical function
#     eval $(zcache export _comps)         # reset completion handler dispatch to canonical
#     eval $(zcache export zstyle)         # reset all zstyle declarations
#     eval $(zcache export bindkey)        # reset keybindings
#     eval $(zcache export setopt)         # reset option mask
#     eval $(zcache export env)            # reset exported env vars (emits: export FOO=bar ...)
#     eval $(zcache export params)         # reset non-exported shell parameters (emits: typeset -g FOO=bar / typeset -ga ARR=(...) / typeset -gA MAP=(k v) per type)
#     eval $(zcache export --all-state)    # full shell-state reset (everything eval-compat in one go)
#                                          # — equivalent to `exec zshrs` minus the exec
#
# Targets that are NOT eval-compat (binary-only or inspection-only): shard, index, catalog,
# zcompdump, daemon_state, history, entry_stats, subscriptions, shells, plugins. For these,
# `zcache export` requires explicit --format native|json|yaml|sql|csv and refuses default-sh.

# Named export targets (each is a discoverable subcommand of `zcache export` and `zcache view`)
zcache export path                  # canonical $PATH
zcache export fpath                 # canonical $FPATH
zcache export manpath
zcache export infopath
zcache export cdpath
zcache export ld_library_path
zcache export named_dir             # hash -d table
zcache export command_hash          # command_name → executable_path
zcache export autoload_table        # function_name → file_path
zcache export aliases               # alias table
zcache export galiases              # global aliases (alias -g)
zcache export saliases              # suffix aliases (alias -s)
zcache export functions [<name>]    # all function bytecode, or one named function
zcache export _comps                # completion handler dispatch table
zcache export _services
zcache export _patcomps
zcache export _describe_handlers
zcache export zstyle                # zstyle context-pattern → key-value registry
zcache export bindkey               # keybinding map
zcache export setopt                # shell option mask
zcache export zmodload              # loaded module set
zcache export env                   # exported env vars (visible to subprocesses)
zcache export params                # non-exported shell parameters (scalar/array/assoc, this-shell-only)
zcache export theme                 # resolved theme templates
zcache export history [--filter <q>] [--range <r>]
zcache export entry_stats           # frecency / call counts / total time
zcache export subscriptions [--all]
zcache export shells                # live shell registry (same as zls)
zcache export plugins               # installed plugins, deps, versions
zcache export shard <name>          # specific rkyv shard
zcache export index                 # index.rkyv lookup table
zcache export catalog               # full catalog.db
zcache export script <path>         # bytecode for a cached `zshrs FILE`
zcache export sourced <path>        # bytecode for a sourced file (single)
zcache export sourced --all         # registry of every sourced file with mtime/hash/sensitive flag
zcache export compiled_files        # full compiled_files table dump
zcache export zcompdump             # synthetic .zcompdump for legacy tools
zcache export daemon_state          # full daemon state for debugging
zcache export --all [--out <path>]  # snapshot every target into one archive

# `zcache view` has identical target surface, default format = text:
zcache view path
zcache view aliases
zcache view _comps --filter 'git*'
zcache view function _git --format disasm
zcache view history --filter 'cargo' --range 7d
# … etc, every export target is also a view target

zcache daemon status                # is daemon running, pid, uptime, RSS
zcache daemon stop                  # graceful shutdown
zcache daemon restart               # graceful + respawn

# Shell registry (cross-shell coordination)
zls                                 # list active shells: id, pid, tty, cwd, tags, login_time
zls --tag <name>                    # filter by tag
zls --user <user>                   # filter by user (root only for cross-user)
zid                                 # print this shell's daemon-assigned id
zping                               # daemon liveness + roundtrip latency
zping --all                         # ping every registered shell
ztag <name…>                        # self-tag this shell (multiple tags allowed)
zuntag <name…>                      # remove tags
zuntag --all                        # remove all tags

# Cross-shell dispatch
zsend <shell_id> <cmd…>             # dispatch command to one shell
zsend --all <cmd…>                  # broadcast
zsend --tag <name> <cmd…>           # dispatch by tag
zsend --user <user> <cmd…>          # cross-user (root only)
zsend --wait <shell_id> <cmd…>      # block on completion + capture output
zsend --json <shell_id> <cmd…>      # return structured result

# Notifications (status-line / OSC-9 / queued if shell busy)
znotify <shell_id> <msg…>
znotify --all <msg…>
znotify --tag <name> <msg…>
znotify --urgency <low|normal|critical> <shell_id> <msg…>

# Pub/sub
zsubscribe <pattern>                # e.g. shell:42.commands, *.commands, tag:prod.chpwd
zunsubscribe <pattern>
zsubscribe --list                   # show this shell's active subscriptions
zsubscribe --pause                  # mute deliveries without dropping subscriptions
zsubscribe --resume

# Job supervision (planned: session-persistent jobs)
zjob submit <cmd…>                  # detached, supervised, survives shell exit
zjob list                           # this user's running supervised jobs
zjob status <job_id>
zjob output <job_id>                # tail captured stdout/stderr
zjob wait <job_id>                  # block on completion
zjob cancel <job_id>                # SIGTERM, then SIGKILL after grace period
zjob attach <job_id>                # foreground attach

# State promotion (overlay → daemon canonical for future shells)
zsync up <subsystem> [name…]        # promote local overlay to canonical (path/fpath/named_dir/alias/function/compdef/env/zstyle/bindkey/setopt/zmodload)
zsync up --all                      # promote everything
zsync diff [subsystem]              # show overlay-vs-canonical
zsync watch <subsystem…>            # subscribe to canonical_changed events
zsync pull <subsystem>              # opt-in mid-session refresh from canonical (breaks snapshot rule on user request)

# Daemon log inspection
zlog                                # default: live tail of ~/.cache/zshrs/zshrs.log
zlog tail [-n N] [--follow]
zlog grep <pattern> [--rotated]
zlog level [<new_level>] [<module=level>…]
zlog clear
zlog rotate
zlog path
zlog stats
```

Subscription pattern grammar: `<scope>.<topic>`.

**Scopes:** `shell:<id>` | `tag:<name>` | `user:<name>` (root only) | `*`
**Topics:** `commands` | `chpwd` | `prompt` | `precmd` | `preexec` | `exit` | `signal` | `error` | `cd_history` | `aliases_changed` | `tagged` | `untagged`

Examples:

- `zsubscribe shell:42.commands` — pair-programming, audit
- `zsubscribe *.commands` — fleet-wide command logging
- `zsubscribe tag:prod.chpwd` — track cwd of all prod shells
- `zsubscribe shell:1.chpwd` — mirror cwd from shell #1 in this shell

### Three personality modes (cache layer gated by mode)

| Mode | Trigger | Cache | Daemon |
|------|---------|-------|--------|
| POSIX | `--posix`, `emulate sh`, argv[0] = `sh`/`dash`/`bash` | OFF | NEVER spawned |
| Vanilla zsh | argv[0] = `zsh`, `--zsh-compat` | OFF | NEVER spawned |
| Turbocharged zshrs | argv[0] = `zshrs`, default | ON | spawned by first client |

POSIX mode never spawns the daemon, never creates `~/.cache/zshrs/`. Required for `/bin/sh → zshrs` symlink in containers / cron / init / shebang.

### Daemon lifecycle

- **Spawn-on-demand:** first client checks for `daemon.sock`; if absent or unresponsive, fork-spawns `zshrs --daemon`, waits ~50ms, retries connect.
- **Singleton enforcement:** daemon takes `flock(LOCK_EX)` on `daemon.pid` at startup. Second instance sees lock held, exits.
- **Lifetime:** persists across shell sessions; survives logout. Killed only by explicit `zcache daemon stop` or `pkill zshrs-daemon`.
- **Crash recovery:** if daemon dies, next client to fail socket connect kills stale pidfile and respawns. No state loss — rkyv shards and `catalog.db` are durable on disk.
- **Degraded mode:** if daemon disabled or unreachable, clients fall back to source-interp for everything. Cache stops updating but shells stay functional. User never blocked.

### First-run user notification (the one-time exception to no-banner)

The global "no startup banner / no init progress to terminal" rule has exactly **one** exception: the first-ever zshrs invocation on a machine, when the daemon is being spawned for the first time and the cold-cache build is starting from zero. This is a multi-second-to-multi-minute operation depending on corpus size; running it silently would be confusing and potentially indistinguishable from a hung shell.

**Detection:** "first-ever run" = no `~/.cache/zshrs/daemon.pid` AND no `~/.cache/zshrs/index.rkyv` AND no `~/.cache/zshrs/images/` shards on disk. After the first run completes, this branch is never taken again on this machine for this user.

**What gets printed (stderr, single block, before first prompt):**

```
zshrs first-run init — daemon spawning, cold cache building.
  scope: ~/.zshrc + transitive sources + $PATH + $FPATH + plugins
  estimated: 579 files, ~1.6M LOC, ~60s on this machine
  background: shells work via source-interp until cache is warm
  log:        ~/.cache/zshrs/zshrs.log
  inspect:    zcache info | zcache jobs | zcache view <target>
  reset:      zcache clean | rm -rf ~/.cache/zshrs/
```

Six lines, factual, no welcome / no congratulations / no emoji / no version stripe. After this block prints, the prompt appears immediately. Daemon continues building in the background; clients run via source-interp fallback until shards atomic-rename in.

**Completion notice:** when the daemon finishes the cold build, it can emit a single-line `znotify` to the originating shell: `daemon ready — future shells <10ms cold-start (took 47s, 17042 entries)`. The user-visible status-line update is a `znotify` not a stdout/stderr write, so it doesn't disrupt whatever the user is currently doing.

**Subsequent runs**: silent. Per the CLAUDE.md global rule, no banner, no progress, no chatter. The first-run notification is one-shot for this user/machine pair, gated by the on-disk first-run-detection check.

**Override flags** (for users who want first-run silent or who want every-run verbose):

- `--quiet-first-run` (or env `ZSHRS_QUIET_FIRST_RUN=1`): suppress the first-run notification block. Everything still goes to log; user just sees an immediate prompt.
- `--verbose-init` (debug-only): show daemon work to stderr on every run, not just first. For testing daemon behavior; not recommended for daily use.

### Long-running command completion notices

Daemon tracks command duration via the `history_append` IPC (clients send `duration_ns` per command). When a command exceeds the long-cmd threshold and completes, daemon pushes a `long_cmd_complete` event to the user's other registered shells. Original shell already knows it finished (it just got control back); the value is alerting *other* tmux panes / ssh sessions where the user is doing parallel work.

**Threshold:** default 30 seconds. Configurable via `ZSHRS_LONG_CMD_THRESHOLD=<seconds>` env var or `zcache config set long_cmd_threshold <seconds>` runtime override. Per-shell overrides via `ZSHRS_LONG_CMD_THRESHOLD` set before shell launch.

**Event payload (`long_cmd_complete`):**

```json
{"event":"long_cmd_complete",
 "from_shell":42,
 "command":"cargo build --release",
 "exit_code":0,
 "duration_ns":492137000000,
 "cwd":"/Users/wizard/RustroverProjects/zshrs",
 "ts_ns":1735305600000000000}
```

**Routing:** by default delivered to all of the user's currently-registered shells *except* the originating shell. Subscribers can refine via patterns: `zsubscribe *.long_cmd_complete` (any shell), `zsubscribe shell:42.long_cmd_complete` (shell 42 only), `zsubscribe tag:dev.long_cmd_complete` (only dev-tagged shells), `zsubscribe --filter 'duration_ns > 600e9' *.long_cmd_complete` (only commands over 10 min).

**Client rendering:** receiving shell renders the event via the existing `znotify` channel (OSC-9 + status-line update). User sees `[shell:42 ✓ cargo build (8m12s)]` in their other shell's status bar without losing focus on what they were doing.

**Companion events** for richer awareness (same routing rules):

- `long_cmd_started` — fires when a command crosses 5s of runtime (not waiting for completion); useful for "I started something heavy, expect it to take a while" pre-warning.
- `long_cmd_failed` — fires on non-zero exit when duration exceeds threshold; same payload + `stderr_tail` field with the last N lines of stderr captured by daemon's command-output ring buffer.
- `long_cmd_signaled` — fires when a long command exits via signal (SIGINT, SIGTERM, SIGKILL).

**Disable:** `ZSHRS_LONG_CMD_NOTICES=0` for users who don't want any of this. Default on.

### `zask` — daemon-queued UI primitives (pull-mode, never interferes with prompt)

> **Naming note:** `zask` (not `zui`) — `zui` is taken by [`zdharma-continuum/zui`](https://github.com/zdharma-continuum/zui), an existing zsh TUI library plugin. Mnemonic: `zask` = the daemon (or another shell) **asks** the user something.

**Hard rule: daemon-pushed UI never interferes with the user's active prompt.** No auto-overlay, no key capture, no cursor moves, no inline interruption. Cross-shell scripts that need user input from another shell don't get to take over that shell's prompt unannounced.

Instead: daemon-pushed UI is **queued**. The user is *notified* (status-line / OSC-9 / unobtrusive bell) that a UI request is pending; the user explicitly activates it on their own time via a keybinding or `zask take`. Until activated, the prompt is untouched and the user keeps typing.

**Flow:**

1. Some script / shell / daemon-job pushes a UI request: `zask --target shell:42 picker --items "..."`.
2. Daemon enqueues the request in shell:42's pending-UI inbox + pushes a `ask:pending` event.
3. shell:42's status-line shows `[zask:1 pending]` (count of queued requests). OSC-9 fires for terminal-native notification. No prompt change, no cursor move, no input capture.
4. User finishes whatever they're typing. When ready, they hit the configured activation key (default `Ctrl-X q`, vi-mode-compatible) or type `zask take`.
5. shell:42 then renders the next pending UI element — but only after the user explicitly engaged. The picker / input / dialog draws above the prompt, captures keystrokes, returns the response. User-initiated, not daemon-imposed.
6. Response routes back over IPC to the originating shell / script.

**Push events (daemon → target client) — informational only, never auto-render:**

| Event | Payload | Client behavior |
|-------|---------|-----------------|
| `ask:pending` | `request_id, kind, from_shell, summary, urgency` | Increment status-line counter, optional OSC-9, write to log. NEVER render UI. |
| `ask:dismissed` | `request_id, reason` | Decrement counter, clear status-line if count==0 |
| `ask:progress` | `request_id, label, percent, eta_ms` | Update status-line bar (which is already a passive surface, doesn't touch prompt). Allowed to update in place because status-line is reserved space, not the prompt line |

UI rendering events (`ask:picker`, `ask:input`, `ask:dialog`, `ask:menu`) are **only sent to the client when the user explicitly takes the request** via `zask take`. The daemon stages the rendering payload in the inbox; the actual render-event ships only when client signals readiness.

**Builtin: `zask`** (top-level, thin IPC wrapper):

```
# Push side — script asks daemon to queue a UI request on a target shell:
zask --target <shell|tag|*> picker --items <list>
zask --target shell:42 picker --items "$(ls)" --multi
zask --target tag:operator input --prompt "username: "
zask --target shell:7 input --prompt "password: " --secret
zask --target shell:42 dialog --message "Deploy to prod?" --options yes,no --urgency critical
zask --target shell:42 menu --title "Select host" --items "host-1 host-2 host-3"

# Progress is the one passive exception (status-line only, no key capture):
zask progress --target shell:42 --label "Building" --percent 47 --eta 30000
zask progress --target shell:42 --request-id <id> --percent 78    # update in place
zask progress --target shell:42 --request-id <id> --done

# Pull side — user manages their own inbox:
zask pending                                  # list queued requests in this shell
zask take                                     # render the oldest pending; blocks for response
zask take <id>                                # render a specific pending request by id
zask dismiss [<id>|--all]                     # decline/cancel pending request(s); originator gets cancelled=true
zask inbox-clear                              # dismiss every pending request in this shell at once
```

**Activation keybinding:** `Ctrl-X q` by default — chained Ctrl-prefix (Ctrl-X then q for "queue"), works identically in vi-insert, vi-cmd, and emacs-insert modes. Chosen to avoid colliding with zsh's existing `^Xu` (undo) and other established `^X*` bindings. **No default keybindings ever use Meta / Alt** — meta keys are unreliable across terminals (macOS Option-key special chars, tmux pass-through quirks, ssh meta-bit stripping, locale-dependent escape sequences). Ctrl combinations and chained-Ctrl chords pass through every terminal cleanly. Bound via `bindkey '^Xq' zask-take` shipped in zshrs's default keymap, plus `bindkey -M vicmd '^Xq' zask-take` so it works post-Esc too. User overrides via `bindkey '<key>' zask-take` or env var `ZSHRS_ZASK_TAKE_KEY=^G` to remap. The widget activates one pending request at a time. Status-line counter decrements on take or dismiss.

**Status-line presentation:** the only daemon→client UI surface that updates without user activation is the status-line / RPROMPT region — explicitly designed-as-passive area. Format: `[zask:N urgent:M]` where N is total pending and M is critical-urgency count. Updates in place, no prompt-line touch. Client treats this as a watched parameter that triggers a status-line redraw, not a prompt redraw.

**Out-of-band rendering (planned):** integration hooks for tmux status-line, kitty's status bar, alacritty title-bar updates, and OSC-9 → macOS Notification Center / Linux libnotify. Daemon emits the same `ask:pending` event; client routes to whichever passive surface the user has configured. None of these touch the active prompt.

**Use cases (revised under the no-prompt-interference rule):**

- **Cross-shell wizard:** script in shell A pushes `zask --target shell:B picker ...`. Shell B's status-line shows `[zask:1 pending]`. User in shell B finishes their current line, hits `Ctrl-X q`, picks. Answer routes back to A. Script in A blocks until user gets around to it (or times out).
- **Pair programming intervention:** remote operator pushes a `ask:dialog`. Local user's status-line shows `[zask:1 urgent:1]`. User finishes typing, hits `Ctrl-X q`, sees the dialog, picks yes/no.
- **Background-job confirmation:** `zjob` supervisor pushes a `ask:dialog`. Status-line surfaces it. User answers when convenient (or `zjob` times out and aborts).
- **`zsync up` conflict resolution:** queued for the originating shell with summary `function foo conflict — file changed since push`. User takes when ready.
- **Long-running ops with progress:** the only UI event allowed to live-update the status-line. `zask progress` updates the status bar without ever touching the prompt.

**Timeouts:** every queued UI request has a default 60-minute timeout. Originator gets `ask:timeout` event if user never engages. Configurable per-request via `--timeout <seconds>` or `--no-timeout`.

**Visibility model:** `zask pending` shows this shell's queue. `zls --ask-pending` shows pending requests across all of the user's registered shells. Every push, take, dismiss, timeout logged to `~/.cache/zshrs/zshrs.log`.

### Daemon logging (every action goes to logfile)

Daemon is fully observable via `~/.cache/zshrs/zshrs.log`. Every action it takes — every cache build, every fsnotify event, every IPC op handled, every shard rename, every error, every plugin discovery, every cross-shell dispatch — is logged. The log is the canonical record of "what did the daemon do" for debugging, post-mortem analysis, and behavior verification.

**What gets logged at INFO level (default):**

- Daemon start / stop / restart with PID + version + config
- First-init begin / end with corpus size + duration
- Cache bust commands received + scope + duration
- Per-shard rebuild jobs: trigger, source paths walked, entries written, atomic-rename complete, generation bumped
- fsnotify events received: path, kind (create/modify/delete/rename), affected shard, action taken
- Client connect / disconnect: client_id, pid, tty, cwd, session_id
- All IPC ops handled: op name, args summary, response code, duration
- `zsync up` canonical promotions: subsystem, value summary, originating client
- `zsend` / `znotify` dispatches: from, to, command/message, delivery status
- Subscription matches: pattern, scope, topic, recipient_count
- Integrity check results from `zcache verify`
- Log rotation events (when rotating to `zshrs.log.1`, file sizes)

**Levels:**

- `ERROR` — daemon-level failures (rebuild crash, lock contention timeout, irrecoverable corruption)
- `WARN` — recoverable issues (stale `.zwc` skipped on import, fsnotify queue overflow, slow IPC client, malformed message dropped)
- `INFO` — default, all the bullet points above
- `DEBUG` — internal state transitions, individual file parses, hash-table slot writes (high volume)
- `TRACE` — every fn entry/exit (extreme volume; for bug repro only)

**Configuration:**

- Default level: `INFO`
- Override via `ZSHRS_LOG=debug` env var or `--log-level <level>` flag at daemon spawn
- Per-module override: `ZSHRS_LOG=info,fsnotify=debug,ipc=trace` (tracing-subscriber compatible)

**Format:** structured `tracing` output. Each line: `[ISO-8601 timestamp] [LEVEL] [module] message {key=value, key=value, …}`. Compatible with `tail -f`, `grep`, and any log-aggregation pipeline. Optional `--log-format json` for structured ingestion.

**Rotation:** ticker rotates `zshrs.log` to `zshrs.log.1` when size hits 10 MB (configurable via `ZSHRS_LOG_MAX_BYTES`). Up to 4 rotated copies kept (`.1` through `.4`); `.4` is purged when `.3` rotates in. Total disk footprint capped at ~50 MB.

**Top-level builtin: `zlog`.** Dedicated builtin for log inspection — top-level for discoverability since this is a daily-use operation:

```
zlog                                # default: zlog tail --follow (live tail of current log)
zlog tail [-n N]                    # tail of current log (default: last 100 lines)
zlog tail --follow                  # live tail, streams new entries as daemon writes
zlog grep <pattern> [--rotated]     # ripgrep current log; --rotated includes .1-.4 archives
zlog level [<new_level>]            # show or set runtime log level (no daemon restart)
zlog level fsnotify=debug,ipc=trace # per-module overrides at runtime
zlog clear                          # truncate current log (rotated copies preserved)
zlog rotate                         # force a rotation now (don't wait for size threshold)
zlog path                           # print absolute path to current log file
zlog stats                          # daemon log self-stats: line counts, size, errors-since-start
```

`zlog` is a thin IPC wrapper; daemon does the file IO and tailing. Client never opens the log file directly. The `zcache log <verb>` form remains as an alias for `zlog <verb>` so `zcache *` discoverability stays intact, but `zlog` is the canonical surface.

### Hard invariants (rejected proposal classes)

- ANY client-side polling loop, timer, fsnotify watcher, SQLite handle for cache — REJECT.
- ANY client-side data-structure walk over rkyv contents — REJECT.
- ANY client-side write to a daemon-owned file — REJECT.
- ANY second daemon instance — REJECT (singleton via `flock` on `daemon.pid`).
- ANY plugin / compsys bytecode baked into the zshrs binary `.text` — REJECT (working set must scale with what's called).
- ANY mandatory daemon (no source-truth fallback) — REJECT.
- ANY daemon spawn under POSIX mode — REJECT.
- ANY z\* builtin without `z` prefix or that shadows upstream zsh — REJECT.
- ANY hydration progress on stderr/stdout — REJECT (`tracing::info!` to log file only).
- ANY scattered per-plugin cache files outside `~/.cache/zshrs/images/` — REJECT.
- ANY removal of `entry_stats` to "simplify" — REJECT.
- ANY auto-consumption of `.zwc` / `.zcompdump` files on daemon scans, fpath walks, fsnotify watches, or plugin-tree enumeration — REJECT. They're invisible to all automatic discovery; only `zcache import zwc|zcompdump <path>` (user-explicit, freshness-validated) may ingest them.
- ANY periodic re-walk of `$PATH` / `$FPATH` / plugin trees / source-statement targets by the daemon — REJECT. Walks happen exactly twice in daemon's life: first init (cold cache) and explicit cache bust (`zcache clean` / `rebuild`). Steady state is fsnotify-driven incremental updates only. No polling, no cron, no "every 5 minutes refresh."
- ANY default keybinding that depends on Meta / Alt — REJECT. Meta keys behave unpredictably across terminals (macOS Option-key chars, tmux pass-through quirks, ssh meta-bit stripping, locale escape sequences). Ctrl-prefixed bindings only for shipped defaults; users can opt into Meta bindings explicitly via `bindkey` if their setup tolerates it.

### Acceptance criteria

- Cold client launch (daemon already running): <5ms (mmap + connect + handshake).
- Cold client launch (daemon spawn-on-demand): <50ms (spawn + connect + handshake).
- Tab completion lookup: ~150-200ns end-to-end (perfect-hash mmap dereference).
- Inline autosuggest: <2ms IPC roundtrip including FTS query.
- Syntax highlight per keystroke: <2ms IPC roundtrip including parse.
- 100 parallel clients share <30 MB RSS attributable to images (page-cache shared across mmaps).
- Per-client cache overhead: <5 MB.
- Full-corpus rebuild via `zcache rebuild`: <30s clean.
- Per-shard rebuild: ~100-500ms small, ~3-5s large.
- POSIX mode: never spawns daemon, never creates `~/.cache/zshrs/`.
- `~/.zshrc` cold-source: <50ms with cache hit (mmap + replay env log), regardless of file size.
- `zshrs FILE` cold-launch with cache hit: <10ms.