zidane 6.6.4 → 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.
- package/dist/{acp-BQqhozmt.js → acp-Wr8l4IDI.js} +7 -180
- package/dist/acp-Wr8l4IDI.js.map +1 -0
- package/dist/acp-cli.js +6 -6
- package/dist/acp.d.ts +1 -1
- package/dist/acp.d.ts.map +1 -1
- package/dist/acp.js +2 -1
- package/dist/{agent-DTxH7RHW.js → agent-C-camKQW.js} +162 -12
- package/dist/agent-C-camKQW.js.map +1 -0
- package/dist/agent.d.ts +1 -1
- package/dist/agent.js +1 -1
- package/dist/{anthropic-DRNRnd6W.js → anthropic-BDOGzhtj.js} +3 -3
- package/dist/{anthropic-DRNRnd6W.js.map → anthropic-BDOGzhtj.js.map} +1 -1
- package/dist/{transcript-anchors-BjH-68k2.js → approvals-CqqxE5Vh.js} +4924 -275
- package/dist/approvals-CqqxE5Vh.js.map +1 -0
- package/dist/{auth-7rv6ZfWk.js → auth-BdwDROYg.js} +12 -8
- package/dist/{auth-7rv6ZfWk.js.map → auth-BdwDROYg.js.map} +1 -1
- package/dist/chat/pure.d.ts +221 -4
- package/dist/chat/pure.d.ts.map +1 -0
- package/dist/chat/pure.js +4 -2
- package/dist/chat.d.ts +6 -2911
- package/dist/chat.js +8 -261
- package/dist/{context-transport-D8yeGdpd.js → context-transport-BqYwzJYE.js} +2 -2
- package/dist/{context-transport-D8yeGdpd.js.map → context-transport-BqYwzJYE.js.map} +1 -1
- package/dist/contexts/daytona.d.ts +1 -1
- package/dist/contexts/daytona.js +1 -1
- package/dist/contexts/e2b.d.ts +1 -1
- package/dist/contexts/e2b.js +1 -1
- package/dist/daemon-cli.d.ts +1 -0
- package/dist/daemon-cli.js +90 -0
- package/dist/daemon-cli.js.map +1 -0
- package/dist/eval.d.ts +1 -1
- package/dist/eval.js +1 -1
- package/dist/extensions.d.ts +2 -2
- package/dist/extensions.js +2 -2
- package/dist/headless.d.ts +1 -1
- package/dist/headless.js +4 -4
- package/dist/{index-D0O-QO7Z.d.ts → index-B-FSaM3_.d.ts} +2863 -2375
- package/dist/index-B-FSaM3_.d.ts.map +1 -0
- package/dist/index-DgdzXRPH.d.ts +3590 -0
- package/dist/index-DgdzXRPH.d.ts.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +14 -1179
- package/dist/ipc-CIv87xuH.js +331 -0
- package/dist/ipc-CIv87xuH.js.map +1 -0
- package/dist/ipc.d.ts +849 -0
- package/dist/ipc.d.ts.map +1 -0
- package/dist/ipc.js +4 -0
- package/dist/json-rpc-rSWMUh9b.js +192 -0
- package/dist/json-rpc-rSWMUh9b.js.map +1 -0
- package/dist/{logger-DBX9uYOw.js → logger--C1VQjwJ.js} +3 -13
- package/dist/logger--C1VQjwJ.js.map +1 -0
- package/dist/logs-view-CWG8aUAG.js +44 -0
- package/dist/logs-view-CWG8aUAG.js.map +1 -0
- package/dist/{mcp-CVtSQTWg.js → mcp-BIWa57eY.js} +3 -3
- package/dist/{mcp-CVtSQTWg.js.map → mcp-BIWa57eY.js.map} +1 -1
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.js +1 -1
- package/dist/{messages-CZMUuKzo.js → messages-21XfHxyF.js} +3 -3
- package/dist/{messages-CZMUuKzo.js.map → messages-21XfHxyF.js.map} +1 -1
- package/dist/{openai-compat-BoTi-9fd.js → openai-compat-DY-AKxCg.js} +2 -2
- package/dist/{openai-compat-BoTi-9fd.js.map → openai-compat-DY-AKxCg.js.map} +1 -1
- package/dist/output/stream-json.d.ts +1 -1
- package/dist/output/terminal.d.ts +1 -1
- package/dist/{plugins-onDvTHEm.js → plugins-B531Tz5b.js} +2 -2
- package/dist/{plugins-onDvTHEm.js.map → plugins-B531Tz5b.js.map} +1 -1
- package/dist/plugins.d.ts +1 -1
- package/dist/plugins.js +1 -1
- package/dist/presets.d.ts +1 -1
- package/dist/presets.js +1 -1
- package/dist/providers/anthropic.d.ts +1 -1
- package/dist/providers/anthropic.js +1 -1
- package/dist/providers/arcee.d.ts +1 -1
- package/dist/providers/arcee.js +1 -1
- package/dist/providers/baseten.d.ts +1 -1
- package/dist/providers/baseten.js +1 -1
- package/dist/providers/cerebras.d.ts +1 -1
- package/dist/providers/cerebras.js +1 -1
- package/dist/providers/local.d.ts +1 -1
- package/dist/providers/local.js +1 -1
- package/dist/providers/openai-compat.d.ts +1 -1
- package/dist/providers/openai-compat.js +1 -1
- package/dist/providers/openai.d.ts +1 -1
- package/dist/providers/openai.js +3 -3
- package/dist/providers/openrouter.d.ts +1 -1
- package/dist/providers/openrouter.js +1 -1
- package/dist/providers/xai.d.ts +1 -1
- package/dist/providers/xai.js +1 -1
- package/dist/{providers-CmhdtD7w.js → providers-DX91jwir.js} +2 -2
- package/dist/{providers-CmhdtD7w.js.map → providers-DX91jwir.js.map} +1 -1
- package/dist/providers.d.ts +1 -1
- package/dist/providers.js +3 -3
- package/dist/restate.d.ts +1 -1
- package/dist/restate.js +8 -8
- package/dist/restate.js.map +1 -1
- package/dist/server-DJqp9dIR.js +1317 -0
- package/dist/server-DJqp9dIR.js.map +1 -0
- package/dist/session/sqlite.d.ts +1 -1
- package/dist/session/sqlite.d.ts.map +1 -1
- package/dist/session/sqlite.js +42 -4
- package/dist/session/sqlite.js.map +1 -1
- package/dist/{session-CyRLSsVa.js → session-K3icbPH9.js} +8 -2
- package/dist/session-K3icbPH9.js.map +1 -0
- package/dist/session.d.ts +1 -1
- package/dist/session.js +2 -2
- package/dist/{skills-BMGH_KK1.js → skills-PJyiSRCp.js} +3 -21
- package/dist/skills-PJyiSRCp.js.map +1 -0
- package/dist/skills.d.ts +1 -1
- package/dist/skills.js +1 -1
- package/dist/src-DzzhE2YO.js +2461 -0
- package/dist/src-DzzhE2YO.js.map +1 -0
- package/dist/todo-edits-Dw2oO2ON.js +378 -0
- package/dist/todo-edits-Dw2oO2ON.js.map +1 -0
- package/dist/{tool-formatters-NrzD6Fkk.d.ts → tool-formatters-DO56YBBw.d.ts} +104 -4
- package/dist/tool-formatters-DO56YBBw.d.ts.map +1 -0
- package/dist/tools/fetch-url.d.ts +1 -1
- package/dist/tools/web-search.d.ts +1 -1
- package/dist/tools.d.ts +1 -1
- package/dist/tools.js +1 -1
- package/dist/{transcript-anchors-UhC58Tmj.d.ts → transcript-anchors-DXqWWGbq.d.ts} +53 -4
- package/dist/transcript-anchors-DXqWWGbq.d.ts.map +1 -0
- package/dist/tui.d.ts +94 -13
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +2241 -1009
- package/dist/tui.js.map +1 -1
- package/dist/{turn-operations-mMxDRgLY.js → turn-operations-CZLsNgw-.js} +896 -11
- package/dist/turn-operations-CZLsNgw-.js.map +1 -0
- package/dist/{turn-operations-Cuc9pwiB.d.ts → turn-operations-DKFYWP3O.d.ts} +501 -4
- package/dist/turn-operations-DKFYWP3O.d.ts.map +1 -0
- package/dist/{types-BIarq1qE.js → types-CWgENVLY.js} +24 -2
- package/dist/types-CWgENVLY.js.map +1 -0
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/dist/xml-D5HBhJle.js +22 -0
- package/dist/xml-D5HBhJle.js.map +1 -0
- package/docs/CHAT.md +39 -0
- package/docs/IPC.md +420 -0
- package/docs/TUI.md +62 -0
- package/package.json +8 -2
- package/dist/acp-BQqhozmt.js.map +0 -1
- package/dist/agent-DTxH7RHW.js.map +0 -1
- package/dist/chat.d.ts.map +0 -1
- package/dist/chat.js.map +0 -1
- package/dist/index-D0O-QO7Z.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/inline-autocomplete-D0MPvBaS.js +0 -596
- package/dist/inline-autocomplete-D0MPvBaS.js.map +0 -1
- package/dist/logger-DBX9uYOw.js.map +0 -1
- package/dist/login-CB23LyI1.js +0 -707
- package/dist/login-CB23LyI1.js.map +0 -1
- package/dist/session-CyRLSsVa.js.map +0 -1
- package/dist/skills-BMGH_KK1.js.map +0 -1
- package/dist/tool-formatters-NrzD6Fkk.d.ts.map +0 -1
- package/dist/transcript-anchors-BjH-68k2.js.map +0 -1
- package/dist/transcript-anchors-UhC58Tmj.d.ts.map +0 -1
- package/dist/turn-operations-Cuc9pwiB.d.ts.map +0 -1
- package/dist/turn-operations-mMxDRgLY.js.map +0 -1
- 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": "
|
|
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",
|