zidane 6.6.3 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/dist/{acp-BQqhozmt.js → acp-Wr8l4IDI.js} +7 -180
  2. package/dist/acp-Wr8l4IDI.js.map +1 -0
  3. package/dist/acp-cli.js +6 -6
  4. package/dist/acp.d.ts +1 -1
  5. package/dist/acp.d.ts.map +1 -1
  6. package/dist/acp.js +2 -1
  7. package/dist/{agent-DTxH7RHW.js → agent-C-camKQW.js} +162 -12
  8. package/dist/agent-C-camKQW.js.map +1 -0
  9. package/dist/agent.d.ts +1 -1
  10. package/dist/agent.js +1 -1
  11. package/dist/{anthropic-DRNRnd6W.js → anthropic-BDOGzhtj.js} +3 -3
  12. package/dist/{anthropic-DRNRnd6W.js.map → anthropic-BDOGzhtj.js.map} +1 -1
  13. package/dist/{transcript-anchors-BjH-68k2.js → approvals-CqqxE5Vh.js} +4924 -275
  14. package/dist/approvals-CqqxE5Vh.js.map +1 -0
  15. package/dist/{auth-7rv6ZfWk.js → auth-BdwDROYg.js} +12 -8
  16. package/dist/{auth-7rv6ZfWk.js.map → auth-BdwDROYg.js.map} +1 -1
  17. package/dist/chat/pure.d.ts +221 -4
  18. package/dist/chat/pure.d.ts.map +1 -0
  19. package/dist/chat/pure.js +4 -2
  20. package/dist/chat.d.ts +6 -2911
  21. package/dist/chat.js +8 -261
  22. package/dist/{context-transport-D8yeGdpd.js → context-transport-BqYwzJYE.js} +2 -2
  23. package/dist/{context-transport-D8yeGdpd.js.map → context-transport-BqYwzJYE.js.map} +1 -1
  24. package/dist/contexts/daytona.d.ts +2 -1
  25. package/dist/contexts/daytona.d.ts.map +1 -1
  26. package/dist/contexts/daytona.js +14 -3
  27. package/dist/contexts/daytona.js.map +1 -1
  28. package/dist/contexts/e2b.d.ts +1 -1
  29. package/dist/contexts/e2b.js +1 -1
  30. package/dist/daemon-cli.d.ts +1 -0
  31. package/dist/daemon-cli.js +90 -0
  32. package/dist/daemon-cli.js.map +1 -0
  33. package/dist/eval.d.ts +1 -1
  34. package/dist/eval.js +1 -1
  35. package/dist/extensions.d.ts +2 -2
  36. package/dist/extensions.js +2 -2
  37. package/dist/headless.d.ts +1 -1
  38. package/dist/headless.js +4 -4
  39. package/dist/{index-D0O-QO7Z.d.ts → index-B-FSaM3_.d.ts} +2863 -2375
  40. package/dist/index-B-FSaM3_.d.ts.map +1 -0
  41. package/dist/index-DgdzXRPH.d.ts +3590 -0
  42. package/dist/index-DgdzXRPH.d.ts.map +1 -0
  43. package/dist/index.d.ts +2 -2
  44. package/dist/index.js +14 -1179
  45. package/dist/ipc-CIv87xuH.js +331 -0
  46. package/dist/ipc-CIv87xuH.js.map +1 -0
  47. package/dist/ipc.d.ts +849 -0
  48. package/dist/ipc.d.ts.map +1 -0
  49. package/dist/ipc.js +4 -0
  50. package/dist/json-rpc-rSWMUh9b.js +192 -0
  51. package/dist/json-rpc-rSWMUh9b.js.map +1 -0
  52. package/dist/{logger-DBX9uYOw.js → logger--C1VQjwJ.js} +3 -13
  53. package/dist/logger--C1VQjwJ.js.map +1 -0
  54. package/dist/logs-view-CWG8aUAG.js +44 -0
  55. package/dist/logs-view-CWG8aUAG.js.map +1 -0
  56. package/dist/{mcp-CVtSQTWg.js → mcp-BIWa57eY.js} +3 -3
  57. package/dist/{mcp-CVtSQTWg.js.map → mcp-BIWa57eY.js.map} +1 -1
  58. package/dist/mcp.d.ts +1 -1
  59. package/dist/mcp.js +1 -1
  60. package/dist/{messages-CZMUuKzo.js → messages-21XfHxyF.js} +3 -3
  61. package/dist/{messages-CZMUuKzo.js.map → messages-21XfHxyF.js.map} +1 -1
  62. package/dist/{openai-compat-BoTi-9fd.js → openai-compat-DY-AKxCg.js} +2 -2
  63. package/dist/{openai-compat-BoTi-9fd.js.map → openai-compat-DY-AKxCg.js.map} +1 -1
  64. package/dist/output/stream-json.d.ts +1 -1
  65. package/dist/output/terminal.d.ts +1 -1
  66. package/dist/{plugins-onDvTHEm.js → plugins-B531Tz5b.js} +2 -2
  67. package/dist/{plugins-onDvTHEm.js.map → plugins-B531Tz5b.js.map} +1 -1
  68. package/dist/plugins.d.ts +1 -1
  69. package/dist/plugins.js +1 -1
  70. package/dist/presets.d.ts +1 -1
  71. package/dist/presets.js +1 -1
  72. package/dist/providers/anthropic.d.ts +1 -1
  73. package/dist/providers/anthropic.js +1 -1
  74. package/dist/providers/arcee.d.ts +1 -1
  75. package/dist/providers/arcee.js +1 -1
  76. package/dist/providers/baseten.d.ts +1 -1
  77. package/dist/providers/baseten.js +1 -1
  78. package/dist/providers/cerebras.d.ts +1 -1
  79. package/dist/providers/cerebras.js +1 -1
  80. package/dist/providers/local.d.ts +1 -1
  81. package/dist/providers/local.js +1 -1
  82. package/dist/providers/openai-compat.d.ts +1 -1
  83. package/dist/providers/openai-compat.js +1 -1
  84. package/dist/providers/openai.d.ts +1 -1
  85. package/dist/providers/openai.js +3 -3
  86. package/dist/providers/openrouter.d.ts +1 -1
  87. package/dist/providers/openrouter.js +1 -1
  88. package/dist/providers/xai.d.ts +1 -1
  89. package/dist/providers/xai.js +1 -1
  90. package/dist/{providers-CmhdtD7w.js → providers-DX91jwir.js} +2 -2
  91. package/dist/{providers-CmhdtD7w.js.map → providers-DX91jwir.js.map} +1 -1
  92. package/dist/providers.d.ts +1 -1
  93. package/dist/providers.js +3 -3
  94. package/dist/restate.d.ts +1 -1
  95. package/dist/restate.js +8 -8
  96. package/dist/restate.js.map +1 -1
  97. package/dist/server-DJqp9dIR.js +1317 -0
  98. package/dist/server-DJqp9dIR.js.map +1 -0
  99. package/dist/session/sqlite.d.ts +1 -1
  100. package/dist/session/sqlite.d.ts.map +1 -1
  101. package/dist/session/sqlite.js +42 -4
  102. package/dist/session/sqlite.js.map +1 -1
  103. package/dist/{session-CyRLSsVa.js → session-K3icbPH9.js} +8 -2
  104. package/dist/session-K3icbPH9.js.map +1 -0
  105. package/dist/session.d.ts +1 -1
  106. package/dist/session.js +2 -2
  107. package/dist/{skills-BMGH_KK1.js → skills-PJyiSRCp.js} +3 -21
  108. package/dist/skills-PJyiSRCp.js.map +1 -0
  109. package/dist/skills.d.ts +1 -1
  110. package/dist/skills.js +1 -1
  111. package/dist/src-DzzhE2YO.js +2461 -0
  112. package/dist/src-DzzhE2YO.js.map +1 -0
  113. package/dist/todo-edits-Dw2oO2ON.js +378 -0
  114. package/dist/todo-edits-Dw2oO2ON.js.map +1 -0
  115. package/dist/{tool-formatters-NrzD6Fkk.d.ts → tool-formatters-DO56YBBw.d.ts} +104 -4
  116. package/dist/tool-formatters-DO56YBBw.d.ts.map +1 -0
  117. package/dist/tools/fetch-url.d.ts +1 -1
  118. package/dist/tools/web-search.d.ts +1 -1
  119. package/dist/tools.d.ts +1 -1
  120. package/dist/tools.js +1 -1
  121. package/dist/{transcript-anchors-UhC58Tmj.d.ts → transcript-anchors-DXqWWGbq.d.ts} +53 -4
  122. package/dist/transcript-anchors-DXqWWGbq.d.ts.map +1 -0
  123. package/dist/tui.d.ts +94 -13
  124. package/dist/tui.d.ts.map +1 -1
  125. package/dist/tui.js +2241 -1009
  126. package/dist/tui.js.map +1 -1
  127. package/dist/{turn-operations-mMxDRgLY.js → turn-operations-CZLsNgw-.js} +896 -11
  128. package/dist/turn-operations-CZLsNgw-.js.map +1 -0
  129. package/dist/{turn-operations-Cuc9pwiB.d.ts → turn-operations-DKFYWP3O.d.ts} +501 -4
  130. package/dist/turn-operations-DKFYWP3O.d.ts.map +1 -0
  131. package/dist/{types-BIarq1qE.js → types-CWgENVLY.js} +24 -2
  132. package/dist/types-CWgENVLY.js.map +1 -0
  133. package/dist/types.d.ts +1 -1
  134. package/dist/types.js +1 -1
  135. package/dist/xml-D5HBhJle.js +22 -0
  136. package/dist/xml-D5HBhJle.js.map +1 -0
  137. package/docs/CHAT.md +39 -0
  138. package/docs/IPC.md +420 -0
  139. package/docs/TUI.md +62 -0
  140. package/package.json +8 -2
  141. package/dist/acp-BQqhozmt.js.map +0 -1
  142. package/dist/agent-DTxH7RHW.js.map +0 -1
  143. package/dist/chat.d.ts.map +0 -1
  144. package/dist/chat.js.map +0 -1
  145. package/dist/index-D0O-QO7Z.d.ts.map +0 -1
  146. package/dist/index.js.map +0 -1
  147. package/dist/inline-autocomplete-D0MPvBaS.js +0 -596
  148. package/dist/inline-autocomplete-D0MPvBaS.js.map +0 -1
  149. package/dist/logger-DBX9uYOw.js.map +0 -1
  150. package/dist/login-CB23LyI1.js +0 -707
  151. package/dist/login-CB23LyI1.js.map +0 -1
  152. package/dist/session-CyRLSsVa.js.map +0 -1
  153. package/dist/skills-BMGH_KK1.js.map +0 -1
  154. package/dist/tool-formatters-NrzD6Fkk.d.ts.map +0 -1
  155. package/dist/transcript-anchors-BjH-68k2.js.map +0 -1
  156. package/dist/transcript-anchors-UhC58Tmj.d.ts.map +0 -1
  157. package/dist/turn-operations-Cuc9pwiB.d.ts.map +0 -1
  158. package/dist/turn-operations-mMxDRgLY.js.map +0 -1
  159. package/dist/types-BIarq1qE.js.map +0 -1
package/docs/IPC.md ADDED
@@ -0,0 +1,420 @@
1
+ # zidane/ipc — shared chat host, daemon, and multi-window sync
2
+
3
+ `zidane/ipc` is the layer that lets several zidane windows — terminal
4
+ sessions, the GUI, `zidane attach` clients — share **one set of live agents**
5
+ and stay converged on the same state: the same transcript stream, the same
6
+ pending approvals, the same interaction prompts.
7
+
8
+ ```
9
+ ┌────────────┐ ┌────────────┐ ┌─────────────┐
10
+ │ zidane TUI │ │ zidane GUI │ │zidane attach│ clients (presentation only)
11
+ └─────┬──────┘ └─────┬──────┘ └──────┬──────┘
12
+ │ JSON-RPC over │ unix socket │ (or Electron IPC for the GUI)
13
+ └───────────┬────┴─────────────────┘
14
+ ┌─────▼──────┐
15
+ │ ChatHost │ createChatHost(env) — owns live Agents,
16
+ │ (daemon or │ safe-mode gate, interactions, compaction,
17
+ │ GUI main) │ work tracking, todos
18
+ └─────┬──────┘
19
+ ┌─────▼──────┐
20
+ │ SessionStore│ shared ~/.zidane (sessions.db, credentials,
21
+ │ + settings │ safelist, state.json)
22
+ └────────────┘
23
+ ```
24
+
25
+ ## The pieces
26
+
27
+ | Module | What it is |
28
+ | --- | --- |
29
+ | `protocol.ts` | Transport contract: the typed `IpcMethods` RPC map, `IPC_PROTOCOL_VERSION`, handshake shapes. Re-exports the host-domain shapes (`HostSignal` & co.) from `src/chat/host/signals.ts`. |
30
+ | `src/chat/host/` | `createChatHost(env)` — the transport-agnostic engine. Owns live `Agent`s, forwards agent-loop hooks as signals, runs the approval + interaction hubs. Lives under `src/chat` (it's the chat engine, not an IPC feature); `zidane/ipc` re-exports it for back-compat, new code imports it from `zidane/chat`. |
31
+ | `daemon/` | Unix-socket JSON-RPC server (`startIpcDaemon`), disk-backed environment, lifecycle helpers (socket path, liveness probe, spawn-on-demand). |
32
+ | `client.ts` | `connectIpcClient` / `connectOrSpawnDaemon` — typed `request()`, `onSignal()`, protocol-version handshake with automatic stale-daemon drain. |
33
+ | `attach.ts` | `zidane attach` — a thin readline client proving the multi-window contract end-to-end. |
34
+
35
+ ## Host environment
36
+
37
+ `createChatHost` never touches disk or Electron directly — the embedding
38
+ process supplies a `ChatHostEnvironment`:
39
+
40
+ - **GUI main** (`gui/src/main/chat/host.ts`): broadcast → `webContents.send`
41
+ demuxed onto the legacy renderer channels; settings in `state.json`'s
42
+ `settings` slot (shared with the TUI and the daemon; the legacy sqlite
43
+ row is migrated by `initSettings`), pending approvals in sqlite; the
44
+ extension-aware runtime-config cache.
45
+ - **Daemon** (`src/ipc/daemon/environment.ts`): broadcast → socket peers;
46
+ settings in `state.json`'s `settings` slot (shared with the TUI); pending
47
+ approvals in `pending-approvals.json`; per-cwd extension discovery with
48
+ the same allowlist + trust contract as every other surface.
49
+
50
+ Both run the **same engine** — the gate flow, edit-approval pipeline,
51
+ compaction, and hook forwarding cannot drift between surfaces anymore.
52
+
53
+ ## Signals and convergence
54
+
55
+ Every state change broadcasts a `HostSignal` to every connected client:
56
+
57
+ - `run:start` / `event` / `run:end` — the live stream. A window attaching
58
+ mid-run starts receiving events immediately and rebuilds prior history via
59
+ `sessions.loadEvents` (canonical `eventsFromTurns` replay).
60
+ - **The user message is part of the stream**: the host broadcasts a
61
+ `user-prompt` event for every send / steer / follow-up, BEFORE
62
+ `run:start`, and stamps the sender's own copy `echo: true` —
63
+ ORIGIN-AGNOSTICALLY (one broadcast, every peer sees the tag; the host
64
+ cannot know which window originated the send). Clients that render
65
+ purely from the broadcast (TUI, attach) ignore the flag. A client that
66
+ appends its own sends optimistically (the Electron GUI, for
67
+ refs/attachments enrichment) must dedupe CLIENT-SIDE: consume exactly
68
+ one pending optimistic append per matching echo and render every other
69
+ user prompt — echo-tagged prompts with no pending match are genuine
70
+ remote sends (another window, a queue drain). Never blanket-drop
71
+ echo-tagged prompts in an adapter: that swallows other windows'
72
+ messages (see `StreamStore._pendingEcho`).
73
+ - `queue:changed` — the shared type-ahead queue. Host-owned: `queue.add`
74
+ queues while a run is in flight (from ANY window) and sends otherwise;
75
+ the host drains FIFO at run end, so queued messages run even after the
76
+ window that queued them closed. Every window renders the same queue box
77
+ and can drop (`queue.remove`) or steer (`queue.pushNow`) any entry.
78
+ - **Re-convergence guarantee**: at every run end (and on reconnect) the
79
+ TUI replaces its transcript with the canonical `sessions.loadEvents`
80
+ replay — any signal a window missed (disconnect, attach gap, daemon
81
+ respawn) is reconciled at the next run boundary, so windows cannot
82
+ drift apart permanently. A dropped daemon connection auto-reconnects
83
+ (re-activation with full replay) after a short grace.
84
+ - **Slash commands execute HOST-SIDE** (`chat.send` / `chat.pushQueued`
85
+ route them; full parity incl. agent-spawning commands). The TUI's own
86
+ `/` commands and `!` shell still execute window-locally by design
87
+ (UI-coupled); their persisted transcript effects converge immediately
88
+ via `sessions.touch`.
89
+ - `search:changed` — the shared transcript-search state (mode + query):
90
+ entering/exiting search or editing the query in one window mirrors in
91
+ every window on the session, same origin-excluded last-writer-wins
92
+ contract as drafts, backfilled on attach. Draft publishing is
93
+ suppressed while searching (the buffer holds the QUERY; the parked
94
+ draft stays the shared text) and shell mode rides the draft as an
95
+ out-of-band `shell` flag — never encoded into the text, so a literal
96
+ leading `!` in a normal draft can't flip other windows into
97
+ shell-execution semantics.
98
+ - `draft:changed` — the shared per-session prompt draft. The daemon
99
+ excludes the ORIGINATING peer from the fan-out, so a window's own
100
+ keystrokes never bounce back and fight its cursor (echo-safety by
101
+ construction, no client-side dedupe). Last-writer-wins whole-text
102
+ replacement; drafts are ephemeral daemon memory (`drafts.get/set`),
103
+ backfilled on attach, and cleared for every other window when any
104
+ window submits. The TUI publishes EVERY keystroke as a fire-and-forget
105
+ JSON-RPC notification (no id, no response, FIFO on the socket — the
106
+ last keystroke always wins), so other windows mirror the draft at
107
+ keystroke latency; remote drafts apply through the same imperative
108
+ `setText` path the external-editor reload uses.
109
+ - `settings:changed` — settings written through the host fan out so every
110
+ window's settings UI converges live. The TUI publishes its local
111
+ settings changes through the daemon when in daemon mode; behavioral
112
+ reads (safe-mode gates, etc.) were already convergent because the
113
+ daemon re-reads `state.json` per call.
114
+ - `approval:request` / `approval:resolved` — the safe-mode queue. Any window
115
+ may answer; `approval:resolved` clears the surface everywhere else. Pending
116
+ rows survive daemon restarts (persisted before broadcast) and are listable
117
+ via `approvals.listPending` for late attachers.
118
+ - `interaction:request` / `interaction:resolved` — `present_plan` /
119
+ `ask_user`, same fan-out semantics.
120
+ - `sessions:changed` — create/update/delete, keeps session lists in sync.
121
+ - `work:snapshot`, `todos:snapshot`, `compacting:start` / `compacting:progress`
122
+ / `compacting:end`, `model:fallback`, `stream:retry` / `stream:retry:cleared`,
123
+ `cache:break` — auxiliary indicators. `compacting:progress` carries the live
124
+ summary-token estimate; `stream:retry:cleared` is a distinct kind (not a
125
+ nullable `attempt`) so a subagent stream dying mid-run clears only its own
126
+ chip.
127
+ - `prompt-turns` — prompt turns that just persisted, reduced to
128
+ `PromptTurnRef` (`id` + texts + attachment-only) so a window can bind its
129
+ live `user-prompt` echo to a turn id during the run. Deliberately not the
130
+ full `SessionTurn`: those carry base64 attachment payloads.
131
+ - `event { kind: 'session-event' }` — a worker's outcome delivered to its main
132
+ session (rides the existing `event` signal; no new kind). Clickable in both
133
+ clients: activation opens the worker.
134
+ - `system-md` — which `SYSTEM.md` override (if any) the host folded into the
135
+ session's prompt, emitted at agent-build time, plus the precedence losers.
136
+ Windows cannot derive this locally: the daemon owns the cwd and the
137
+ `userInstructionsScope` setting.
138
+
139
+ RPCs added alongside (all daemon-dispatched, all mirrored by the GUI's
140
+ embedded path through the same `ChatHost` method):
141
+
142
+ - `todos.set` — apply a user todo edit to the `Session` the MODEL reads
143
+ (`ChatHost.setTodos`, validated + capped host-side). Returns the resulting
144
+ snapshot, or `null` when no agent is live so the caller falls back to a
145
+ store-level write. This is what makes the todos modal's edits survive the
146
+ model's next `todowrite`.
147
+ - `sessions.systemMd` — attach-time backfill of the resolved `SYSTEM.md`
148
+ (windows cannot probe the daemon's filesystem).
149
+ - `sessions.contextSegments` — the Instructions tab: every prompt segment with
150
+ provenance, from the live agent's assembly.
151
+ - `sessions.createSystemOverride` — seed a user-scope `SYSTEM.md` from the
152
+ doctrine in effect; throws when an override could never apply (host-supplied
153
+ profile, or a scope that excludes user files) rather than writing a file
154
+ that would never be read.
155
+
156
+ > **Adding a signal.** The consuming ends fail CLOSED on purpose: both
157
+ > `src/tui/runtime/daemon.ts` and `gui/src/main/chat/host.ts` end their
158
+ > dispatch with `signal satisfies never`, so a new `HostSignal` kind does not
159
+ > compile until it is routed explicitly. Note that this catches an unrouted
160
+ > KIND, not an unconsumed CALLBACK — `onRetry` and `onModelFallback` were once
161
+ > declared, decoded, and dispatched while the TUI never supplied them, which
162
+ > silently removed the retry indicator and the model-fallback notice. Callbacks
163
+ > whose absence is a user-visible regression are therefore REQUIRED (not
164
+ > optional) in `DaemonSessionCallbacks`.
165
+
166
+ ### Concurrent sends
167
+
168
+ Two windows racing `chat.send` on one session is refused host-side: the
169
+ loser gets an explicit error. `chat.pushQueued` is the race-free spelling —
170
+ it **steers** the live run when one is active and sends otherwise.
171
+
172
+ > **Cross-process run guard.** Both directions are now guarded through the
173
+ > store: the canonical loop persists session-level `status: 'running'`, so
174
+ > the daemon's `sendPrompt` refuses a session a local in-process window is
175
+ > running, and the local TUI refuses a session the daemon is running —
176
+ > each with an explicit, actionable error. A freshness bound (5 min against
177
+ > `updatedAt`) means a crashed process's leftover marker never blocks new
178
+ > runs; live runs keep the marker fresh through a 60s heartbeat (targeted
179
+ > single-column status UPDATE — safe against concurrent turn appends), so
180
+ > long quiet tool calls never make a live run look stale.
181
+
182
+ ## Extension bus (`ctx.bus`)
183
+
184
+ A private, permission-gated, per-extension cross-window message channel.
185
+ An extension declaring the `'ipc'` permission gets `ctx.bus` —
186
+ `emit(payload, { sessionId? })` plus `onMessage(fn)`; without the
187
+ permission `ctx.bus` is `undefined` and no emit path exists.
188
+
189
+ **Privacy model.** Channels are private per extension id: a subscriber
190
+ only ever receives its own extension's messages. The id on every wire
191
+ envelope is stamped by the host from the registration identity (the
192
+ `buildExtensionRegistry` entry name) — the emit surface has no field a
193
+ caller could use to impersonate another extension, and the daemon
194
+ re-validates inbound `extension.emit` ids against the session cwd's
195
+ registry (id must be loaded AND `'ipc'`-permitted) before fanning out.
196
+
197
+ **Delivery.** `emit` delivers to local subscribers synchronously
198
+ (`origin: 'local'`) — in-process mode is fully functional with zero IPC —
199
+ and forwards the envelope (`extension:signal`, additive v6) to peers when
200
+ a transport exists. Receivers re-inject with `origin: 'remote'`, dropping
201
+ ids without a local registration and deduping by `(extensionId, seq)`
202
+ (bounded last-seen window) so a self-echo through a second connection of
203
+ the same process, or the same signal arriving on several session
204
+ connections, lands at most once. Payloads are JSON values capped at
205
+ 64 KiB serialized — enforced at emit (dropped with a logger warning) and
206
+ again at the daemon trust boundary.
207
+
208
+ Per embedding:
209
+
210
+ - **daemon** — shared bus across the per-cwd registries; emits broadcast
211
+ to every peer; client `extension.emit` notifications are validated,
212
+ delivered daemon-side, and re-broadcast to every peer except the origin.
213
+ - **TUI** — process-global bus (`tui-bus.ts`); local mode is loopback-only.
214
+ In daemon mode extensions still run in the TUI process: emits ride the
215
+ active session connection's `extension.emit` notification, and every
216
+ connection routes inbound `extension:signal`s back into the one bus.
217
+ - **GUI** — extensions run in the main process; loopback covers all
218
+ main-side subscribers and the renderer (no extension runtime) never
219
+ sees these signals. In daemon mode inbound signals re-inject via the
220
+ main demux, and main-side emits ride the daemon connection's
221
+ `extension.emit` notification (same shape as the TUI's outbound hook).
222
+
223
+ ## Daemon lifecycle
224
+
225
+ - Socket: `~/.zidane/daemon/daemon.sock` — a dedicated 0700 subdir so the socket
226
+ never inherits the shared state dir's permissions (honors `ZIDANE_STORAGE_DIR` /
227
+ `ZIDANE_PREFIX`), override with `ZIDANE_DAEMON_SOCKET`.
228
+ - **`useDaemon` setting** (Settings → agent, experimental): selects the
229
+ TUI's execution runtime — in-process agent (default) vs the shared
230
+ daemon. Plain `zidane` always opens the full TUI either way; the flag
231
+ only decides where the agent loop runs. With it on, two terminals
232
+ opening the same session converge on the same live stream: both see
233
+ the run, either can steer / interrupt / answer approvals, and closing
234
+ a window leaves the run executing. Window-local by design in daemon mode:
235
+ the TUI's own `/` command execution and `!` shell (both converge via
236
+ `sessions.touch`); slash commands sent over the wire execute host-side,
237
+ and attachments sync end-to-end. Implementation:
238
+ `src/tui/runtime/daemon.ts` (`connectDaemonSession`).
239
+ - Start explicitly: `zidane daemon [--socket <path>] [--verbose]` or the
240
+ published `zidane-daemon` bin (`--idle-exit <ms>` exits after a period
241
+ with zero peers; default 30 min).
242
+ - Spawn-on-demand: `connectOrSpawnDaemon()` / `zidane attach` probe the
243
+ socket and spawn a detached daemon when nothing is listening.
244
+ - Stale sockets: a leftover socket file from a crashed daemon is probed
245
+ before unlink — a live daemon is never yanked from under its peers; a
246
+ second daemon on the same socket refuses to start.
247
+ - Version skew: the first request must be `ipc.hello` carrying
248
+ `IPC_PROTOCOL_VERSION`. On mismatch the daemon replies with a structured
249
+ error (its own version attached), the client asks it to drain
250
+ (`daemon.shutdown`) and respawns a fresh one.
251
+ - **Build skew (dev)**: the handshake also carries a build fingerprint
252
+ (`IPC_BUILD_ID`, the ipc module's file mtime at import). A daemon
253
+ started from an older build of the SAME protocol version is rejected,
254
+ drained, and respawned — so during development a rebuild or source edit
255
+ automatically retires running daemons instead of leaving them serving
256
+ stale code. Compiled binaries report `'unknown'` and skip the check
257
+ (releases are governed by the protocol version alone).
258
+
259
+ ## Turning it off (containment guarantees)
260
+
261
+ The daemon runtime is opt-in and designed to be inert when disabled:
262
+
263
+ - **Default off**: `useDaemon` defaults to `false`. Every daemon touchpoint
264
+ in the TUI is gated on the live controller (`daemonRef`) — with the
265
+ setting off, no daemon code runs beyond one boot-time module import: no
266
+ socket, no spawn, no signals, and the in-process path is byte-identical
267
+ to pre-IPC behavior (send/queue/drain, settings persistence, gates).
268
+ - **Cross-process run guard + heartbeat gated too**: the pre-send
269
+ 'running'-marker guard and the 60s marker heartbeat (TUI local runtime
270
+ AND the shared host) only engage when `useDaemon` is on. With IPC
271
+ disabled there is exactly one writer per session, so the only marker a
272
+ fresh process can meet is a crashed one's — ungated, that meant up to a
273
+ 5-minute send lockout after an ungraceful exit mid-run. `loadSession`
274
+ additionally heals stale (>5 min) on-disk 'running' headers with a
275
+ targeted write, bounded so a reader can never clobber a live run's
276
+ marker.
277
+ - **Setting toggle**: flipping `useDaemon` off takes effect at the next
278
+ session activation; teardown disposes the controller and the local
279
+ runtime takes over. Detached daemons idle-exit on their own.
280
+ - **GUI, same gate**: the Electron main process gates on the SAME shared
281
+ `useDaemon` flag (settings live in the one `state.json`). Disabled, the
282
+ GUI never probes the socket; its daemon-mode handlers all carry
283
+ `if (daemon) wire / else local` branches where the local side is the
284
+ embedded-host path the whole GUI test suite exercises (the suites run
285
+ daemon-less), and the renderer sync surfaces are inert without signals
286
+ (the shared-queue latch only engages on a first `queue:changed` and
287
+ releases on `daemon:state` disconnect).
288
+ - **Emergency kill-switch**: `zidane --no-daemon` (or `ZIDANE_NO_DAEMON=1`)
289
+ forces the in-process runtime for that launch regardless of the setting —
290
+ usable even if the settings UI itself is unreachable. The same env var
291
+ gates the GUI above its setting.
292
+ - **Fail-soft activation**: if the daemon can't be reached (spawn failure,
293
+ socket problem, handshake bug), session activation catches the error and
294
+ falls back to the in-process runtime with a visible notice — a broken
295
+ daemon path can never brick a session.
296
+ - **Shared-state footprint when disabled**: none. The daemon's extra state
297
+ (socket, pidfile, `pending-approvals.json`, in-memory drafts/queues)
298
+ is only created by a running daemon; `sessions.db` / `state.json` access
299
+ from the local TUI is unchanged.
300
+
301
+ ## Execution semantics (read this)
302
+
303
+ Moving the host out of the window process changes two things users notice:
304
+
305
+ 1. **Tools run in the daemon's environment.** cwd comes from the session
306
+ (`metadata.cwd`), but env vars are the daemon's, not the terminal's.
307
+ Terminal-specific env (`AWS_PROFILE` exported in one shell) does not
308
+ reach daemon-hosted tools.
309
+ 2. **Runs survive window close.** Detaching (`/exit`, closing the terminal)
310
+ leaves the run executing in the daemon — that's the feature. Interrupt
311
+ explicitly (`/interrupt`, Ctrl+C while running) to stop it.
312
+
313
+ ## Testing
314
+
315
+ - `test/ipc-host.test.ts` — host engine against a mock provider + fake
316
+ broadcast: run lifecycle, gate decisions (accept/deny/denyAll/off),
317
+ interaction flow, dispose/shutdown.
318
+ - `test/ipc-daemon.test.ts` — real unix-socket round-trips: handshake,
319
+ version mismatch, stale socket, multi-peer fan-out, late-attach replay,
320
+ cross-window approval convergence, the concurrent-send guard, and a full
321
+ scripted `zidane attach` session.
322
+
323
+ ## Surface cabling matrix (daemon runtime)
324
+
325
+ Audited feature-by-feature. **Synced** = converges across every window via
326
+ host signals; **local** = executes in the issuing window by design, effects
327
+ converge through the shared store at the next run-end resync; **gap** =
328
+ known missing cabling, listed with its path forward.
329
+
330
+ | Surface | Status | How |
331
+ | --- | --- | --- |
332
+ | Prompt send / stream / interrupt / steer | synced | `chat.*` + `event` signals |
333
+ | User messages (incl. steered/queued) | synced | host-broadcast `user-prompt` before `run:start` |
334
+ | Attachments (paste/drag images + docs) | synced | `queue.add`/`chat.send` carry base64 payloads; `user-prompt` carries chip metadata (name/type/exact size) |
335
+ | Type-ahead queue (queue box, push/drop) | synced | host-owned queue, `queue:*` RPC + `queue:changed` |
336
+ | Prompt draft (input box content) | synced | per-keystroke `drafts.set` notifications, origin-excluded |
337
+ | Safe-mode approvals | synced | `approval:*` signals, cross-window resolve + retraction |
338
+ | `present_plan` / `ask_user` | synced | `interaction:*` signals, resumed orphans included |
339
+ | Settings (UI + behavior) | synced | `settings:changed`; daemon is the sole `state.json` writer in daemon mode |
340
+ | Session list / titles / archive | synced | `sessions:changed` + store reads |
341
+ | Session list "active" scope + status icons | synced | `presence.watch`/`presence.list` + `presence:changed`; each window ALSO runs an app-lifetime presence watcher (probe-only, never spawns, never counted as a window) so the list shows status before activation and across the session-switch teardown gap |
342
+ | Model fallback chains (multi-model picker) | synced | `metadata.modelChain` (TUI byte format) restored host-side into multi-provider agents + `buildRunModel` pinned targets with per-target context windows; `sessions.setModel` carries `fallbacks`, `SessionRow.modelFallbacks` feeds pickers |
343
+ | Compaction summarizer (`compactionModel`) + skill-edit sessions + folderless cwd (HOME anchor) | synced | ported from the GUI engine into the shared host |
344
+ | Model / effort / model-options picker | synced | pick → `sessions.setModel/setEffort/setModelOptions`; session metadata is authoritative, absent metadata seeded from the window's pick |
345
+ | Todos indicator + editing modal | synced | reads: `todos:snapshot` is MIRRORED into the window's session (`applyTodosSnapshot`) after a store reload for `runs` — the bag itself is never persisted, so a reload alone was always empty; `todos.get` backfills on attach. Writes: `todos.set` lands on the live agent's `Session`, and the host fans the result out. Same `chat/todo-edits` mutators on both surfaces |
346
+ | Running work + ctrl+k cancel picker | synced | `work:snapshot` mirror; cancels via `chat.cancelWork` |
347
+ | Context panel (ctrl+?) | synced | `sessions.contextBreakdown` RPC |
348
+ | History edits (edit / delete turns via the selection modal) | synced | mutations refresh the wrapper from the store first (refused mid-run), retire the daemon's cached agent (`chat.rebuildAgent` — the next run uses the edited history), and announce via `sessions.touch` for immediate re-convergence |
349
+ | History search (transcript + cross-session) | synced + accelerated | `history.searchSession`/`history.searchAll` run in the daemon over authoritative turns (fresh mid-run) with one LRU index cache shared by every window; local worker path remains the fallback |
350
+ | Provider-retry indicator ("retrying n/m", parent + subagents) | synced | `stream:retry` signal; clears on resumed streaming / run end |
351
+ | Run ceilings (max turns / cost / tokens / wall clock / retry budget) | synced | host reads the same settings-driven caps per run the local TUI applies |
352
+ | Native server tools (e.g. Anthropic web_search) | synced | host forwards `stream:server_tool_use/result` as the same synthetic tool events replay produces |
353
+ | Footer context gauge + live session cost | synced | `turn:end` signals carry `TurnUsage` (parent turns drive the gauge; parent + child turns accrue cost) |
354
+ | Subagent spawn banners (start/end/error markers) | synced | host forwards `spawn:before/complete/error` as the same `spawn-start`/`spawn-end` events replay produces |
355
+ | Active-skill chips | synced | `skills:snapshot` on every activation change |
356
+ | MCP "needs login" badge | synced | `mcp:auth` signal; the interactive login flow runs window-local (shared credential store; daemon reconnects on next rebuild) |
357
+ | Manual compaction indicator | synced | `compacting:*` signals |
358
+ | Transcript convergence after any miss | synced | authoritative `loadEvents` replace at every run end + reconnect |
359
+ | `/` commands (incl. extension commands) | synced | HOST-SIDE execution (`src/chat/host/commands.ts`): `chat.send`/`chat.pushQueued` route slash commands in the host — full parity incl. `spawnAgents`/`runAgent` (the command spawn seam lives on the host's ActiveRun), output events fan out live to every window, command turns persist via the live Session. `commands.list`/`commands.completeArgs` serve catalogs over the wire. The TUI's daemon mode still executes its own `/` commands locally by design (UI-coupled) with `sessions.touch` convergence |
360
+ | `!` shell mode | synced output | runs in the issuing window's cwd/env; the persisted call + result turns trigger `sessions.touch` → immediate convergence |
361
+ | Extension tools / hooks / MCPs | synced | the daemon discovers + loads the same extension registry (same allowlist, trust, `state.json`) — extension tools run daemon-side for every window |
362
+ | Agent Plugins (skills + MCP servers, `enabledPlugins` gate) | synced | the host discovers disk plugins per cwd (cached) and folds contributions into the agent it builds, and applies the SECOND plugin gate (`filterRegistryPlugins`) to the registry before composing the preset — so a plugin toggled off contributes nothing daemon-side either. Windows keep their own catalog for the settings list |
363
+ | `SYSTEM.md` prompt override | synced | the host discovers it where it composes the prompt (`systemFor`), under the same `userInstructionsScope` as AGENTS.md; the winning file + shadowed candidates ride the `system-md` signal so a window can tell "none on disk" from "in force" without probing a filesystem that may not be the daemon's |
364
+ | Cache-break chip (`cache:break`) + predictive `cache:drift` | synced | detection is per-agent closure state, so it is installed on the daemon's agent and forwarded as `cache:break`; cost pricing stays window-side (it needs only the provider key + token count) |
365
+ | Main session (orchestration) | synced | opt-in (`orchestration`, default off). The host owns the main (`metadata.role='main'`) and its workers (`metadata.orchestratedBy`); `orchestration.main/createWorker/list/read/send/steer/cancel` RPCs, verbs scoped to the caller's own workers. A worker's `run:end` / pending approval reaches the main on two channels: a `session-event` transcript row broadcast immediately (display, safe mid-stream), and a `<session-event>` user turn + empty wake run when the main is idle (context, at the next turn boundary — buffered while running so a user turn never lands between a `tool_call` and its `tool_result`). Requires the daemon in the TUI: the local runtime has no host |
366
+ | Extension UI (renderers, themes, completion, HUD chrome) | local | loaded by each window's own registry, renders broadcast events |
367
+ | Extension hook-driven HUDs (status lines / banners / notify) | synced | the daemon runs a live HUD bus for extension hooks and forwards `hud:state`/`hud:notify`; windows overlay it under their local HUD (local wins per extension) |
368
+ | Draft attachments (pre-submit composing view) | synced | `drafts.setAttachments` notification (payloads included, 24 MiB cap), origin-excluded fan-out, backfill on attach, cleared on submit |
369
+ | Background-task wake runs | synced | host-scheduled: an idle session with pending task notifications runs a wake pass (no `user-prompt` echo) — even with zero windows attached; user interrupts latch suppression until the next send |
370
+ | Steer / follow-up transcript order | synced | `user-prompt` broadcasts at DELIVERY time via the `steer:inject` hook (matched against accepted texts so system injections never render as user messages) |
371
+ | MCP login → daemon credential pickup | synced | `chat.rebuildAgent` retires the daemon-side agent after a window-local OAuth login (run-end deferred) |
372
+ | Cross-process run guard | synced | store-status guard in both directions with a staleness bound (see above) |
373
+ | Agent-spawning `/` commands (`spawnAgents`/`runAgent`) | **gap** | need a local spawn seam without the chat agent (or a host-side spawn RPC) — the last command-parity item |
374
+ | GUI windows on the shared daemon | synced | GUI main is a daemon guest under the SAME strict opt-in gate as the TUI: the shared `useDaemon` setting in `state.json` (probe-first, never spawns — the daemon needs Bun, Electron main is Node; `ZIDANE_NO_DAEMON=1` is the kill-switch above the setting); execution + HITL + queue + drafts + search + presence + settings writes ride the wire, store + settings reads stay local (same `sessions.db` / `state.json`), store mutations stay local + `sessions.touch` / `chat.rebuildAgent`; fail-soft to the embedded host with a 5s background retry |
375
+ | GUI composer draft / attachments / `?` search / queue / presence badges / settings live-apply | synced | renderer surfaces bound to the daemon-mode channels; inert in-process (no signal → local behavior unchanged) |
376
+
377
+ ## Removability
378
+
379
+ The dependency direction is strictly `src/ipc` → `src/chat`: the host and
380
+ its wire shapes (`src/chat/host/**`, incl. `signals.ts`) import nothing from
381
+ `src/ipc`. Deleting the whole transport — `src/ipc/**`, `src/daemon-cli.ts`,
382
+ `src/tui/runtime/daemon.ts`, and the GUI's daemon glue
383
+ (`gui/src/main/chat/daemon-client.ts` plus the daemon branches in
384
+ `ipc.ts`/`engine.ts`/`interactions.ts`/`safeMode.ts`) — removes multi-window
385
+ sync and nothing else: `createChatHost` stays, because it is the chat engine
386
+ the GUI embeds unconditionally.
387
+
388
+ ## Status / follow-ups
389
+
390
+ - The default TUI (`zidane`) still runs its agent in-process; `zidane
391
+ attach` is the daemon-backed terminal client. Migrating the full OpenTUI
392
+ front-end onto `connectIpcClient` is the tracked follow-up.
393
+ - The GUI embeds the SHARED `ChatHost` in the Electron main process
394
+ (re-thinned; commands, model chains, summarizer, skill-edit, session-cwd,
395
+ spawn-markers all canonical) and ALSO speaks daemon: whenever a daemon
396
+ is listening AND the shared `useDaemon` setting is on,
397
+ `gui/src/main/chat/daemon-client.ts` connects as a guest. The gate is
398
+ the SAME strict opt-in as the TUI's — readable here because settings
399
+ live in the one canonical `state.json`; the earlier socket-as-opt-in
400
+ heuristic existed only for the pre-migration sqlite bootstrap gap. A
401
+ manually-run `zidane daemon` with `useDaemon` off is served to explicit
402
+ `zidane attach` clients but never adopted implicitly by the GUI. The
403
+ gate re-evaluates live: local `settings:set`, remote
404
+ `settings:changed` broadcasts, and a 5s poll all trigger
405
+ connect/disconnect (`ZIDANE_NO_DAEMON=1` is the kill-switch above the
406
+ setting, mirroring `--no-daemon`) (`drainOnMismatch: false`, never
407
+ spawns or retires a daemon) and routes execution, HITL, queue, drafts,
408
+ search, presence, and settings writes over the wire; the embedded host
409
+ stays as the fail-soft fallback. Settings live in the canonical
410
+ `state.json` (shared with the TUI): `settings:get` reads it locally in
411
+ both modes, `settings:set` in daemon mode is WIRE-ONLY — a local
412
+ pre-write would empty the daemon's before/after diff and skip its
413
+ agent-teardown ladder on extension/MCP toggles — with a local-write
414
+ fallback when the socket dies mid-write.
415
+ Wire-less session mutations (fork / deleteTurn / editTurnText /
416
+ attachFolder / detachFolder) stay local-store + `sessions.touch` +
417
+ `chat.rebuildAgent`, with a `chat.isRunning` refusal guard on history
418
+ edits. Remaining optional wire additions: paginated `sessions.loadEvents`
419
+ + lazy tool-result fetch and the SQL-narrowed cross-session search fast
420
+ path (perf only — correctness converges through touch/replay today).
package/docs/TUI.md CHANGED
@@ -566,6 +566,67 @@ Interactive login flow (triggered by `ctrl+L` on a needs-auth row in `SettingsMo
566
566
 
567
567
  A pre-existing user picker (when at least one provider is already authed) lists current credentials plus a `+ add or re-configure` entry that opens the wizard with a `← back` affordance. Esc on the wizard bails to the picker (or exits the TUI on a true first launch).
568
568
 
569
+ ## Execution runtimes (in-process vs daemon)
570
+
571
+ The TUI is one UI over two interchangeable engines. `activateSession`
572
+ (`src/tui/app.tsx`) is the single fork point:
573
+
574
+ - **In-process (default)** — `agentRef.current = buildAgent(...)`; the agent
575
+ loop, hooks, gates, and MCP connections live in this window's process.
576
+ This is the battle-tested path and the floor everything degrades to.
577
+ - **Daemon** (`useDaemon` setting, experimental) — `daemonRef.current =
578
+ connectDaemonSession(...)` (`src/tui/runtime/daemon.ts`); execution lives
579
+ in the shared daemon and this window is presentation. Multiple windows on
580
+ one session converge on the same live stream, queue, drafts, approvals,
581
+ and presence. Full protocol + daemon lifecycle: `docs/IPC.md`.
582
+
583
+ **Mutual exclusivity is structural**: an activation populates exactly one of
584
+ `agentRef` / `daemonRef` — no code path ever reconciles two live sources of
585
+ truth. The ~29 daemon touchpoints are shaped `if (daemonRef.current) { RPC }
586
+ else { agent }`, and both branches feed the **same React state** (stream
587
+ buffer, safe-mode queue, interactions queue, queue-box mirror, todo
588
+ indicator, HUD overlay) — the render layer cannot tell the runtimes apart,
589
+ which is why every screen/modal/keybinding works in both modes without a
590
+ fork.
591
+
592
+ Coexistence rules (why mixing windows is safe):
593
+
594
+ - **The store is the neutral ground.** Both runtimes read/write the same
595
+ `sessions.db` / `state.json` through canonical code; a window's live view
596
+ re-converges with the store at every run end and on `sessions.touch`
597
+ (out-of-band appends from `/` commands and `!` shell).
598
+ - **Cross-process run guard.** The loop persists session-level
599
+ `status: 'running'`; both runtimes check it (with a 5-minute freshness
600
+ bound against `updatedAt`) before starting a run, so an in-process window
601
+ and the daemon can never stack `agent.run()` on one session — each side
602
+ gets an explicit error instead of interleaved turns.
603
+ - **Settings single-writer.** While a controller is connected the daemon
604
+ owns `state.json` settings writes (`publishSettings` → `settings.set`);
605
+ locally only the `useDaemon` key persists. With no controller (auth
606
+ screens, disconnect grace, local mode) the pre-IPC local persistence is
607
+ byte-identical.
608
+
609
+ Containment (turning it off must always work):
610
+
611
+ - Every daemon touchpoint gates on the live controller — with the setting
612
+ off, no daemon code runs beyond a module import.
613
+ - `zidane --no-daemon` / `ZIDANE_NO_DAEMON=1` beat the setting entirely
614
+ (usable even if the settings UI is unreachable).
615
+ - Activation is **fail-soft**: a daemon connect failure falls back to
616
+ `buildAgent` for that session with a visible notice — a broken daemon
617
+ path can never brick a window.
618
+ - Teardown order is load-bearing: dispose the controller FIRST (so local
619
+ `denyAll()`/`cancelAll()` can't settle shared daemon HITL state), clear
620
+ the connected flag, bump the activation generation (supersedes an
621
+ in-flight connect), then drop the remote mirrors (draft, attachments,
622
+ HUD overlay, presence).
623
+
624
+ Known asymmetries in daemon mode (documented, not silent): agent-spawning
625
+ `/` commands (`spawnAgents`/`runAgent`) need a local agent and degrade with
626
+ an explicit error; `!` shell and `/` commands execute locally in the
627
+ issuing window (their persisted output converges everywhere via
628
+ `sessions.touch`).
629
+
569
630
  ## Embedding
570
631
 
571
632
  Drive `<App>` against your own renderer when you need:
@@ -701,3 +762,4 @@ src/tui/
701
762
  - `docs/INTERACTIONS.md` — `present_plan` / `ask_user` protocol.
702
763
  - `docs/SKILL.md` — skill system, lifecycle, and authoring.
703
764
  - `docs/ARCHITECTURE.md` — agent loop, hook firing order, gate precedence.
765
+ - `docs/IPC.md` — daemon protocol, signal fan-out, sync matrix, containment guarantees.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zidane",
3
- "version": "6.6.3",
3
+ "version": "7.0.0",
4
4
  "description": "an agent that goes straight to the goal",
5
5
  "type": "module",
6
6
  "private": false,
@@ -211,6 +211,11 @@
211
211
  "import": "./dist/acp.js",
212
212
  "default": "./dist/acp.js"
213
213
  },
214
+ "./ipc": {
215
+ "types": "./dist/ipc.d.ts",
216
+ "import": "./dist/ipc.js",
217
+ "default": "./dist/ipc.js"
218
+ },
214
219
  "./eval": {
215
220
  "types": "./dist/eval.d.ts",
216
221
  "import": "./dist/eval.js",
@@ -221,7 +226,8 @@
221
226
  "main": "./dist/index.js",
222
227
  "types": "./dist/index.d.ts",
223
228
  "bin": {
224
- "zidane-acp": "./dist/acp-cli.js"
229
+ "zidane-acp": "./dist/acp-cli.js",
230
+ "zidane-daemon": "./dist/daemon-cli.js"
225
231
  },
226
232
  "files": [
227
233
  "dist",