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
|
@@ -0,0 +1,1317 @@
|
|
|
1
|
+
import { Mn as serializedPayloadBytes, at as createExtensionHud, jn as createExtensionBus, ot as discoverAndBuildExtensionRegistry } from "./agent-C-camKQW.js";
|
|
2
|
+
import { S as resolveInlineSuggestion, b as createPathAutocompleteProvider } from "./src-DzzhE2YO.js";
|
|
3
|
+
import { t as writeFileAtomic } from "./atomic-write-B7zr1ddF.js";
|
|
4
|
+
import { p as resolveStorageDirs } from "./plugins-B531Tz5b.js";
|
|
5
|
+
import { a as successResponse, i as errorResponse, n as JsonRpcRemoteError, r as createJsonRpcConnection } from "./json-rpc-rSWMUh9b.js";
|
|
6
|
+
import { Pt as isMainSession } from "./turn-operations-CZLsNgw-.js";
|
|
7
|
+
import { c as searchSessionHistoryIndexAsync, n as buildSessionSearchIndexAsync, u as searchSessionIndexAsync } from "./history-search-C4bk1Wz_.js";
|
|
8
|
+
import { Gt as modelChainMetadata, In as resolveConfig, It as createChatHost, Kt as sessionModelFallbacks, Lt as toTodosSnapshot, ai as eventsFromTurns, cn as DEFAULT_SETTINGS, fn as SETTINGS_TOGGLES, ft as createFileMcpCredentialStore, hn as enforceSettingsInvariants, ii as deriveSessionTitle } from "./approvals-CqqxE5Vh.js";
|
|
9
|
+
import { d as BUILTIN_PROVIDERS, n as applyApiKeyEnv, r as credentialsPath } from "./auth-BdwDROYg.js";
|
|
10
|
+
import { basename, dirname, join } from "node:path";
|
|
11
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, unlinkSync } from "node:fs";
|
|
12
|
+
import { spawn } from "node:child_process";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
import { connect, createServer } from "node:net";
|
|
15
|
+
//#region src/ipc/daemon/lifecycle.ts
|
|
16
|
+
/**
|
|
17
|
+
* Daemon lifecycle: socket-path resolution, liveness probing, stale-socket
|
|
18
|
+
* cleanup, and spawn-on-demand.
|
|
19
|
+
*
|
|
20
|
+
* The socket lives in a dedicated daemon subdir of the resolved state dir
|
|
21
|
+
* (`~/.zidane/daemon/daemon.sock` by default, honoring the same
|
|
22
|
+
* `ZIDANE_STORAGE_DIR` / `ZIDANE_PREFIX` env knobs every other surface
|
|
23
|
+
* uses), overridable via `ZIDANE_DAEMON_SOCKET` for parallel installs and
|
|
24
|
+
* tests.
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* Build fingerprint for the handshake — the mtime of this module's file
|
|
28
|
+
* at import time. Client and daemon load the same file from disk, so a
|
|
29
|
+
* daemon spawned before a rebuild (or a source edit in dev) reports a
|
|
30
|
+
* different id than a freshly-started client, and the client drains +
|
|
31
|
+
* respawns it. This closes the "same protocol version, different code"
|
|
32
|
+
* gap that a hand-bumped version constant can't: during development the
|
|
33
|
+
* daemon process is frozen at spawn while the tree keeps moving.
|
|
34
|
+
*
|
|
35
|
+
* Compiled binaries (`/$bunfs/…` virtual paths) can't be stat'ed and
|
|
36
|
+
* report `'unknown'`; the check is skipped when EITHER side is unknown,
|
|
37
|
+
* so released builds rely on `IPC_PROTOCOL_VERSION` alone — correct,
|
|
38
|
+
* since their code only changes when the version does.
|
|
39
|
+
*/
|
|
40
|
+
const IPC_BUILD_ID = (() => {
|
|
41
|
+
try {
|
|
42
|
+
const selfPath = fileURLToPath(import.meta.url);
|
|
43
|
+
const ipcRoot = dirname(dirname(selfPath));
|
|
44
|
+
if (!existsSync(join(ipcRoot, "protocol.ts"))) return String(Math.floor(statSync(selfPath).mtimeMs));
|
|
45
|
+
const srcRoot = dirname(ipcRoot);
|
|
46
|
+
let newest = 0;
|
|
47
|
+
const walk = (dir) => {
|
|
48
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
49
|
+
const full = join(dir, entry.name);
|
|
50
|
+
if (entry.isDirectory()) walk(full);
|
|
51
|
+
else newest = Math.max(newest, statSync(full).mtimeMs);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
try {
|
|
55
|
+
walk(srcRoot);
|
|
56
|
+
} catch {
|
|
57
|
+
newest = statSync(selfPath).mtimeMs;
|
|
58
|
+
}
|
|
59
|
+
return String(Math.floor(newest));
|
|
60
|
+
} catch {
|
|
61
|
+
return "unknown";
|
|
62
|
+
}
|
|
63
|
+
})();
|
|
64
|
+
function defaultSocketPath() {
|
|
65
|
+
const explicit = process.env.ZIDANE_DAEMON_SOCKET;
|
|
66
|
+
if (explicit) return explicit;
|
|
67
|
+
return join(resolveStorageDirs({ ...process.env.ZIDANE_PREFIX !== void 0 ? { prefix: process.env.ZIDANE_PREFIX } : {} }).stateDir, "daemon", "daemon.sock");
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Pre-v6 socket location for the given (v6) socket path, or null when the
|
|
71
|
+
* path isn't the default `<stateDir>/daemon/daemon.sock` layout (explicit
|
|
72
|
+
* `ZIDANE_DAEMON_SOCKET` users never had a migration to run). A daemon
|
|
73
|
+
* still serving the old path would be ORPHANED by the move — new clients
|
|
74
|
+
* never probe it, and a service-style `--idle-exit 0` daemon would hold
|
|
75
|
+
* agents and run-guard markers against the same sessions.db forever —
|
|
76
|
+
* so daemon startup probes this path and drains any survivor.
|
|
77
|
+
*/
|
|
78
|
+
function legacySocketPath(socketPath) {
|
|
79
|
+
const dir = dirname(socketPath);
|
|
80
|
+
if (basename(socketPath) !== "daemon.sock" || basename(dir) !== "daemon") return null;
|
|
81
|
+
return join(dirname(dir), "daemon.sock");
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Spawn command that re-enters the CURRENT zidane program as `… daemon`.
|
|
85
|
+
*
|
|
86
|
+
* Three launch shapes:
|
|
87
|
+
* - compiled Bun binary — `Bun.main` is the virtual `/$bunfs/…` entry the
|
|
88
|
+
* binary self-inserts as argv[1], so the spawn must NOT repeat it
|
|
89
|
+
* (verified: repeating it makes the child parse the junk path as its
|
|
90
|
+
* subcommand and boot the full TUI instead of the daemon).
|
|
91
|
+
* - dev (`bun run cli.ts`) — the script path must ride along.
|
|
92
|
+
* - Node hosts (no `Bun` global) — fall back to the published bin.
|
|
93
|
+
*/
|
|
94
|
+
function selfDaemonSpawnCommand() {
|
|
95
|
+
const bunMain = globalThis.Bun?.main;
|
|
96
|
+
if (bunMain?.startsWith("/$bunfs")) return [process.execPath, "daemon"];
|
|
97
|
+
if (bunMain && /(?:^|[/\\])cli\.[cm]?[jt]s$/.test(bunMain)) return [
|
|
98
|
+
process.execPath,
|
|
99
|
+
bunMain,
|
|
100
|
+
"daemon"
|
|
101
|
+
];
|
|
102
|
+
return ["zidane-daemon"];
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* True when something accepts connections on `socketPath`. Distinguishes a
|
|
106
|
+
* live daemon from a stale socket file left by a crashed one.
|
|
107
|
+
*/
|
|
108
|
+
function probeSocket(socketPath, timeoutMs = 1e3) {
|
|
109
|
+
return new Promise((resolve) => {
|
|
110
|
+
try {
|
|
111
|
+
if (!existsSync(socketPath) || !statSync(socketPath).isSocket()) {
|
|
112
|
+
resolve(false);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
} catch {
|
|
116
|
+
resolve(false);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
let socket;
|
|
120
|
+
try {
|
|
121
|
+
socket = connect(socketPath);
|
|
122
|
+
} catch {
|
|
123
|
+
resolve(false);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const done = (alive) => {
|
|
127
|
+
socket.destroy();
|
|
128
|
+
resolve(alive);
|
|
129
|
+
};
|
|
130
|
+
socket.once("connect", () => done(true));
|
|
131
|
+
socket.once("error", () => done(false));
|
|
132
|
+
socket.setTimeout(timeoutMs, () => done(false));
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Make sure a daemon is listening, spawning one if needed. Resolves with
|
|
137
|
+
* the socket path once a connection succeeds; rejects when the daemon
|
|
138
|
+
* failed to come up within `timeoutMs`.
|
|
139
|
+
*/
|
|
140
|
+
async function ensureDaemonRunning(options = {}) {
|
|
141
|
+
const socketPath = options.socketPath ?? defaultSocketPath();
|
|
142
|
+
if (await probeSocket(socketPath)) return socketPath;
|
|
143
|
+
const [cmd, ...args] = options.spawnCommand ?? ["zidane-daemon"];
|
|
144
|
+
const child = spawn(cmd, args, {
|
|
145
|
+
detached: true,
|
|
146
|
+
stdio: "ignore",
|
|
147
|
+
env: {
|
|
148
|
+
...process.env,
|
|
149
|
+
ZIDANE_DAEMON_SOCKET: socketPath
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
let spawnError;
|
|
153
|
+
child.once("error", (err) => {
|
|
154
|
+
spawnError = err;
|
|
155
|
+
});
|
|
156
|
+
child.unref();
|
|
157
|
+
const deadline = Date.now() + (options.timeoutMs ?? 8e3);
|
|
158
|
+
for (;;) {
|
|
159
|
+
if (spawnError !== void 0) throw new Error(`Failed to spawn zidane daemon (${cmd}): ${spawnError.message}`);
|
|
160
|
+
if (await probeSocket(socketPath)) return socketPath;
|
|
161
|
+
if (Date.now() > deadline) throw new Error(`zidane daemon did not come up on ${socketPath} within ${options.timeoutMs ?? 8e3}ms.`);
|
|
162
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
//#endregion
|
|
166
|
+
//#region src/ipc/protocol.ts
|
|
167
|
+
/**
|
|
168
|
+
* v6 (changes since v5):
|
|
169
|
+
* - `event` signals carry an optional `echo` flag marking host-generated
|
|
170
|
+
* SEND/STEER echoes of the user's own message.
|
|
171
|
+
* - `chat.pushQueued` result gained the `'queued'` delivered variant.
|
|
172
|
+
* - `sessions.setModel` now honors `fallbacks`: it persists the canonical
|
|
173
|
+
* `metadata.modelChain` and CLEARS a previously persisted chain when
|
|
174
|
+
* omitted/empty.
|
|
175
|
+
* - additive: `extension.emit` notification + `extension:signal` fan-out
|
|
176
|
+
* (the permission-gated per-extension cross-window bus, `ctx.bus`).
|
|
177
|
+
* Older daemons ignore the unknown notification; older clients drop the
|
|
178
|
+
* unknown signal kind — no version bump needed.
|
|
179
|
+
* - additive: `extension.emit` / `extension:signal` carry an optional
|
|
180
|
+
* `nonce` (the emitter's per-process random id) so receivers dedupe on
|
|
181
|
+
* `(extensionId, nonce, seq)` instead of the collision-prone
|
|
182
|
+
* `(extensionId, seq)`. Older peers omit it and fall back to the
|
|
183
|
+
* seq-only key — no version bump needed.
|
|
184
|
+
* - `SessionRow.modelFallbacks` is now populated from the session's
|
|
185
|
+
* persisted `metadata.modelChain`.
|
|
186
|
+
* - Default socket path moved from `<stateDir>/daemon.sock` to the
|
|
187
|
+
* dedicated `<stateDir>/daemon/daemon.sock` (dir chmod'd 0700) so the
|
|
188
|
+
* daemon can harden its socket dir without touching shared state files.
|
|
189
|
+
* - ADDITIVE (no version bump — older daemons answer "method not
|
|
190
|
+
* found", clients degrade to local compute): `suggest.resolve` RPC
|
|
191
|
+
* resolves inline ghost-text autocomplete daemon-side, and the
|
|
192
|
+
* `suggest:changed` signal fans the result out to every OTHER peer
|
|
193
|
+
* on the session so mirroring windows paint the same ghost without
|
|
194
|
+
* recomputing.
|
|
195
|
+
* - ADDITIVE (no version bump — optional field, older daemons simply
|
|
196
|
+
* omit it and clients fall back to the single `suggestion`):
|
|
197
|
+
* `suggest.resolve` results and `suggest:changed` signals carry the
|
|
198
|
+
* resolver's full ordered `candidates` list so daemon mode keeps
|
|
199
|
+
* ↑/↓ candidate rotation instead of degrading to one suggestion.
|
|
200
|
+
*/
|
|
201
|
+
const IPC_PROTOCOL_VERSION = 8;
|
|
202
|
+
/**
|
|
203
|
+
* JSON-RPC error codes specific to the zidane daemon. Lives here (not in
|
|
204
|
+
* `daemon/server.ts`) so clients can import the codes without pulling the
|
|
205
|
+
* server's module graph.
|
|
206
|
+
*/
|
|
207
|
+
const IpcErrorCode = {
|
|
208
|
+
VersionMismatch: -32010,
|
|
209
|
+
NotHandshaken: -32011
|
|
210
|
+
};
|
|
211
|
+
//#endregion
|
|
212
|
+
//#region src/ipc/client.ts
|
|
213
|
+
var IpcVersionMismatchError = class extends Error {
|
|
214
|
+
daemon;
|
|
215
|
+
constructor(message, daemon) {
|
|
216
|
+
super(message);
|
|
217
|
+
this.daemon = daemon;
|
|
218
|
+
this.name = "IpcVersionMismatchError";
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
async function connectIpcClient(options) {
|
|
222
|
+
const socket = await new Promise((resolvePromise, reject) => {
|
|
223
|
+
const s = connect(options.socketPath);
|
|
224
|
+
s.once("connect", () => {
|
|
225
|
+
s.off("error", reject);
|
|
226
|
+
resolvePromise(s);
|
|
227
|
+
});
|
|
228
|
+
s.once("error", reject);
|
|
229
|
+
});
|
|
230
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
231
|
+
let closed = false;
|
|
232
|
+
const connection = createJsonRpcConnection({
|
|
233
|
+
input: socket,
|
|
234
|
+
output: socket,
|
|
235
|
+
onRequest: (message) => errorResponse(message.id, -32601, "Clients do not accept requests."),
|
|
236
|
+
onNotification: (message) => {
|
|
237
|
+
if (message.method !== "ipc.signal") return;
|
|
238
|
+
const signal = message.params;
|
|
239
|
+
if (!signal) return;
|
|
240
|
+
for (const cb of listeners) cb(signal);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
connection.start();
|
|
244
|
+
const close = () => {
|
|
245
|
+
if (closed) return;
|
|
246
|
+
closed = true;
|
|
247
|
+
connection.close();
|
|
248
|
+
socket.destroy();
|
|
249
|
+
};
|
|
250
|
+
socket.on("close", () => {
|
|
251
|
+
if (closed) return;
|
|
252
|
+
closed = true;
|
|
253
|
+
connection.close();
|
|
254
|
+
options.onClose?.();
|
|
255
|
+
});
|
|
256
|
+
socket.on("error", () => {});
|
|
257
|
+
const HELLO_TIMEOUT_MS = 5e3;
|
|
258
|
+
const REQUEST_TIMEOUT_MS = 3e4;
|
|
259
|
+
const LONG_REQUEST_TIMEOUT_MS = 600 * 1e3;
|
|
260
|
+
/** RPCs that legitimately run long — full compaction, index-backed cross-session search. */
|
|
261
|
+
const LONG_REQUEST_METHODS = /* @__PURE__ */ new Set([
|
|
262
|
+
"sessions.compact",
|
|
263
|
+
"history.searchAll",
|
|
264
|
+
"history.searchSession"
|
|
265
|
+
]);
|
|
266
|
+
let helloTimer;
|
|
267
|
+
let hello;
|
|
268
|
+
try {
|
|
269
|
+
hello = await Promise.race([connection.sendRequest("ipc.hello", {
|
|
270
|
+
protocolVersion: 8,
|
|
271
|
+
appVersion: options.appVersion ?? "dev",
|
|
272
|
+
pid: process.pid,
|
|
273
|
+
buildId: options.drainOnMismatch === false ? "unknown" : IPC_BUILD_ID
|
|
274
|
+
}), new Promise((_, reject) => {
|
|
275
|
+
helloTimer = setTimeout(() => reject(/* @__PURE__ */ new Error(`Daemon handshake timed out after ${HELLO_TIMEOUT_MS}ms on ${options.socketPath}.`)), HELLO_TIMEOUT_MS);
|
|
276
|
+
})]);
|
|
277
|
+
} catch (err) {
|
|
278
|
+
if (err instanceof JsonRpcRemoteError && err.code === IpcErrorCode.VersionMismatch) {
|
|
279
|
+
if (options.drainOnMismatch !== false) await Promise.race([connection.sendRequest("daemon.shutdown", {}).catch(() => void 0), new Promise((resolve) => setTimeout(resolve, 1500))]);
|
|
280
|
+
const daemon = err.data?.daemon;
|
|
281
|
+
close();
|
|
282
|
+
throw new IpcVersionMismatchError(err.message, daemon);
|
|
283
|
+
}
|
|
284
|
+
close();
|
|
285
|
+
throw err;
|
|
286
|
+
} finally {
|
|
287
|
+
clearTimeout(helloTimer);
|
|
288
|
+
}
|
|
289
|
+
return {
|
|
290
|
+
hello,
|
|
291
|
+
notify: (method, params) => {
|
|
292
|
+
try {
|
|
293
|
+
connection.sendNotification(method, params);
|
|
294
|
+
} catch {}
|
|
295
|
+
},
|
|
296
|
+
request: async (method, params) => {
|
|
297
|
+
const timeoutMs = LONG_REQUEST_METHODS.has(method) ? LONG_REQUEST_TIMEOUT_MS : REQUEST_TIMEOUT_MS;
|
|
298
|
+
let timer;
|
|
299
|
+
try {
|
|
300
|
+
return await Promise.race([connection.sendRequest(method, params), new Promise((_, reject) => {
|
|
301
|
+
timer = setTimeout(() => {
|
|
302
|
+
reject(/* @__PURE__ */ new Error(`Daemon request ${method} timed out after ${timeoutMs}ms — tearing the connection down.`));
|
|
303
|
+
socket.destroy();
|
|
304
|
+
}, timeoutMs);
|
|
305
|
+
timer.unref?.();
|
|
306
|
+
})]);
|
|
307
|
+
} finally {
|
|
308
|
+
clearTimeout(timer);
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
onSignal: (cb) => {
|
|
312
|
+
listeners.add(cb);
|
|
313
|
+
return () => listeners.delete(cb);
|
|
314
|
+
},
|
|
315
|
+
close
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* One-call convenience for windows: make sure a daemon is up (spawning if
|
|
320
|
+
* needed), connect, and — when the running daemon speaks an older protocol
|
|
321
|
+
* — drain it and spawn a fresh one before retrying once.
|
|
322
|
+
*/
|
|
323
|
+
async function connectOrSpawnDaemon(options = {}) {
|
|
324
|
+
const socketPath = await ensureDaemonRunning(options);
|
|
325
|
+
try {
|
|
326
|
+
return await connectIpcClient({
|
|
327
|
+
...options,
|
|
328
|
+
socketPath
|
|
329
|
+
});
|
|
330
|
+
} catch (err) {
|
|
331
|
+
if (!(err instanceof IpcVersionMismatchError)) throw err;
|
|
332
|
+
const drainDeadline = Date.now() + 2e3;
|
|
333
|
+
while (await probeSocket(socketPath)) {
|
|
334
|
+
if (Date.now() > drainDeadline) break;
|
|
335
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
336
|
+
}
|
|
337
|
+
const retryPath = await ensureDaemonRunning(options);
|
|
338
|
+
return connectIpcClient({
|
|
339
|
+
...options,
|
|
340
|
+
socketPath: retryPath
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
//#endregion
|
|
345
|
+
//#region src/ipc/daemon/environment.ts
|
|
346
|
+
/**
|
|
347
|
+
* File-backed pending-approvals persistence (`pending-approvals.json` next
|
|
348
|
+
* to `state.json`). Small, rewritten atomically enough for its purpose —
|
|
349
|
+
* the queue holds at most a handful of rows.
|
|
350
|
+
*/
|
|
351
|
+
function createFileApprovalsStore(path) {
|
|
352
|
+
function read() {
|
|
353
|
+
if (!existsSync(path)) return [];
|
|
354
|
+
try {
|
|
355
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
356
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
357
|
+
} catch {
|
|
358
|
+
return [];
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
function write(rows) {
|
|
362
|
+
if (rows.length === 0) {
|
|
363
|
+
try {
|
|
364
|
+
unlinkSync(path);
|
|
365
|
+
} catch {}
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
369
|
+
writeFileAtomic(path, JSON.stringify(rows, null, 2));
|
|
370
|
+
}
|
|
371
|
+
return {
|
|
372
|
+
insert(request) {
|
|
373
|
+
const rows = read().filter((r) => r.id !== request.id);
|
|
374
|
+
rows.push(request);
|
|
375
|
+
write(rows);
|
|
376
|
+
},
|
|
377
|
+
delete(id) {
|
|
378
|
+
write(read().filter((r) => r.id !== id));
|
|
379
|
+
},
|
|
380
|
+
clear() {
|
|
381
|
+
write([]);
|
|
382
|
+
},
|
|
383
|
+
list() {
|
|
384
|
+
return read();
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
async function createDaemonEnvironment(options) {
|
|
389
|
+
const { createTuiStore } = await import("./session/sqlite.js");
|
|
390
|
+
const config = resolveConfig({
|
|
391
|
+
...options.cwd !== void 0 ? { cwd: options.cwd } : {},
|
|
392
|
+
store: (resolved) => createTuiStore(resolved.db)
|
|
393
|
+
});
|
|
394
|
+
const store = config.store;
|
|
395
|
+
if (config.paths.projectDir) process.stderr.write(`[zidane-daemon] warning: project-DB mode is active — this daemon is bound to ${config.paths.db}. Windows opened in other projects will share this project's sessions. Run a per-project daemon (ZIDANE_DAEMON_SOCKET) to isolate them.\n`);
|
|
396
|
+
const readSettings = () => ({
|
|
397
|
+
...DEFAULT_SETTINGS,
|
|
398
|
+
...config.initialSettings,
|
|
399
|
+
...config.stateStore.load().settings ?? {}
|
|
400
|
+
});
|
|
401
|
+
/**
|
|
402
|
+
* Fields the invariant forced (only ever `false`), so the stored file records
|
|
403
|
+
* them too — otherwise a later read of `state.settings` would resurrect the
|
|
404
|
+
* value the invariant just rejected.
|
|
405
|
+
*/
|
|
406
|
+
const pickInvariantFields = (merged, patch) => {
|
|
407
|
+
const out = {};
|
|
408
|
+
for (const row of SETTINGS_TOGGLES) if (row.requires && merged[row.key] === false && patch[row.key] !== false) out[row.key] = false;
|
|
409
|
+
return out;
|
|
410
|
+
};
|
|
411
|
+
const writeSettings = (patch) => {
|
|
412
|
+
const state = config.stateStore.load();
|
|
413
|
+
const merged = enforceSettingsInvariants({
|
|
414
|
+
...DEFAULT_SETTINGS,
|
|
415
|
+
...config.initialSettings,
|
|
416
|
+
...state.settings ?? {},
|
|
417
|
+
...patch
|
|
418
|
+
});
|
|
419
|
+
const next = {
|
|
420
|
+
...state.settings ?? {},
|
|
421
|
+
...patch,
|
|
422
|
+
...pickInvariantFields(merged, patch)
|
|
423
|
+
};
|
|
424
|
+
config.stateStore.save({
|
|
425
|
+
...state,
|
|
426
|
+
settings: next
|
|
427
|
+
});
|
|
428
|
+
return merged;
|
|
429
|
+
};
|
|
430
|
+
const hud = createExtensionHud();
|
|
431
|
+
hud.subscribe(() => {
|
|
432
|
+
const state = hud.getState();
|
|
433
|
+
options.broadcast({
|
|
434
|
+
kind: "hud:state",
|
|
435
|
+
statuses: Object.fromEntries(Object.entries(state.statuses).map(([name, s]) => [name, {
|
|
436
|
+
text: s.text,
|
|
437
|
+
loader: s.loader
|
|
438
|
+
}])),
|
|
439
|
+
banners: Object.fromEntries(Object.entries(state.banners).map(([name, lines]) => [name, [...lines]]))
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
hud.onNotify((n) => {
|
|
443
|
+
options.broadcast({
|
|
444
|
+
kind: "hud:notify",
|
|
445
|
+
extension: n.extension,
|
|
446
|
+
text: n.text,
|
|
447
|
+
tone: n.tone
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
const extensionBus = createExtensionBus();
|
|
451
|
+
extensionBus.onEmit((envelope) => {
|
|
452
|
+
options.broadcast({
|
|
453
|
+
kind: "extension:signal",
|
|
454
|
+
...envelope
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
const RUNTIME_CACHE_MAX = 16;
|
|
458
|
+
const runtimeCache = /* @__PURE__ */ new Map();
|
|
459
|
+
/**
|
|
460
|
+
* Best-effort release of an evicted entry's extension registry
|
|
461
|
+
* (teardown hooks, HUD writers — `registry.dispose()` is idempotent).
|
|
462
|
+
* Fire-and-forget: eviction must never block or throw on a broken
|
|
463
|
+
* extension teardown, and a rejected resolve has nothing to release.
|
|
464
|
+
*/
|
|
465
|
+
const disposeRuntime = (promise) => {
|
|
466
|
+
promise.then((config) => config.extensions.dispose()).catch(() => void 0);
|
|
467
|
+
};
|
|
468
|
+
const runtimeConfig = (cwd) => {
|
|
469
|
+
const settings = readSettings();
|
|
470
|
+
const key = [
|
|
471
|
+
cwd,
|
|
472
|
+
JSON.stringify(settings.enabledExtensions ?? null),
|
|
473
|
+
String(settings.loadProjectExtensions === true)
|
|
474
|
+
].join("\0");
|
|
475
|
+
const existing = runtimeCache.get(key);
|
|
476
|
+
if (existing) {
|
|
477
|
+
runtimeCache.delete(key);
|
|
478
|
+
runtimeCache.set(key, existing);
|
|
479
|
+
return existing;
|
|
480
|
+
}
|
|
481
|
+
const promise = (async () => {
|
|
482
|
+
return resolveConfig({
|
|
483
|
+
cwd,
|
|
484
|
+
extensionRegistry: await discoverAndBuildExtensionRegistry({
|
|
485
|
+
cwd,
|
|
486
|
+
hud,
|
|
487
|
+
bus: extensionBus,
|
|
488
|
+
prefix: config.prefix,
|
|
489
|
+
...settings.enabledExtensions !== void 0 ? { enabledAllowlist: [...settings.enabledExtensions] } : {},
|
|
490
|
+
trustProjectExtensions: settings.loadProjectExtensions === true,
|
|
491
|
+
readSettings
|
|
492
|
+
}),
|
|
493
|
+
store: () => store
|
|
494
|
+
});
|
|
495
|
+
})();
|
|
496
|
+
runtimeCache.set(key, promise);
|
|
497
|
+
while (runtimeCache.size > RUNTIME_CACHE_MAX) {
|
|
498
|
+
const oldest = runtimeCache.keys().next().value;
|
|
499
|
+
if (oldest === void 0) break;
|
|
500
|
+
const evicted = runtimeCache.get(oldest);
|
|
501
|
+
runtimeCache.delete(oldest);
|
|
502
|
+
if (evicted) disposeRuntime(evicted);
|
|
503
|
+
}
|
|
504
|
+
promise.catch(() => {
|
|
505
|
+
if (runtimeCache.get(key) === promise) runtimeCache.delete(key);
|
|
506
|
+
});
|
|
507
|
+
return promise;
|
|
508
|
+
};
|
|
509
|
+
const userDataDir = () => config.paths.userDir;
|
|
510
|
+
const applyAuthEnv = () => {
|
|
511
|
+
const dir = userDataDir();
|
|
512
|
+
process.env.ZIDANE_CREDENTIALS_PATH = credentialsPath(dir);
|
|
513
|
+
applyApiKeyEnv(dir, BUILTIN_PROVIDERS);
|
|
514
|
+
};
|
|
515
|
+
return {
|
|
516
|
+
multiProcess: true,
|
|
517
|
+
broadcast: options.broadcast,
|
|
518
|
+
store,
|
|
519
|
+
readSettings,
|
|
520
|
+
writeSettings,
|
|
521
|
+
runtimeConfig,
|
|
522
|
+
userDataDir,
|
|
523
|
+
applyAuthEnv,
|
|
524
|
+
mcpCredentialStore: () => createFileMcpCredentialStore(userDataDir()),
|
|
525
|
+
approvalsPersistence: createFileApprovalsStore(join(dirname(config.paths.state), "pending-approvals.json")),
|
|
526
|
+
...options.debugStream !== void 0 ? { debugStream: options.debugStream } : {},
|
|
527
|
+
invalidateRuntime: () => {
|
|
528
|
+
for (const entry of runtimeCache.values()) disposeRuntime(entry);
|
|
529
|
+
runtimeCache.clear();
|
|
530
|
+
},
|
|
531
|
+
config,
|
|
532
|
+
extensionBus
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
//#endregion
|
|
536
|
+
//#region src/ipc/daemon/server.ts
|
|
537
|
+
/**
|
|
538
|
+
* Per-peer write backpressure bound. A wedged peer (e.g. a SIGSTOP'd
|
|
539
|
+
* terminal) stops reading its socket, so fan-out writes pile up in
|
|
540
|
+
* `socket.writableLength` without bound and can OOM the daemon. A peer
|
|
541
|
+
* buffering more than this is destroyed — the protocol supports re-attach
|
|
542
|
+
* + backfill, so killing a wedged peer is safe and self-healing.
|
|
543
|
+
*/
|
|
544
|
+
const PEER_WRITE_HIGH_WATERMARK = 8 * 1024 * 1024;
|
|
545
|
+
async function startIpcDaemon(options) {
|
|
546
|
+
const { socketPath } = options;
|
|
547
|
+
const log = options.onLog ?? (() => void 0);
|
|
548
|
+
const startedAt = Date.now();
|
|
549
|
+
const appVersion = options.appVersion ?? "dev";
|
|
550
|
+
const peers = /* @__PURE__ */ new Set();
|
|
551
|
+
/** Sessions with a run in flight, tracked from the run signals themselves. */
|
|
552
|
+
const runningSessions = /* @__PURE__ */ new Set();
|
|
553
|
+
const broadcast = (signal) => {
|
|
554
|
+
broadcastExcept(null, signal);
|
|
555
|
+
switch (signal.kind) {
|
|
556
|
+
case "run:start":
|
|
557
|
+
runningSessions.add(signal.sessionId);
|
|
558
|
+
broadcastPresence();
|
|
559
|
+
break;
|
|
560
|
+
case "run:end":
|
|
561
|
+
runningSessions.delete(signal.sessionId);
|
|
562
|
+
broadcastPresence();
|
|
563
|
+
break;
|
|
564
|
+
case "approval:request":
|
|
565
|
+
case "approval:resolved":
|
|
566
|
+
case "interaction:request":
|
|
567
|
+
case "interaction:resolved":
|
|
568
|
+
broadcastPresence();
|
|
569
|
+
break;
|
|
570
|
+
default: break;
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
/**
|
|
574
|
+
* Fan out to every handshaken peer except `origin`. Draft signals use
|
|
575
|
+
* this so a window's own keystrokes never bounce back and fight its
|
|
576
|
+
* cursor — echo-safety by construction, no client-side dedupe.
|
|
577
|
+
*/
|
|
578
|
+
function broadcastExcept(origin, signal) {
|
|
579
|
+
for (const peer of peers) {
|
|
580
|
+
if (!peer.ready || peer === origin) continue;
|
|
581
|
+
if (peer.socket.writableLength > PEER_WRITE_HIGH_WATERMARK) {
|
|
582
|
+
console.warn(`[ipc-daemon] peer buffered ${peer.socket.writableLength} bytes (> ${PEER_WRITE_HIGH_WATERMARK}) — destroying wedged connection`);
|
|
583
|
+
peer.socket.destroy();
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
try {
|
|
587
|
+
peer.connection.sendNotification("ipc.signal", signal);
|
|
588
|
+
} catch {}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Shared per-session prompt drafts. Ephemeral by design — in-memory
|
|
593
|
+
* only, cleared when any window submits, gone on daemon restart.
|
|
594
|
+
*/
|
|
595
|
+
const drafts = /* @__PURE__ */ new Map();
|
|
596
|
+
/**
|
|
597
|
+
* Bound a peer-supplied attachment list: shape-guarded entries only,
|
|
598
|
+
* 24 MiB of base64 per list. Shared by `queue.add` and
|
|
599
|
+
* `drafts.setAttachments`.
|
|
600
|
+
*/
|
|
601
|
+
function boundAttachments(list) {
|
|
602
|
+
let budget = 24 * 1024 * 1024;
|
|
603
|
+
const out = [];
|
|
604
|
+
for (const entry of list) {
|
|
605
|
+
const a = entry;
|
|
606
|
+
if (typeof a?.mediaType !== "string" || typeof a?.data !== "string") continue;
|
|
607
|
+
budget -= a.data.length;
|
|
608
|
+
if (budget < 0) break;
|
|
609
|
+
out.push({
|
|
610
|
+
mediaType: a.mediaType,
|
|
611
|
+
data: a.data,
|
|
612
|
+
...typeof a.name === "string" ? { name: a.name.slice(0, 256) } : {}
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
return out;
|
|
616
|
+
}
|
|
617
|
+
/** Total base64 bytes currently pinned by ALL draft attachments. */
|
|
618
|
+
function draftAttachmentBytes() {
|
|
619
|
+
let total = 0;
|
|
620
|
+
for (const entry of drafts.values()) for (const attachment of entry.attachments ?? []) total += attachment.data.length;
|
|
621
|
+
return total;
|
|
622
|
+
}
|
|
623
|
+
/** Consume the draft after a submit — every window except the sender clears. */
|
|
624
|
+
function clearDraft(sessionId, origin) {
|
|
625
|
+
const entry = drafts.get(sessionId);
|
|
626
|
+
if (!entry) return;
|
|
627
|
+
drafts.delete(sessionId);
|
|
628
|
+
if (entry.text) broadcastExcept(origin, {
|
|
629
|
+
kind: "draft:changed",
|
|
630
|
+
sessionId,
|
|
631
|
+
text: ""
|
|
632
|
+
});
|
|
633
|
+
if (entry.attachments?.length) broadcastExcept(origin, {
|
|
634
|
+
kind: "draft:attachments",
|
|
635
|
+
sessionId,
|
|
636
|
+
attachments: []
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Shared inline-autocomplete state — per-session latest-wins
|
|
641
|
+
* coalescing for `suggest.resolve`. `seq` is the newest sequence seen
|
|
642
|
+
* (stale requests return without computing); `controller` aborts an
|
|
643
|
+
* in-flight compute when a newer request supersedes it (the GUI's
|
|
644
|
+
* controller-map pattern, daemon-side). Bounded like `drafts`: keys
|
|
645
|
+
* only evict on session delete, so cap the map at 256 sessions.
|
|
646
|
+
*/
|
|
647
|
+
const suggests = /* @__PURE__ */ new Map();
|
|
648
|
+
/** Drop empty draft entries so the map only holds live composing state. */
|
|
649
|
+
function compactDraft(sessionId, entry) {
|
|
650
|
+
if (!entry.text && !entry.shell && !entry.attachments?.length && !entry.search?.active) drafts.delete(sessionId);
|
|
651
|
+
else drafts.set(sessionId, entry);
|
|
652
|
+
}
|
|
653
|
+
const env = await (options.createEnvironment ? options.createEnvironment(broadcast) : createDaemonEnvironment({ broadcast }));
|
|
654
|
+
const host = createChatHost(env);
|
|
655
|
+
/**
|
|
656
|
+
* Live presence: which sessions have attached windows, and their
|
|
657
|
+
* run / awaiting-input state. Union of watched, live-agent, and
|
|
658
|
+
* pending-HITL sessions — small by construction.
|
|
659
|
+
*/
|
|
660
|
+
function computePresence() {
|
|
661
|
+
const windowCounts = /* @__PURE__ */ new Map();
|
|
662
|
+
for (const peer of peers) if (peer.ready && peer.watching) windowCounts.set(peer.watching, (windowCounts.get(peer.watching) ?? 0) + 1);
|
|
663
|
+
const attention = /* @__PURE__ */ new Set();
|
|
664
|
+
for (const request of host.approvals.listPending()) attention.add(request.sessionId);
|
|
665
|
+
for (const pending of host.interactions.list()) attention.add(pending.sessionId);
|
|
666
|
+
const live = new Map(host.listLiveSessions().map((entry) => [entry.sessionId, entry.running]));
|
|
667
|
+
return [.../* @__PURE__ */ new Set([
|
|
668
|
+
...windowCounts.keys(),
|
|
669
|
+
...attention,
|
|
670
|
+
...live.keys(),
|
|
671
|
+
...runningSessions
|
|
672
|
+
])].map((sessionId) => ({
|
|
673
|
+
sessionId,
|
|
674
|
+
windows: windowCounts.get(sessionId) ?? 0,
|
|
675
|
+
running: runningSessions.has(sessionId) || live.get(sessionId) === true,
|
|
676
|
+
attention: attention.has(sessionId)
|
|
677
|
+
}));
|
|
678
|
+
}
|
|
679
|
+
function broadcastPresence() {
|
|
680
|
+
broadcastExcept(null, {
|
|
681
|
+
kind: "presence:changed",
|
|
682
|
+
sessions: computePresence()
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
const SEARCH_INDEX_CACHE_MAX = 64;
|
|
686
|
+
/** Latest `history.searchAll` generation per peer — see the handler. */
|
|
687
|
+
const searchGenerations = /* @__PURE__ */ new WeakMap();
|
|
688
|
+
const searchIndexCache = /* @__PURE__ */ new Map();
|
|
689
|
+
async function sessionSearchIndex(sessionId) {
|
|
690
|
+
const liveTurns = host.getLiveTurns(sessionId);
|
|
691
|
+
let turns;
|
|
692
|
+
let key;
|
|
693
|
+
if (liveTurns) {
|
|
694
|
+
turns = [...liveTurns];
|
|
695
|
+
key = `live:${turns.length}:${turns.at(-1)?.id ?? ""}`;
|
|
696
|
+
} else {
|
|
697
|
+
const data = await env.store.load(sessionId);
|
|
698
|
+
if (!data) return null;
|
|
699
|
+
turns = data.turns;
|
|
700
|
+
key = `store:${data.updatedAt}:${data.turns.length}`;
|
|
701
|
+
}
|
|
702
|
+
const cached = searchIndexCache.get(sessionId);
|
|
703
|
+
if (cached?.key === key) {
|
|
704
|
+
searchIndexCache.delete(sessionId);
|
|
705
|
+
searchIndexCache.set(sessionId, cached);
|
|
706
|
+
return cached.index;
|
|
707
|
+
}
|
|
708
|
+
const index = await buildSessionSearchIndexAsync(turns);
|
|
709
|
+
searchIndexCache.set(sessionId, {
|
|
710
|
+
key,
|
|
711
|
+
index
|
|
712
|
+
});
|
|
713
|
+
while (searchIndexCache.size > SEARCH_INDEX_CACHE_MAX) {
|
|
714
|
+
const oldest = searchIndexCache.keys().next().value;
|
|
715
|
+
if (oldest === void 0) break;
|
|
716
|
+
searchIndexCache.delete(oldest);
|
|
717
|
+
}
|
|
718
|
+
return index;
|
|
719
|
+
}
|
|
720
|
+
host.approvals.rehydrate();
|
|
721
|
+
const store = env.store;
|
|
722
|
+
function toRow(data) {
|
|
723
|
+
const meta = data.metadata ?? {};
|
|
724
|
+
const title = deriveSessionTitle(data.turns, meta);
|
|
725
|
+
return {
|
|
726
|
+
id: data.id,
|
|
727
|
+
title,
|
|
728
|
+
...isMainSession(meta) ? { isMain: true } : {},
|
|
729
|
+
projectRoot: data.projectRoot ?? null,
|
|
730
|
+
cwd: typeof meta.cwd === "string" ? meta.cwd : data.projectRoot ?? "",
|
|
731
|
+
...typeof data.agentId === "string" ? { agentId: data.agentId } : {},
|
|
732
|
+
status: host.isSessionRunning(data.id) ? "running" : data.status === "running" && Date.now() - data.updatedAt >= 3e5 ? "idle" : data.status,
|
|
733
|
+
createdAt: data.createdAt,
|
|
734
|
+
updatedAt: data.updatedAt,
|
|
735
|
+
archived: meta.archived === true,
|
|
736
|
+
...typeof meta.providerKey === "string" ? { providerKey: meta.providerKey } : {},
|
|
737
|
+
...typeof meta.modelId === "string" ? { modelId: meta.modelId } : {},
|
|
738
|
+
...typeof meta.effort === "string" ? { effort: meta.effort } : {},
|
|
739
|
+
...meta.modelOptions && typeof meta.modelOptions === "object" ? { modelOptions: meta.modelOptions } : {},
|
|
740
|
+
...(() => {
|
|
741
|
+
const fallbacks = sessionModelFallbacks(meta);
|
|
742
|
+
return fallbacks ? { modelFallbacks: fallbacks } : {};
|
|
743
|
+
})()
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
async function loadOrThrow(sessionId) {
|
|
747
|
+
const data = await store.load(sessionId);
|
|
748
|
+
if (!data) throw new Error(`Session "${sessionId}" not found.`);
|
|
749
|
+
return data;
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Apply a metadata-only patch. Prefers the host's live-session path (safe
|
|
753
|
+
* mid-run — routes through the in-memory `Session` whose turns are the
|
|
754
|
+
* source of truth); only falls back to a store-level full-document
|
|
755
|
+
* `save()` when the session is idle, where no run is appending turns so
|
|
756
|
+
* `load → mutate → save` can't drop concurrently-persisted turns.
|
|
757
|
+
*
|
|
758
|
+
* `bumpUpdatedAt: false` skips the recency bump so a rename doesn't yank
|
|
759
|
+
* the row to the top of recency-sorted lists (mirrors the GUI).
|
|
760
|
+
*/
|
|
761
|
+
async function patchSessionMeta(sessionId, patch, opts = {}) {
|
|
762
|
+
if (await host.patchLiveSessionMeta(sessionId, patch)) {
|
|
763
|
+
broadcast({
|
|
764
|
+
kind: "sessions:changed",
|
|
765
|
+
op: "update",
|
|
766
|
+
sessionId
|
|
767
|
+
});
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
const data = await loadOrThrow(sessionId);
|
|
771
|
+
for (const [key, value] of Object.entries(patch)) data.metadata[key] = value;
|
|
772
|
+
if (opts.bumpUpdatedAt !== false) data.updatedAt = Date.now();
|
|
773
|
+
await store.save(data);
|
|
774
|
+
broadcast({
|
|
775
|
+
kind: "sessions:changed",
|
|
776
|
+
op: "update",
|
|
777
|
+
sessionId
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
async function patchSession(sessionId, mutate) {
|
|
781
|
+
const data = await loadOrThrow(sessionId);
|
|
782
|
+
mutate(data);
|
|
783
|
+
data.updatedAt = Date.now();
|
|
784
|
+
await store.save(data);
|
|
785
|
+
broadcast({
|
|
786
|
+
kind: "sessions:changed",
|
|
787
|
+
op: "update",
|
|
788
|
+
sessionId
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
async function createSessionRow(params) {
|
|
792
|
+
const id = await host.createSession(params);
|
|
793
|
+
const data = await loadOrThrow(id);
|
|
794
|
+
broadcast({
|
|
795
|
+
kind: "sessions:changed",
|
|
796
|
+
op: "create",
|
|
797
|
+
sessionId: id
|
|
798
|
+
});
|
|
799
|
+
return toRow(data);
|
|
800
|
+
}
|
|
801
|
+
const methods = {
|
|
802
|
+
"chat.send": async (p, peer) => {
|
|
803
|
+
const { wake: _stripped, ...params } = p;
|
|
804
|
+
const result = await host.sendPrompt(params);
|
|
805
|
+
clearDraft(p.sessionId, peer);
|
|
806
|
+
return result;
|
|
807
|
+
},
|
|
808
|
+
"chat.interrupt": (p) => host.interrupt(p.sessionId),
|
|
809
|
+
"chat.steer": (p) => host.steer(p.sessionId, p.message),
|
|
810
|
+
"chat.followUp": (p) => host.followUp(p.sessionId, p.message),
|
|
811
|
+
"chat.pushQueued": async (p, peer) => {
|
|
812
|
+
const result = await host.pushQueued(p.sessionId, p.message, p.activatedSkills);
|
|
813
|
+
clearDraft(p.sessionId, peer);
|
|
814
|
+
return result;
|
|
815
|
+
},
|
|
816
|
+
"commands.list": (p) => host.listCommands(p.cwd),
|
|
817
|
+
"commands.completeArgs": (p) => host.completeCommandArgs(p.cwd, p.name, p.argsPrefix),
|
|
818
|
+
"queue.list": (p) => host.queueList(p.sessionId),
|
|
819
|
+
"sessions.systemMd": (p) => host.sessionSystemMd(p.sessionId),
|
|
820
|
+
"queue.add": async (p, peer) => {
|
|
821
|
+
const text = typeof p.text === "string" ? p.text.slice(0, 64 * 1024) : "";
|
|
822
|
+
const activatedSkills = Array.isArray(p.activatedSkills) ? p.activatedSkills.filter((name) => typeof name === "string") : void 0;
|
|
823
|
+
const attachments = Array.isArray(p.attachments) ? boundAttachments(p.attachments) : void 0;
|
|
824
|
+
const result = await host.queueAdd(p.sessionId, text, activatedSkills, attachments);
|
|
825
|
+
clearDraft(p.sessionId, peer);
|
|
826
|
+
return result;
|
|
827
|
+
},
|
|
828
|
+
"sessions.contextSegments": (p) => host.getContextSegments(p.sessionId),
|
|
829
|
+
"sessions.createSystemOverride": (p) => host.createSystemOverride(p.sessionId),
|
|
830
|
+
"queue.remove": (p) => host.queueRemove(p.sessionId, p.id),
|
|
831
|
+
"queue.pushNow": (p) => host.queuePushNow(p.sessionId, p.id),
|
|
832
|
+
"drafts.get": (p) => {
|
|
833
|
+
const entry = drafts.get(p.sessionId);
|
|
834
|
+
return {
|
|
835
|
+
text: entry?.text ?? "",
|
|
836
|
+
...entry?.shell ? { shell: true } : {},
|
|
837
|
+
...entry?.attachments?.length ? { attachments: entry.attachments } : {},
|
|
838
|
+
...entry?.search?.active ? { search: entry.search } : {}
|
|
839
|
+
};
|
|
840
|
+
},
|
|
841
|
+
"drafts.set": (p, peer) => {
|
|
842
|
+
const text = typeof p.text === "string" ? p.text.slice(0, 64 * 1024) : "";
|
|
843
|
+
const truncated = typeof p.text === "string" && p.text.length > text.length;
|
|
844
|
+
if (!drafts.has(p.sessionId) && drafts.size >= 256) return;
|
|
845
|
+
const entry = drafts.get(p.sessionId) ?? {};
|
|
846
|
+
entry.text = text.length > 0 ? text : void 0;
|
|
847
|
+
entry.shell = p.shell === true ? true : void 0;
|
|
848
|
+
compactDraft(p.sessionId, entry);
|
|
849
|
+
broadcastExcept(truncated ? null : peer, {
|
|
850
|
+
kind: "draft:changed",
|
|
851
|
+
sessionId: p.sessionId,
|
|
852
|
+
text,
|
|
853
|
+
...p.shell === true ? { shell: true } : {}
|
|
854
|
+
});
|
|
855
|
+
},
|
|
856
|
+
"search.set": (p, peer) => {
|
|
857
|
+
const active = p.active === true;
|
|
858
|
+
const query = typeof p.query === "string" ? p.query.slice(0, 4 * 1024) : "";
|
|
859
|
+
if (!drafts.has(p.sessionId) && drafts.size >= 256) return;
|
|
860
|
+
const entry = drafts.get(p.sessionId) ?? {};
|
|
861
|
+
entry.search = active || query.length > 0 ? {
|
|
862
|
+
active,
|
|
863
|
+
query
|
|
864
|
+
} : void 0;
|
|
865
|
+
compactDraft(p.sessionId, entry);
|
|
866
|
+
broadcastExcept(peer, {
|
|
867
|
+
kind: "search:changed",
|
|
868
|
+
sessionId: p.sessionId,
|
|
869
|
+
active,
|
|
870
|
+
query
|
|
871
|
+
});
|
|
872
|
+
},
|
|
873
|
+
"extension.emit": async (p, peer) => {
|
|
874
|
+
const extensionId = typeof p.extensionId === "string" ? p.extensionId : "";
|
|
875
|
+
if (extensionId.length === 0 || extensionId.length > 256) return;
|
|
876
|
+
if (typeof p.seq !== "number" || !Number.isFinite(p.seq)) return;
|
|
877
|
+
const sessionId = typeof p.sessionId === "string" ? p.sessionId : void 0;
|
|
878
|
+
const bytes = serializedPayloadBytes(p.payload);
|
|
879
|
+
if (bytes === null || bytes > 65536) return;
|
|
880
|
+
let registry = env.config.extensions;
|
|
881
|
+
if (sessionId !== void 0) try {
|
|
882
|
+
const data = await loadOrThrow(sessionId);
|
|
883
|
+
const meta = data.metadata ?? {};
|
|
884
|
+
const cwd = typeof meta.cwd === "string" ? meta.cwd : data.projectRoot ?? "";
|
|
885
|
+
registry = (await env.runtimeConfig(cwd)).extensions;
|
|
886
|
+
} catch {
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
if (!registry.loaded.some((ext) => !ext.error && ext.name === extensionId && ext.definition?.permissions?.includes("ipc") === true)) return;
|
|
890
|
+
const nonce = typeof p.nonce === "string" ? p.nonce : void 0;
|
|
891
|
+
const envelope = {
|
|
892
|
+
extensionId,
|
|
893
|
+
...sessionId !== void 0 ? { sessionId } : {},
|
|
894
|
+
payload: p.payload,
|
|
895
|
+
seq: p.seq,
|
|
896
|
+
...nonce !== void 0 ? { nonce } : {}
|
|
897
|
+
};
|
|
898
|
+
env.extensionBus?.deliver(envelope);
|
|
899
|
+
broadcastExcept(peer, {
|
|
900
|
+
kind: "extension:signal",
|
|
901
|
+
...envelope
|
|
902
|
+
});
|
|
903
|
+
},
|
|
904
|
+
"suggest.resolve": async (p, peer) => {
|
|
905
|
+
const sessionId = String(p.sessionId);
|
|
906
|
+
const seq = typeof p.seq === "number" ? p.seq : 0;
|
|
907
|
+
const text = typeof p.text === "string" ? p.text.slice(0, 64 * 1024) : "";
|
|
908
|
+
const caret = typeof p.caret === "number" ? Math.max(0, Math.min(Math.floor(p.caret), text.length)) : text.length;
|
|
909
|
+
const prev = suggests.get(sessionId);
|
|
910
|
+
if (prev && seq < prev.seq) return {
|
|
911
|
+
suggestion: null,
|
|
912
|
+
seq
|
|
913
|
+
};
|
|
914
|
+
if (!prev && suggests.size >= 256) return {
|
|
915
|
+
suggestion: null,
|
|
916
|
+
seq
|
|
917
|
+
};
|
|
918
|
+
prev?.controller?.abort();
|
|
919
|
+
const controller = new AbortController();
|
|
920
|
+
const entry = {
|
|
921
|
+
seq,
|
|
922
|
+
controller
|
|
923
|
+
};
|
|
924
|
+
suggests.set(sessionId, entry);
|
|
925
|
+
try {
|
|
926
|
+
const data = await loadOrThrow(sessionId);
|
|
927
|
+
const meta = data.metadata ?? {};
|
|
928
|
+
const cwd = typeof meta.cwd === "string" ? meta.cwd : data.projectRoot ?? "";
|
|
929
|
+
const config = await env.runtimeConfig(cwd);
|
|
930
|
+
const result = await resolveInlineSuggestion([createPathAutocompleteProvider({ cwd: () => cwd }), ...config.extensions.inlineAutocompleteProviders], {
|
|
931
|
+
text,
|
|
932
|
+
cursor: caret,
|
|
933
|
+
cwd,
|
|
934
|
+
signal: controller.signal
|
|
935
|
+
});
|
|
936
|
+
if (controller.signal.aborted || suggests.get(sessionId) !== entry) return {
|
|
937
|
+
suggestion: null,
|
|
938
|
+
seq
|
|
939
|
+
};
|
|
940
|
+
const candidates = result ? [...result.suffixes] : [];
|
|
941
|
+
const suggestion = candidates[0] ?? null;
|
|
942
|
+
broadcastExcept(peer, {
|
|
943
|
+
kind: "suggest:changed",
|
|
944
|
+
sessionId,
|
|
945
|
+
seq,
|
|
946
|
+
basis: text,
|
|
947
|
+
caret,
|
|
948
|
+
suggestion,
|
|
949
|
+
...candidates.length > 0 ? { candidates } : {}
|
|
950
|
+
});
|
|
951
|
+
return {
|
|
952
|
+
suggestion,
|
|
953
|
+
seq,
|
|
954
|
+
...candidates.length > 0 ? { candidates } : {}
|
|
955
|
+
};
|
|
956
|
+
} finally {
|
|
957
|
+
if (suggests.get(sessionId) === entry) entry.controller = null;
|
|
958
|
+
}
|
|
959
|
+
},
|
|
960
|
+
"drafts.setAttachments": (p, peer) => {
|
|
961
|
+
const bounded = Array.isArray(p.attachments) ? boundAttachments(p.attachments) : [];
|
|
962
|
+
if (!drafts.has(p.sessionId) && drafts.size >= 256) return;
|
|
963
|
+
const currentBytes = (drafts.get(p.sessionId)?.attachments ?? []).reduce((sum, a) => sum + a.data.length, 0);
|
|
964
|
+
const incomingBytes = bounded.reduce((sum, a) => sum + a.data.length, 0);
|
|
965
|
+
if (draftAttachmentBytes() - currentBytes + incomingBytes > 96 * 1024 * 1024) return;
|
|
966
|
+
const entry = drafts.get(p.sessionId) ?? {};
|
|
967
|
+
entry.attachments = bounded.length > 0 ? bounded : void 0;
|
|
968
|
+
compactDraft(p.sessionId, entry);
|
|
969
|
+
broadcastExcept(peer, {
|
|
970
|
+
kind: "draft:attachments",
|
|
971
|
+
sessionId: p.sessionId,
|
|
972
|
+
attachments: bounded
|
|
973
|
+
});
|
|
974
|
+
},
|
|
975
|
+
"chat.isRunning": (p) => host.isSessionRunning(p.sessionId),
|
|
976
|
+
"chat.rebuildAgent": (p) => host.disposeAgentForRebuild(p.sessionId),
|
|
977
|
+
"chat.listWork": (p) => host.listRunningWork(p.sessionId),
|
|
978
|
+
"chat.cancelWork": (p) => host.cancelRunningWork(p.sessionId, p.id, p.reason),
|
|
979
|
+
"sessions.list": async (p) => {
|
|
980
|
+
const filter = p?.projectRoot !== void 0 ? { projectRoot: p.projectRoot } : void 0;
|
|
981
|
+
const applyArchive = (rows) => {
|
|
982
|
+
const filtered = rows.filter((row) => {
|
|
983
|
+
if (p?.archive === "active") return !row.archived;
|
|
984
|
+
if (p?.archive === "archived") return row.archived;
|
|
985
|
+
return true;
|
|
986
|
+
});
|
|
987
|
+
filtered.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
988
|
+
return filtered;
|
|
989
|
+
};
|
|
990
|
+
if (store.listMeta) return applyArchive((await store.listMeta(filter)).map((meta) => {
|
|
991
|
+
const md = meta.metadata;
|
|
992
|
+
const title = deriveSessionTitle(meta.firstUserTurn ? [meta.firstUserTurn] : [], md);
|
|
993
|
+
return {
|
|
994
|
+
id: meta.id,
|
|
995
|
+
title,
|
|
996
|
+
projectRoot: meta.projectRoot ?? null,
|
|
997
|
+
cwd: typeof md.cwd === "string" ? md.cwd : meta.projectRoot ?? "",
|
|
998
|
+
...meta.agentId ? { agentId: meta.agentId } : {},
|
|
999
|
+
status: host.isSessionRunning(meta.id) ? "running" : meta.status === "running" && Date.now() - meta.updatedAt >= 3e5 ? "idle" : meta.status ?? "idle",
|
|
1000
|
+
createdAt: meta.createdAt ?? 0,
|
|
1001
|
+
updatedAt: meta.updatedAt,
|
|
1002
|
+
archived: md.archived === true,
|
|
1003
|
+
...typeof md.providerKey === "string" ? { providerKey: md.providerKey } : {},
|
|
1004
|
+
...typeof md.modelId === "string" ? { modelId: md.modelId } : {},
|
|
1005
|
+
...typeof md.effort === "string" ? { effort: md.effort } : {},
|
|
1006
|
+
...md.modelOptions && typeof md.modelOptions === "object" ? { modelOptions: md.modelOptions } : {},
|
|
1007
|
+
...(() => {
|
|
1008
|
+
const fallbacks = sessionModelFallbacks(md);
|
|
1009
|
+
return fallbacks ? { modelFallbacks: fallbacks } : {};
|
|
1010
|
+
})()
|
|
1011
|
+
};
|
|
1012
|
+
}));
|
|
1013
|
+
const ids = await store.list(filter);
|
|
1014
|
+
const rows = [];
|
|
1015
|
+
for (const id of ids) {
|
|
1016
|
+
const data = await store.load(id);
|
|
1017
|
+
if (!data) continue;
|
|
1018
|
+
rows.push(toRow(data));
|
|
1019
|
+
}
|
|
1020
|
+
return applyArchive(rows);
|
|
1021
|
+
},
|
|
1022
|
+
"sessions.create": (p) => createSessionRow(p),
|
|
1023
|
+
"sessions.delete": async (p) => {
|
|
1024
|
+
drafts.delete(p.sessionId);
|
|
1025
|
+
suggests.get(p.sessionId)?.controller?.abort();
|
|
1026
|
+
suggests.delete(p.sessionId);
|
|
1027
|
+
host.disposeSession(p.sessionId);
|
|
1028
|
+
await store.delete(p.sessionId);
|
|
1029
|
+
broadcast({
|
|
1030
|
+
kind: "sessions:changed",
|
|
1031
|
+
op: "delete",
|
|
1032
|
+
sessionId: p.sessionId
|
|
1033
|
+
});
|
|
1034
|
+
},
|
|
1035
|
+
"sessions.setTitle": (p) => patchSessionMeta(p.sessionId, { title: p.title }, { bumpUpdatedAt: false }),
|
|
1036
|
+
"sessions.setArchived": (p) => patchSessionMeta(p.sessionId, { archived: p.archived }),
|
|
1037
|
+
"sessions.setAgentId": async (p) => {
|
|
1038
|
+
if (await host.setLiveSessionAgentId(p.sessionId, p.agentId)) broadcast({
|
|
1039
|
+
kind: "sessions:changed",
|
|
1040
|
+
op: "update",
|
|
1041
|
+
sessionId: p.sessionId
|
|
1042
|
+
});
|
|
1043
|
+
else await patchSession(p.sessionId, (d) => {
|
|
1044
|
+
d.agentId = p.agentId;
|
|
1045
|
+
});
|
|
1046
|
+
host.disposeAgentForRebuild(p.sessionId);
|
|
1047
|
+
},
|
|
1048
|
+
"sessions.setEffort": (p) => patchSessionMeta(p.sessionId, { effort: p.effort }),
|
|
1049
|
+
"sessions.setModel": async (p) => {
|
|
1050
|
+
const chainPatch = p.fallbacks === void 0 ? {} : { modelChain: modelChainMetadata(p.providerKey, p.modelId, p.fallbacks) };
|
|
1051
|
+
await patchSessionMeta(p.sessionId, {
|
|
1052
|
+
providerKey: p.providerKey,
|
|
1053
|
+
modelId: p.modelId,
|
|
1054
|
+
...chainPatch
|
|
1055
|
+
});
|
|
1056
|
+
host.disposeAgentForRebuild(p.sessionId);
|
|
1057
|
+
},
|
|
1058
|
+
"sessions.setModelOptions": (p) => patchSessionMeta(p.sessionId, { modelOptions: p.options }),
|
|
1059
|
+
"sessions.loadEvents": async (p) => {
|
|
1060
|
+
const data = await loadOrThrow(p.sessionId);
|
|
1061
|
+
return eventsFromTurns(data.turns, data.runs);
|
|
1062
|
+
},
|
|
1063
|
+
"sessions.getTurn": async (p) => {
|
|
1064
|
+
return (await store.load(p.sessionId))?.turns.find((t) => t.id === p.turnId) ?? null;
|
|
1065
|
+
},
|
|
1066
|
+
"sessions.contextBreakdown": (p) => host.getContextBreakdown(p.sessionId),
|
|
1067
|
+
"history.searchSession": async (p) => {
|
|
1068
|
+
const index = await sessionSearchIndex(p.sessionId);
|
|
1069
|
+
if (!index) return [];
|
|
1070
|
+
const limit = Number.isInteger(p.limit) && p.limit > 0 ? Math.min(p.limit, index.turns.length) : index.turns.length;
|
|
1071
|
+
const matches = await searchSessionIndexAsync(index, p.query, { limit });
|
|
1072
|
+
const order = new Map(index.turns.map((entry, i) => [entry.turn.id, i]));
|
|
1073
|
+
matches.sort((a, b) => (order.get(a.turn.id) ?? 0) - (order.get(b.turn.id) ?? 0));
|
|
1074
|
+
return matches;
|
|
1075
|
+
},
|
|
1076
|
+
"history.searchAll": async (p, peer) => {
|
|
1077
|
+
const generation = (searchGenerations.get(peer) ?? 0) + 1;
|
|
1078
|
+
searchGenerations.set(peer, generation);
|
|
1079
|
+
const results = {};
|
|
1080
|
+
const ids = Array.isArray(p.sessionIds) ? p.sessionIds.slice(0, 500) : [];
|
|
1081
|
+
for (const sessionId of ids) {
|
|
1082
|
+
if (searchGenerations.get(peer) !== generation) break;
|
|
1083
|
+
try {
|
|
1084
|
+
const index = await sessionSearchIndex(sessionId);
|
|
1085
|
+
if (!index) continue;
|
|
1086
|
+
const match = await searchSessionHistoryIndexAsync(index, p.query, p.excerptLimit ?? 3);
|
|
1087
|
+
if (match) results[sessionId] = match;
|
|
1088
|
+
} catch {}
|
|
1089
|
+
}
|
|
1090
|
+
return results;
|
|
1091
|
+
},
|
|
1092
|
+
"sessions.touch": (p) => {
|
|
1093
|
+
broadcast({
|
|
1094
|
+
kind: "sessions:changed",
|
|
1095
|
+
op: "update",
|
|
1096
|
+
sessionId: p.sessionId
|
|
1097
|
+
});
|
|
1098
|
+
},
|
|
1099
|
+
"sessions.compact": (p) => host.compactSessionNow(p.sessionId),
|
|
1100
|
+
"todos.get": (p) => {
|
|
1101
|
+
const state = host.getTodosSnapshot(p.sessionId);
|
|
1102
|
+
return state ? toTodosSnapshot(state) : null;
|
|
1103
|
+
},
|
|
1104
|
+
"todos.set": async (p) => {
|
|
1105
|
+
const state = await host.setTodos(p.sessionId, p.items);
|
|
1106
|
+
return state ? toTodosSnapshot(state) : null;
|
|
1107
|
+
},
|
|
1108
|
+
"orchestration.main": () => host.orchestration.ensureMain(),
|
|
1109
|
+
"orchestration.createWorker": ({ mainId, ...params }) => host.orchestration.createWorker(mainId, params),
|
|
1110
|
+
"orchestration.list": (p) => host.orchestration.list(p.mainId, p.limit),
|
|
1111
|
+
"orchestration.read": async (p) => await host.orchestration.canOrchestrate(p.mainId, p.sessionId) ? host.orchestration.read(p.sessionId) : null,
|
|
1112
|
+
"orchestration.send": (p) => host.orchestration.send(p.mainId, p.sessionId, p.prompt, p.wake),
|
|
1113
|
+
"orchestration.steer": (p) => host.orchestration.steer(p.mainId, p.sessionId, p.message),
|
|
1114
|
+
"orchestration.cancel": (p) => host.orchestration.cancel(p.mainId, p.sessionId),
|
|
1115
|
+
"orchestration.transcript": (p) => host.orchestration.transcript(p.mainId, p.sessionId, p.limit),
|
|
1116
|
+
"interactions.list": (p) => host.interactions.list(p?.sessionId),
|
|
1117
|
+
"interactions.respond": (p) => host.interactions.respond(p.requestId, p.response),
|
|
1118
|
+
"interactions.cancel": (p) => host.interactions.cancel(p.requestId, p.reason),
|
|
1119
|
+
"interactions.rehydrate": (p) => host.interactions.rehydrate(p.sessionId),
|
|
1120
|
+
"approvals.resolve": (p) => host.approvals.resolve(p.requestId, p.decision),
|
|
1121
|
+
"approvals.denyAll": (p) => host.approvals.denyAll(p?.sessionId),
|
|
1122
|
+
"approvals.listPending": () => host.approvals.listPending(),
|
|
1123
|
+
"settings.get": () => env.readSettings(),
|
|
1124
|
+
"settings.set": (p) => {
|
|
1125
|
+
const before = env.readSettings();
|
|
1126
|
+
const next = env.writeSettings(p.patch);
|
|
1127
|
+
const changed = (key) => JSON.stringify(before[key]) !== JSON.stringify(next[key]);
|
|
1128
|
+
if (changed("enabledExtensions") || changed("loadProjectExtensions")) {
|
|
1129
|
+
env.invalidateRuntime();
|
|
1130
|
+
host.shutdown();
|
|
1131
|
+
} else if (changed("enabledMcps") || changed("disabledMcpTools") || changed("enabledSkills") || changed("enabledPlugins") || changed("importForeignConfigs") || changed("importRulerConfigs")) host.shutdown();
|
|
1132
|
+
if (changed("interactionMode") && next.interactionMode === "skip") host.interactions.resolveAutomatically();
|
|
1133
|
+
broadcast({
|
|
1134
|
+
kind: "settings:changed",
|
|
1135
|
+
settings: next
|
|
1136
|
+
});
|
|
1137
|
+
return next;
|
|
1138
|
+
},
|
|
1139
|
+
"presence.list": () => computePresence(),
|
|
1140
|
+
"presence.watch": (p, peer) => {
|
|
1141
|
+
peer.watching = p.sessionId;
|
|
1142
|
+
broadcastPresence();
|
|
1143
|
+
store.load(p.sessionId).then((data) => {
|
|
1144
|
+
if (!data) return;
|
|
1145
|
+
const meta = data.metadata ?? {};
|
|
1146
|
+
const cwd = typeof meta.cwd === "string" ? meta.cwd : data.projectRoot ?? "";
|
|
1147
|
+
return env.runtimeConfig(cwd);
|
|
1148
|
+
}).catch(() => void 0);
|
|
1149
|
+
},
|
|
1150
|
+
"daemon.info": () => ({
|
|
1151
|
+
protocolVersion: 8,
|
|
1152
|
+
appVersion,
|
|
1153
|
+
pid: process.pid,
|
|
1154
|
+
startedAt
|
|
1155
|
+
}),
|
|
1156
|
+
"daemon.shutdown": async () => {
|
|
1157
|
+
setImmediate(() => {
|
|
1158
|
+
close().then(() => options.onIdle?.());
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
};
|
|
1162
|
+
let idleTimer;
|
|
1163
|
+
function armIdleTimer() {
|
|
1164
|
+
if (!options.idleShutdownMs || !options.onIdle) return;
|
|
1165
|
+
clearTimeout(idleTimer);
|
|
1166
|
+
idleTimer = setTimeout(() => {
|
|
1167
|
+
if (peers.size > 0) return;
|
|
1168
|
+
if (host.hasActiveWork()) {
|
|
1169
|
+
armIdleTimer();
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
options.onIdle?.();
|
|
1173
|
+
}, options.idleShutdownMs);
|
|
1174
|
+
}
|
|
1175
|
+
function handleConnection(socket) {
|
|
1176
|
+
const peer = {
|
|
1177
|
+
socket,
|
|
1178
|
+
connection: void 0,
|
|
1179
|
+
ready: false,
|
|
1180
|
+
watching: null
|
|
1181
|
+
};
|
|
1182
|
+
const connection = createJsonRpcConnection({
|
|
1183
|
+
input: socket,
|
|
1184
|
+
output: socket,
|
|
1185
|
+
onRequest: async (message) => {
|
|
1186
|
+
if (message.method === "ipc.hello") {
|
|
1187
|
+
const params = message.params ?? {};
|
|
1188
|
+
const clientBuild = Number(params.buildId);
|
|
1189
|
+
const daemonBuild = Number(IPC_BUILD_ID);
|
|
1190
|
+
const staleBuild = params.buildId !== void 0 && params.buildId !== "unknown" && IPC_BUILD_ID !== "unknown" && Number.isFinite(clientBuild) && Number.isFinite(daemonBuild) && clientBuild > daemonBuild;
|
|
1191
|
+
if (params.protocolVersion !== 8 || staleBuild) return errorResponse(message.id, IpcErrorCode.VersionMismatch, staleBuild ? `Build mismatch: the daemon was started from an older build (daemon ${IPC_BUILD_ID}, client ${params.buildId}). Draining for respawn.` : `Protocol version mismatch: daemon speaks v8, client sent v${params.protocolVersion ?? "?"}.`, { daemon: {
|
|
1192
|
+
protocolVersion: 8,
|
|
1193
|
+
appVersion,
|
|
1194
|
+
pid: process.pid,
|
|
1195
|
+
startedAt,
|
|
1196
|
+
buildId: IPC_BUILD_ID
|
|
1197
|
+
} });
|
|
1198
|
+
peer.ready = true;
|
|
1199
|
+
const result = {
|
|
1200
|
+
protocolVersion: 8,
|
|
1201
|
+
appVersion,
|
|
1202
|
+
pid: process.pid,
|
|
1203
|
+
startedAt,
|
|
1204
|
+
buildId: IPC_BUILD_ID
|
|
1205
|
+
};
|
|
1206
|
+
return successResponse(message.id, result);
|
|
1207
|
+
}
|
|
1208
|
+
if (!peer.ready && message.method !== "daemon.shutdown" && message.method !== "daemon.info") return errorResponse(message.id, IpcErrorCode.NotHandshaken, "Handshake required: send ipc.hello first.");
|
|
1209
|
+
const handler = methods[message.method];
|
|
1210
|
+
if (!handler) return errorResponse(message.id, -32601, `Unknown method "${message.method}".`);
|
|
1211
|
+
try {
|
|
1212
|
+
const result = await handler(message.params, peer);
|
|
1213
|
+
return successResponse(message.id, result ?? null);
|
|
1214
|
+
} catch (err) {
|
|
1215
|
+
return errorResponse(message.id, -32e3, err instanceof Error ? err.message : String(err));
|
|
1216
|
+
}
|
|
1217
|
+
},
|
|
1218
|
+
onNotification: async (message) => {
|
|
1219
|
+
if (!peer.ready) return;
|
|
1220
|
+
const handler = methods[message.method];
|
|
1221
|
+
if (!handler) return;
|
|
1222
|
+
try {
|
|
1223
|
+
await handler(message.params, peer);
|
|
1224
|
+
} catch {}
|
|
1225
|
+
},
|
|
1226
|
+
onError: (err) => log(`[ipc-daemon] peer error: ${err.message}`)
|
|
1227
|
+
});
|
|
1228
|
+
peer.connection = connection;
|
|
1229
|
+
peers.add(peer);
|
|
1230
|
+
connection.start();
|
|
1231
|
+
log(`[ipc-daemon] peer connected (${peers.size} total)`);
|
|
1232
|
+
const drop = () => {
|
|
1233
|
+
if (!peers.delete(peer)) return;
|
|
1234
|
+
connection.close();
|
|
1235
|
+
log(`[ipc-daemon] peer disconnected (${peers.size} left)`);
|
|
1236
|
+
if (peer.watching) broadcastPresence();
|
|
1237
|
+
if (peers.size === 0) armIdleTimer();
|
|
1238
|
+
};
|
|
1239
|
+
socket.on("close", drop);
|
|
1240
|
+
socket.on("error", drop);
|
|
1241
|
+
}
|
|
1242
|
+
if (existsSync(socketPath)) {
|
|
1243
|
+
if (await probeSocket(socketPath)) throw new Error(`A zidane daemon is already listening on ${socketPath}.`);
|
|
1244
|
+
if (await probeSocket(socketPath)) throw new Error(`A zidane daemon is already listening on ${socketPath}.`);
|
|
1245
|
+
unlinkSync(socketPath);
|
|
1246
|
+
}
|
|
1247
|
+
const socketDir = dirname(socketPath);
|
|
1248
|
+
const dedicatedSocketDir = basename(socketDir) === "daemon" || !existsSync(socketDir);
|
|
1249
|
+
mkdirSync(socketDir, {
|
|
1250
|
+
recursive: true,
|
|
1251
|
+
mode: 448
|
|
1252
|
+
});
|
|
1253
|
+
if (dedicatedSocketDir) try {
|
|
1254
|
+
chmodSync(socketDir, 448);
|
|
1255
|
+
} catch {}
|
|
1256
|
+
const server = createServer(handleConnection);
|
|
1257
|
+
await new Promise((resolvePromise, reject) => {
|
|
1258
|
+
server.once("error", reject);
|
|
1259
|
+
server.listen(socketPath, () => {
|
|
1260
|
+
server.off("error", reject);
|
|
1261
|
+
resolvePromise();
|
|
1262
|
+
});
|
|
1263
|
+
});
|
|
1264
|
+
try {
|
|
1265
|
+
chmodSync(socketPath, 384);
|
|
1266
|
+
} catch {}
|
|
1267
|
+
log(`[ipc-daemon] listening on ${socketPath}`);
|
|
1268
|
+
let ownsSocketFile = true;
|
|
1269
|
+
armIdleTimer();
|
|
1270
|
+
const legacyPath = legacySocketPath(socketPath);
|
|
1271
|
+
if (legacyPath) (async () => {
|
|
1272
|
+
if (!await probeSocket(legacyPath)) return;
|
|
1273
|
+
log(`[ipc-daemon] draining legacy daemon on ${legacyPath}`);
|
|
1274
|
+
try {
|
|
1275
|
+
const survivor = await connectIpcClient({
|
|
1276
|
+
socketPath: legacyPath,
|
|
1277
|
+
appVersion: `${appVersion}/legacy-drain`,
|
|
1278
|
+
drainOnMismatch: true
|
|
1279
|
+
});
|
|
1280
|
+
await survivor.request("daemon.shutdown", {}).catch(() => void 0);
|
|
1281
|
+
survivor.close();
|
|
1282
|
+
} catch {}
|
|
1283
|
+
})();
|
|
1284
|
+
async function doClose() {
|
|
1285
|
+
clearTimeout(idleTimer);
|
|
1286
|
+
host.shutdown();
|
|
1287
|
+
for (const peer of peers) {
|
|
1288
|
+
peer.connection.close();
|
|
1289
|
+
peer.socket.destroy();
|
|
1290
|
+
}
|
|
1291
|
+
peers.clear();
|
|
1292
|
+
await new Promise((resolvePromise) => {
|
|
1293
|
+
server.close(() => resolvePromise());
|
|
1294
|
+
});
|
|
1295
|
+
if (ownsSocketFile) {
|
|
1296
|
+
ownsSocketFile = false;
|
|
1297
|
+
try {
|
|
1298
|
+
unlinkSync(socketPath);
|
|
1299
|
+
} catch {}
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
let closePromise = null;
|
|
1303
|
+
function close() {
|
|
1304
|
+
closePromise ??= doClose();
|
|
1305
|
+
return closePromise;
|
|
1306
|
+
}
|
|
1307
|
+
return {
|
|
1308
|
+
socketPath,
|
|
1309
|
+
host,
|
|
1310
|
+
peerCount: () => peers.size,
|
|
1311
|
+
close
|
|
1312
|
+
};
|
|
1313
|
+
}
|
|
1314
|
+
//#endregion
|
|
1315
|
+
export { connectIpcClient as a, IpcErrorCode as c, ensureDaemonRunning as d, probeSocket as f, IpcVersionMismatchError as i, IPC_BUILD_ID as l, createDaemonEnvironment as n, connectOrSpawnDaemon as o, selfDaemonSpawnCommand as p, createFileApprovalsStore as r, IPC_PROTOCOL_VERSION as s, startIpcDaemon as t, defaultSocketPath as u };
|
|
1316
|
+
|
|
1317
|
+
//# sourceMappingURL=server-DJqp9dIR.js.map
|