crbro-memory 2.10.0 → 2.11.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/LICENSE +21 -21
- package/README.md +256 -465
- package/SECURITY.md +114 -12
- package/bin/crbro.mjs +539 -300
- package/dist/build-id.json +10 -0
- package/dist/cli/common.js +211 -0
- package/dist/cli/export.js +522 -0
- package/dist/cli/import.js +683 -0
- package/dist/cli/markdown.js +313 -0
- package/dist/cli/mcpclient.js +107 -0
- package/dist/cli/serve.js +453 -0
- package/dist/cli/view.js +358 -0
- package/dist/cli/viewer-page.js +378 -0
- package/dist/daemon/daemon.js +119 -11
- package/dist/daemon/endpoint.js +222 -21
- package/dist/daemon/lines.js +102 -13
- package/dist/daemon/proxy.js +480 -125
- package/dist/daemon/surface.js +116 -0
- package/dist/daemon/trace.js +73 -0
- package/dist/engine/atrest.js +132 -0
- package/dist/engine/backup.js +139 -19
- package/dist/engine/brain.js +22 -6
- package/dist/engine/codexconfig.js +476 -0
- package/dist/engine/cortex.js +667 -232
- package/dist/engine/doctor.js +312 -14
- package/dist/engine/heat.js +81 -37
- package/dist/engine/hippocampus.js +18 -7
- package/dist/engine/hookrefresh.js +296 -0
- package/dist/engine/hookverify.js +15 -3
- package/dist/engine/installer.js +1022 -0
- package/dist/engine/installio.js +279 -0
- package/dist/engine/keychain.js +721 -162
- package/dist/engine/maintenance.js +355 -87
- package/dist/engine/modinstall.js +98 -45
- package/dist/engine/prefrontal.js +29 -6
- package/dist/engine/quarantine.js +204 -0
- package/dist/engine/secrets.js +41 -5
- package/dist/engine/shelf.js +25 -3
- package/dist/engine/synapses.js +183 -80
- package/dist/engine/triggers.js +141 -11
- package/dist/index.js +37 -3
- package/dist/mcp-surface.json +1 -0
- package/dist/miner/extractor.js +64 -17
- package/dist/miner/scheduler.js +144 -68
- package/dist/search/index.js +797 -176
- package/dist/search/semantic.js +208 -18
- package/dist/search/tokenize.js +44 -0
- package/dist/server.js +583 -373
- package/dist/sync/git.js +203 -72
- package/dist/sync/space.js +100 -25
- package/dist/sync/validate.js +204 -0
- package/dist/utils/budget.js +15 -2
- package/dist/utils/fs.js +40 -6
- package/dist/utils/ids.js +128 -14
- package/dist/utils/lock.js +94 -7
- package/hooks/crbro-codex.mjs +284 -0
- package/hooks/crbro-guard.mjs +281 -173
- package/hooks/crbro-hooks.shipped.json +20 -0
- package/hooks/crbro-lifecycle.mjs +16 -4
- package/hooks/crbro-subagent.mjs +180 -180
- package/package.json +9 -5
- package/dist/daemon/daemon.d.ts +0 -25
- package/dist/daemon/daemon.d.ts.map +0 -1
- package/dist/daemon/daemon.js.map +0 -1
- package/dist/daemon/endpoint.d.ts +0 -66
- package/dist/daemon/endpoint.d.ts.map +0 -1
- package/dist/daemon/endpoint.js.map +0 -1
- package/dist/daemon/lines.d.ts +0 -55
- package/dist/daemon/lines.d.ts.map +0 -1
- package/dist/daemon/lines.js.map +0 -1
- package/dist/daemon/proxy.d.ts +0 -55
- package/dist/daemon/proxy.d.ts.map +0 -1
- package/dist/daemon/proxy.js.map +0 -1
- package/dist/engine/backup.d.ts +0 -68
- package/dist/engine/backup.d.ts.map +0 -1
- package/dist/engine/backup.js.map +0 -1
- package/dist/engine/brain.d.ts +0 -79
- package/dist/engine/brain.d.ts.map +0 -1
- package/dist/engine/brain.js.map +0 -1
- package/dist/engine/cortex.d.ts +0 -682
- package/dist/engine/cortex.d.ts.map +0 -1
- package/dist/engine/cortex.js.map +0 -1
- package/dist/engine/dates.d.ts +0 -28
- package/dist/engine/dates.d.ts.map +0 -1
- package/dist/engine/dates.js.map +0 -1
- package/dist/engine/doctor.d.ts +0 -93
- package/dist/engine/doctor.d.ts.map +0 -1
- package/dist/engine/doctor.js.map +0 -1
- package/dist/engine/evidence.d.ts +0 -26
- package/dist/engine/evidence.d.ts.map +0 -1
- package/dist/engine/evidence.js.map +0 -1
- package/dist/engine/heat.d.ts +0 -28
- package/dist/engine/heat.d.ts.map +0 -1
- package/dist/engine/heat.js.map +0 -1
- package/dist/engine/hippocampus.d.ts +0 -60
- package/dist/engine/hippocampus.d.ts.map +0 -1
- package/dist/engine/hippocampus.js.map +0 -1
- package/dist/engine/hookverify.d.ts +0 -42
- package/dist/engine/hookverify.d.ts.map +0 -1
- package/dist/engine/hookverify.js.map +0 -1
- package/dist/engine/keychain.d.ts +0 -32
- package/dist/engine/keychain.d.ts.map +0 -1
- package/dist/engine/keychain.js.map +0 -1
- package/dist/engine/maintenance.d.ts +0 -218
- package/dist/engine/maintenance.d.ts.map +0 -1
- package/dist/engine/maintenance.js.map +0 -1
- package/dist/engine/modinstall.d.ts +0 -215
- package/dist/engine/modinstall.d.ts.map +0 -1
- package/dist/engine/modinstall.js.map +0 -1
- package/dist/engine/postmortem.d.ts +0 -81
- package/dist/engine/postmortem.d.ts.map +0 -1
- package/dist/engine/postmortem.js.map +0 -1
- package/dist/engine/prefrontal.d.ts +0 -64
- package/dist/engine/prefrontal.d.ts.map +0 -1
- package/dist/engine/prefrontal.js.map +0 -1
- package/dist/engine/projects.d.ts +0 -20
- package/dist/engine/projects.d.ts.map +0 -1
- package/dist/engine/projects.js.map +0 -1
- package/dist/engine/secrets.d.ts +0 -30
- package/dist/engine/secrets.d.ts.map +0 -1
- package/dist/engine/secrets.js.map +0 -1
- package/dist/engine/shelf.d.ts +0 -155
- package/dist/engine/shelf.d.ts.map +0 -1
- package/dist/engine/shelf.js.map +0 -1
- package/dist/engine/source.d.ts +0 -6
- package/dist/engine/source.d.ts.map +0 -1
- package/dist/engine/source.js.map +0 -1
- package/dist/engine/synapses.d.ts +0 -82
- package/dist/engine/synapses.d.ts.map +0 -1
- package/dist/engine/synapses.js.map +0 -1
- package/dist/engine/triggers.d.ts +0 -53
- package/dist/engine/triggers.d.ts.map +0 -1
- package/dist/engine/triggers.js.map +0 -1
- package/dist/engine/usage.d.ts +0 -61
- package/dist/engine/usage.d.ts.map +0 -1
- package/dist/engine/usage.js.map +0 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/miner/extractor.d.ts +0 -12
- package/dist/miner/extractor.d.ts.map +0 -1
- package/dist/miner/extractor.js.map +0 -1
- package/dist/miner/index.d.ts +0 -61
- package/dist/miner/index.d.ts.map +0 -1
- package/dist/miner/index.js.map +0 -1
- package/dist/miner/scheduler.d.ts +0 -30
- package/dist/miner/scheduler.d.ts.map +0 -1
- package/dist/miner/scheduler.js.map +0 -1
- package/dist/miner/types.d.ts +0 -11
- package/dist/miner/types.d.ts.map +0 -1
- package/dist/miner/types.js.map +0 -1
- package/dist/search/index.d.ts +0 -328
- package/dist/search/index.d.ts.map +0 -1
- package/dist/search/index.js.map +0 -1
- package/dist/search/quality.d.ts +0 -18
- package/dist/search/quality.d.ts.map +0 -1
- package/dist/search/quality.js.map +0 -1
- package/dist/search/semantic.d.ts +0 -80
- package/dist/search/semantic.d.ts.map +0 -1
- package/dist/search/semantic.js.map +0 -1
- package/dist/search/synonyms.d.ts +0 -2
- package/dist/search/synonyms.d.ts.map +0 -1
- package/dist/search/synonyms.js.map +0 -1
- package/dist/search/tokenize.d.ts +0 -39
- package/dist/search/tokenize.d.ts.map +0 -1
- package/dist/search/tokenize.js.map +0 -1
- package/dist/server.d.ts +0 -41
- package/dist/server.d.ts.map +0 -1
- package/dist/server.js.map +0 -1
- package/dist/sync/git.d.ts +0 -36
- package/dist/sync/git.d.ts.map +0 -1
- package/dist/sync/git.js.map +0 -1
- package/dist/sync/materialize.d.ts +0 -36
- package/dist/sync/materialize.d.ts.map +0 -1
- package/dist/sync/materialize.js.map +0 -1
- package/dist/sync/ops.d.ts +0 -147
- package/dist/sync/ops.d.ts.map +0 -1
- package/dist/sync/ops.js.map +0 -1
- package/dist/sync/space.d.ts +0 -118
- package/dist/sync/space.d.ts.map +0 -1
- package/dist/sync/space.js.map +0 -1
- package/dist/types/index.d.ts +0 -369
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js.map +0 -1
- package/dist/utils/budget.d.ts +0 -44
- package/dist/utils/budget.d.ts.map +0 -1
- package/dist/utils/budget.js.map +0 -1
- package/dist/utils/fs.d.ts +0 -59
- package/dist/utils/fs.d.ts.map +0 -1
- package/dist/utils/fs.js.map +0 -1
- package/dist/utils/hash.d.ts +0 -20
- package/dist/utils/hash.d.ts.map +0 -1
- package/dist/utils/hash.js.map +0 -1
- package/dist/utils/ids.d.ts +0 -47
- package/dist/utils/ids.d.ts.map +0 -1
- package/dist/utils/ids.js.map +0 -1
- package/dist/utils/lock.d.ts +0 -13
- package/dist/utils/lock.d.ts.map +0 -1
- package/dist/utils/lock.js.map +0 -1
- package/dist/utils/transcripts.d.ts +0 -63
- package/dist/utils/transcripts.d.ts.map +0 -1
- package/dist/utils/transcripts.js.map +0 -1
- package/dist/version.d.ts +0 -26
- package/dist/version.d.ts.map +0 -1
- package/dist/version.js.map +0 -1
package/dist/daemon/proxy.js
CHANGED
|
@@ -5,17 +5,22 @@
|
|
|
5
5
|
// always has. In daemon mode that process loads no index and no model: it
|
|
6
6
|
// finds the daemon (or starts it), proves who it is, and from then on copies
|
|
7
7
|
// lines — client to daemon, daemon to client. It reads each line only to keep
|
|
8
|
-
//
|
|
8
|
+
// what it needs on the worst day:
|
|
9
9
|
//
|
|
10
|
-
// ·
|
|
11
|
-
//
|
|
10
|
+
// · `initialize`, `ping` and `tools/list` it answers itself, at once: their
|
|
11
|
+
// answers are fixed per build (surface.ts) and a client must not wait for
|
|
12
|
+
// a daemon to be found or started to learn them;
|
|
13
|
+
// · the client's `initialize`, which it replays — under an id of its own,
|
|
14
|
+
// answer swallowed — on every backend it attaches to, the first included;
|
|
12
15
|
// · which requests are in flight, so each gets an error instead of a
|
|
13
16
|
// silence that would hang the conversation until a timeout;
|
|
14
17
|
// · nothing else.
|
|
15
18
|
//
|
|
16
|
-
// And if no daemon can be had
|
|
19
|
+
// And if no daemon can be had in time, the backend is a server inside this
|
|
17
20
|
// process — CRBRO exactly as it was before 2.5. The daemon is an optimisation;
|
|
18
|
-
// the memory is not allowed to depend on it.
|
|
21
|
+
// the memory is not allowed to depend on it. Nor is that fallback for life:
|
|
22
|
+
// the proxy keeps looking, quietly, and moves back to a daemon once one
|
|
23
|
+
// answers and nothing is running here.
|
|
19
24
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
20
25
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
21
26
|
};
|
|
@@ -32,25 +37,40 @@ const lines_js_1 = require("./lines.js");
|
|
|
32
37
|
const endpoint_js_1 = require("./endpoint.js");
|
|
33
38
|
const CONNECT_TIMEOUT_MS = 1_500;
|
|
34
39
|
const HANDSHAKE_TIMEOUT_MS = 4_000;
|
|
40
|
+
/** The whole budget for a daemon (finding it, starting it, its handshake) before serving in-process. Every attempt fits inside it. */
|
|
35
41
|
const SPAWN_WAIT_MS = 12_000;
|
|
36
42
|
const POLL_MS = 200;
|
|
37
|
-
/** Daemon connections lost inside this window before the proxy stops
|
|
43
|
+
/** Daemon connections lost inside this window before the proxy stops trying daemons for a while. */
|
|
38
44
|
const FLAP_WINDOW_MS = 60_000;
|
|
39
45
|
const FLAP_LIMIT = 3;
|
|
46
|
+
/** How long "a while" is, the first time; it doubles on each new burst, up to the cap. Never for ever. */
|
|
47
|
+
const FLAP_COOLDOWN_MS = 60_000;
|
|
48
|
+
const MAX_COOLDOWN_MS = 10 * 60_000;
|
|
49
|
+
/** Serving in-process: how often to look for a daemon again (doubling between the two). */
|
|
50
|
+
const PROBE_MIN_MS = 2_000;
|
|
51
|
+
const PROBE_MAX_MS = 60_000;
|
|
52
|
+
const PROBE_BUDGET_MS = 3_000;
|
|
53
|
+
/** A daemon that refused calls because it is stopping, and then did not go: how long before we stop waiting for it. */
|
|
54
|
+
const STOPPING_GRACE_MS = 15_000;
|
|
40
55
|
/** After the client closes its end: how long calls already accepted may take to finish before we leave anyway. */
|
|
41
56
|
const DRAIN_TIMEOUT_MS = 30_000;
|
|
57
|
+
const LOST_MESSAGE = 'CRBRO restarted while this call was running, before answering it. The call may or may not have taken effect: check (crbro_recall or crbro_inspect) before repeating a write.';
|
|
42
58
|
/**
|
|
43
59
|
* Connect to the daemon of this brain and build, and get through the
|
|
44
60
|
* handshake. Null on any failure; 'config-mismatch' when the daemon is
|
|
45
61
|
* genuine but was started with settings that are not this client's.
|
|
62
|
+
* `deadline` (epoch ms) bounds the whole attempt: no step waits past it.
|
|
46
63
|
*/
|
|
47
64
|
async function connectDaemon(brainRoot, build = (0, endpoint_js_1.buildId)(), options = {}) {
|
|
65
|
+
const left = () => (options.deadline === undefined ? Infinity : options.deadline - Date.now());
|
|
66
|
+
if (left() <= 0)
|
|
67
|
+
return null;
|
|
48
68
|
const state = await (0, endpoint_js_1.readState)(brainRoot, build);
|
|
49
69
|
if (!state)
|
|
50
70
|
return null;
|
|
51
71
|
const socket = await new Promise(resolve => {
|
|
52
72
|
const s = node_net_1.default.connect(state.endpoint);
|
|
53
|
-
const t = setTimeout(() => { s.destroy(); resolve(null); }, CONNECT_TIMEOUT_MS);
|
|
73
|
+
const t = setTimeout(() => { s.destroy(); resolve(null); }, Math.max(1, Math.min(CONNECT_TIMEOUT_MS, left())));
|
|
54
74
|
s.once('connect', () => { clearTimeout(t); resolve(s); });
|
|
55
75
|
s.once('error', () => { clearTimeout(t); resolve(null); });
|
|
56
76
|
});
|
|
@@ -62,8 +82,11 @@ async function connectDaemon(brainRoot, build = (0, endpoint_js_1.buildId)(), op
|
|
|
62
82
|
socket.on('data', chunk => lines.push(typeof chunk === 'string' ? Buffer.from(chunk, 'utf8') : chunk));
|
|
63
83
|
socket.on('error', () => undefined);
|
|
64
84
|
const next = () => new Promise(resolve => {
|
|
65
|
-
|
|
66
|
-
const done = (v) => {
|
|
85
|
+
let settled = false;
|
|
86
|
+
const done = (v) => { if (settled)
|
|
87
|
+
return; settled = true; clearTimeout(t); socket.off('close', onClose); resolve(v); };
|
|
88
|
+
// An answer that arrived while this loop was busy is read before the timeout is believed.
|
|
89
|
+
const t = setTimeout(() => setImmediate(() => done(null)), Math.max(1, Math.min(HANDSHAKE_TIMEOUT_MS, left())));
|
|
67
90
|
const onClose = () => done(null);
|
|
68
91
|
socket.once('close', onClose);
|
|
69
92
|
sink = line => { sink = () => undefined; try {
|
|
@@ -123,37 +146,7 @@ function spawnDetachedDaemon(brainRoot) {
|
|
|
123
146
|
child.on('error', () => undefined);
|
|
124
147
|
child.unref();
|
|
125
148
|
}
|
|
126
|
-
|
|
127
|
-
async function daemonBackend(o, ev, resumed) {
|
|
128
|
-
const build = o.build ?? (0, endpoint_js_1.buildId)();
|
|
129
|
-
let c = await connectDaemon(o.brainRoot, build, { resumed });
|
|
130
|
-
if (!c && o.spawnDaemon) {
|
|
131
|
-
const since = Date.now() - 1_000;
|
|
132
|
-
try {
|
|
133
|
-
await o.spawnDaemon();
|
|
134
|
-
}
|
|
135
|
-
catch (err) {
|
|
136
|
-
o.log?.(`could not start a daemon: ${err.message}`);
|
|
137
|
-
}
|
|
138
|
-
const until = Date.now() + (o.spawnWaitMs ?? SPAWN_WAIT_MS);
|
|
139
|
-
while (!c && Date.now() < until) {
|
|
140
|
-
await new Promise(r => setTimeout(r, POLL_MS));
|
|
141
|
-
c = await connectDaemon(o.brainRoot, build, { resumed });
|
|
142
|
-
if (!c) {
|
|
143
|
-
const why = await (0, endpoint_js_1.blockedSince)(o.brainRoot, build, since);
|
|
144
|
-
if (why) {
|
|
145
|
-
o.log?.(`the daemon could not take its endpoint (${why}): serving from this process`);
|
|
146
|
-
return 'unusable';
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
if (c === 'config-mismatch') {
|
|
152
|
-
o.log?.('a daemon is running with other settings than this client\'s (CRBRO_SEMANTIC, CRBRO_BACKUP_DIR…): serving from this process so they keep their meaning');
|
|
153
|
-
return 'unusable';
|
|
154
|
-
}
|
|
155
|
-
if (!c)
|
|
156
|
-
return null;
|
|
149
|
+
function linkBackend(c, ev) {
|
|
157
150
|
const { socket, reader } = c;
|
|
158
151
|
let closing = false;
|
|
159
152
|
reader(ev.onLine);
|
|
@@ -175,13 +168,47 @@ async function daemonBackend(o, ev, resumed) {
|
|
|
175
168
|
}),
|
|
176
169
|
};
|
|
177
170
|
}
|
|
171
|
+
/** 'unusable': a daemon exists but is not for us, or cannot be had — do not wait for one. */
|
|
172
|
+
async function daemonBackend(a, ev) {
|
|
173
|
+
let c = await connectDaemon(a.brainRoot, a.build, { resumed: a.resumed, deadline: a.deadline });
|
|
174
|
+
if (!c && a.spawn) {
|
|
175
|
+
const since = Date.now() - 1_000;
|
|
176
|
+
try {
|
|
177
|
+
await a.spawn();
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
a.log(`could not start a daemon: ${err.message}`);
|
|
181
|
+
}
|
|
182
|
+
a.trace('daemon launched', '', true);
|
|
183
|
+
while (!c && Date.now() < a.deadline) {
|
|
184
|
+
await new Promise(r => setTimeout(r, Math.max(1, Math.min(POLL_MS, a.deadline - Date.now()))));
|
|
185
|
+
c = await connectDaemon(a.brainRoot, a.build, { resumed: a.resumed, deadline: a.deadline });
|
|
186
|
+
if (!c) {
|
|
187
|
+
const why = await (0, endpoint_js_1.blockedSince)(a.brainRoot, a.build, since);
|
|
188
|
+
if (why) {
|
|
189
|
+
a.log(`the daemon could not start (${why}): serving from this process`);
|
|
190
|
+
return 'unusable';
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (c === 'config-mismatch') {
|
|
196
|
+
if (!a.quiet)
|
|
197
|
+
a.log('a daemon is running with other settings than this client\'s (CRBRO_SEMANTIC, CRBRO_BACKUP_DIR…): serving from this process so they keep their meaning');
|
|
198
|
+
return 'unusable';
|
|
199
|
+
}
|
|
200
|
+
if (!c)
|
|
201
|
+
return null;
|
|
202
|
+
return linkBackend(c, ev);
|
|
203
|
+
}
|
|
178
204
|
/** CRBRO as it was before daemons: the whole server, in this process. */
|
|
179
205
|
async function localBackend(ev, resumed) {
|
|
180
206
|
const { createEngines, createServer } = await import('../server.js');
|
|
181
207
|
const { sessionScope, newTally } = await import('../engine/cortex.js');
|
|
182
208
|
const engines = createEngines();
|
|
183
209
|
const transport = new lines_js_1.LoopbackTransport(ev.onLine);
|
|
184
|
-
|
|
210
|
+
const server = createServer(engines);
|
|
211
|
+
await server.connect(transport);
|
|
185
212
|
// After a lost daemon this server starts with an empty tally that the
|
|
186
213
|
// conversation's earlier writes are not in: mark it, so consolidate says so.
|
|
187
214
|
const scope = resumed ? { tally: newTally(), resumed: true } : null;
|
|
@@ -194,47 +221,102 @@ async function localBackend(ev, resumed) {
|
|
|
194
221
|
await engines.searchEngine.flush();
|
|
195
222
|
}
|
|
196
223
|
catch { /* derived data */ }
|
|
224
|
+
try {
|
|
225
|
+
await server.close();
|
|
226
|
+
}
|
|
227
|
+
catch { /* leaving anyway */ }
|
|
197
228
|
},
|
|
198
229
|
};
|
|
199
230
|
}
|
|
231
|
+
const isRequest = (m) => !!m && typeof m.method === 'string' && m.id !== undefined && m.id !== null;
|
|
232
|
+
const requestIdOf = (line) => {
|
|
233
|
+
try {
|
|
234
|
+
const q = JSON.parse(line);
|
|
235
|
+
return isRequest(q) ? q.id : null;
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
};
|
|
200
241
|
function runProxy(o) {
|
|
201
242
|
const log = o.log ?? (() => undefined);
|
|
243
|
+
const trace = o.trace ?? (() => undefined);
|
|
202
244
|
const isEnabled = o.isEnabled ?? (() => true);
|
|
203
245
|
const makeLocal = o.local ?? localBackend;
|
|
246
|
+
// Once, here: a proxy that recomputed it at every reconnection could pick up
|
|
247
|
+
// a newer release unpacked in place and launch a daemon of code it never ran.
|
|
248
|
+
const build = o.build ?? (0, endpoint_js_1.buildId)();
|
|
249
|
+
const budgetMs = o.spawnWaitMs ?? SPAWN_WAIT_MS;
|
|
250
|
+
const probeMin = o.probeMs?.min ?? PROBE_MIN_MS;
|
|
251
|
+
const probeMax = o.probeMs?.max ?? PROBE_MAX_MS;
|
|
252
|
+
// ── The backend, and where it is in its own handshake ──
|
|
204
253
|
let backend = null;
|
|
205
|
-
let
|
|
254
|
+
let gen = 0; // every backend attempt gets a number; only the active one is listened to
|
|
255
|
+
let activeGen = -1;
|
|
256
|
+
let pendingGen = -1; // a daemon link held while moving over to it
|
|
257
|
+
let pending = null;
|
|
258
|
+
let phase = 'none';
|
|
259
|
+
let backendInitDone = false;
|
|
260
|
+
let acquiring = true;
|
|
261
|
+
let switching = false;
|
|
206
262
|
let inputEnded = false; // the client closed its end: no more requests, but what it asked for is still owed
|
|
207
263
|
let ended = false; // we are done: nothing more is written, nothing more is started
|
|
208
264
|
let reconnects = 0;
|
|
209
|
-
let
|
|
265
|
+
let switches = 0;
|
|
266
|
+
let distrustUntil = 0;
|
|
267
|
+
let cooldownMs = o.flapCooldownMs ?? FLAP_COOLDOWN_MS;
|
|
210
268
|
const losses = [];
|
|
211
|
-
|
|
212
|
-
let
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
let
|
|
269
|
+
// ── What the client said that we must remember ──
|
|
270
|
+
let surface = o.surface;
|
|
271
|
+
const early = []; // lines that arrived before we knew whether we have a surface
|
|
272
|
+
let clientInit = null;
|
|
273
|
+
let initAnswered = false; // the client has its answer to initialize (ours, or a backend's)
|
|
274
|
+
let clientInitialized = null;
|
|
275
|
+
const inflight = new Map(); // sent to the backend, unanswered: id → the line, should it need sending again
|
|
276
|
+
const waiting = []; // client lines held while there is no ready backend
|
|
277
|
+
const toolCalls = new Set();
|
|
278
|
+
let replayId = null;
|
|
216
279
|
let replays = 0;
|
|
280
|
+
// The client's last crbro_boot that succeeded: the project it bound lives on
|
|
281
|
+
// the backend that served it, so a new backend gets it again (2.11).
|
|
282
|
+
let lastBoot = null;
|
|
283
|
+
const bootCalls = new Map(); // crbro_boot calls sent, unanswered → their params
|
|
284
|
+
let rebootId = null;
|
|
285
|
+
let requeueAt = 0; // where the next call a stopping daemon never started goes back in line
|
|
217
286
|
let drainTimer = null;
|
|
287
|
+
let probeTimer = null;
|
|
288
|
+
let probeDelay = probeMin;
|
|
289
|
+
let stoppingTimer = null;
|
|
290
|
+
let idleWaiters = [];
|
|
218
291
|
let resolveClosed;
|
|
219
292
|
let rejectClosed;
|
|
220
293
|
const closed = new Promise((res, rej) => { resolveClosed = res; rejectClosed = rej; });
|
|
221
294
|
const toClient = (line) => { if (!ended)
|
|
222
295
|
o.output.write(`${line}\n`); };
|
|
296
|
+
const ready = () => backend !== null && phase === 'ready' && !switching;
|
|
297
|
+
const owed = () => inflight.size > 0 || waiting.some(l => requestIdOf(l) !== null) || early.some(l => requestIdOf(l) !== null);
|
|
298
|
+
const daemonsWanted = () => isEnabled() && Date.now() >= distrustUntil;
|
|
223
299
|
/** Leave — but only once every call the client made has been answered and written out. */
|
|
224
300
|
const maybeFinish = () => {
|
|
225
|
-
if (ended || !inputEnded ||
|
|
301
|
+
if (ended || !inputEnded || owed())
|
|
226
302
|
return;
|
|
227
303
|
void shutdown();
|
|
228
304
|
};
|
|
229
305
|
const shutdown = async () => {
|
|
230
306
|
if (ended)
|
|
231
307
|
return;
|
|
232
|
-
|
|
233
|
-
|
|
308
|
+
ended = true;
|
|
309
|
+
for (const t of [drainTimer, probeTimer, stoppingTimer])
|
|
310
|
+
if (t)
|
|
311
|
+
clearTimeout(t);
|
|
234
312
|
try {
|
|
235
313
|
await backend?.close();
|
|
236
314
|
}
|
|
237
315
|
catch { /* leaving anyway */ }
|
|
316
|
+
try {
|
|
317
|
+
await pending?.close();
|
|
318
|
+
}
|
|
319
|
+
catch { /* leaving anyway */ }
|
|
238
320
|
// stdout on a Windows pipe is asynchronous: the caller exits the process
|
|
239
321
|
// next, and the last answer must be out of our hands before it does.
|
|
240
322
|
await new Promise(resolve => { try {
|
|
@@ -243,9 +325,54 @@ function runProxy(o) {
|
|
|
243
325
|
catch {
|
|
244
326
|
resolve();
|
|
245
327
|
} });
|
|
246
|
-
ended = true;
|
|
247
328
|
resolveClosed();
|
|
248
329
|
};
|
|
330
|
+
const settleIdle = () => {
|
|
331
|
+
if (inflight.size > 0 || phase === 'handshaking')
|
|
332
|
+
return;
|
|
333
|
+
const w = idleWaiters;
|
|
334
|
+
idleWaiters = [];
|
|
335
|
+
for (const f of w)
|
|
336
|
+
f();
|
|
337
|
+
};
|
|
338
|
+
const whenIdle = () => new Promise(resolve => {
|
|
339
|
+
if (inflight.size === 0 && phase !== 'handshaking')
|
|
340
|
+
resolve();
|
|
341
|
+
else
|
|
342
|
+
idleWaiters.push(resolve);
|
|
343
|
+
});
|
|
344
|
+
const forward = (line) => {
|
|
345
|
+
const id = requestIdOf(line);
|
|
346
|
+
if (id !== null)
|
|
347
|
+
inflight.set(id, line);
|
|
348
|
+
backend.send(line);
|
|
349
|
+
};
|
|
350
|
+
const drain = () => {
|
|
351
|
+
while (ready() && waiting.length)
|
|
352
|
+
forward(waiting.shift());
|
|
353
|
+
maybeFinish();
|
|
354
|
+
};
|
|
355
|
+
/** Bring the backend up to where the client is: the client's initialize (ours to answer, its answer swallowed), then initialized. */
|
|
356
|
+
const handshake = () => {
|
|
357
|
+
if (!backend || backendInitDone || phase === 'handshaking')
|
|
358
|
+
return;
|
|
359
|
+
if (!clientInit || !initAnswered) {
|
|
360
|
+
// Nothing to replay yet; or the client's own initialize is still queued and is about to reach this backend itself.
|
|
361
|
+
phase = 'ready';
|
|
362
|
+
drain();
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
phase = 'handshaking';
|
|
366
|
+
replayId = `crbro-replay-${++replays}`;
|
|
367
|
+
backend.send(JSON.stringify({ ...clientInit, id: replayId }));
|
|
368
|
+
};
|
|
369
|
+
/** The backend is where the client is: let the queue through. */
|
|
370
|
+
const backendReady = () => {
|
|
371
|
+
phase = 'ready';
|
|
372
|
+
trace('backend ready', backend?.kind ?? '', true);
|
|
373
|
+
settleIdle();
|
|
374
|
+
drain();
|
|
375
|
+
};
|
|
249
376
|
const fromBackend = (line) => {
|
|
250
377
|
let m = null;
|
|
251
378
|
try {
|
|
@@ -253,135 +380,244 @@ function runProxy(o) {
|
|
|
253
380
|
}
|
|
254
381
|
catch { /* not ours to judge: pass it on */ }
|
|
255
382
|
if (m && replayId !== null && m.id === replayId && m.method === undefined) {
|
|
256
|
-
// The
|
|
257
|
-
// has
|
|
383
|
+
// The backend answered the handshake we made for the client. The client
|
|
384
|
+
// already has its answer; this one stops here.
|
|
258
385
|
replayId = null;
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
386
|
+
backendInitDone = true;
|
|
387
|
+
if (m.error)
|
|
388
|
+
log(`the backend refused the replayed initialize: ${m.error?.message ?? 'no reason'}`);
|
|
389
|
+
if (clientInitialized && backend)
|
|
390
|
+
backend.send(clientInitialized);
|
|
391
|
+
if (lastBoot && backend) {
|
|
392
|
+
// The project crbro_boot bound for this conversation lived on the
|
|
393
|
+
// backend that is gone: boot again with the same arguments, answer
|
|
394
|
+
// swallowed, before anything queued reaches the new one.
|
|
395
|
+
rebootId = `crbro-reboot-${replays}`;
|
|
396
|
+
backend.send(JSON.stringify({ jsonrpc: '2.0', id: rebootId, method: 'tools/call', params: lastBoot }));
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
backendReady();
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
if (m && rebootId !== null && m.id === rebootId && m.method === undefined) {
|
|
403
|
+
rebootId = null;
|
|
404
|
+
if (m.error || m.result?.isError)
|
|
405
|
+
log('the replayed crbro_boot failed: this conversation may have lost its project binding');
|
|
406
|
+
backendReady();
|
|
263
407
|
return;
|
|
264
408
|
}
|
|
265
|
-
if (m && m.
|
|
409
|
+
if (m && m.method === undefined && m.id !== undefined && m.id !== null) {
|
|
410
|
+
if (m.error?.data?.crbro === lines_js_1.NOT_STARTED && inflight.has(m.id)) {
|
|
411
|
+
// The daemon is stopping and never ran this call: it goes to whoever serves next, first in line.
|
|
412
|
+
waiting.splice(requeueAt++, 0, inflight.get(m.id));
|
|
413
|
+
inflight.delete(m.id);
|
|
414
|
+
stopping();
|
|
415
|
+
settleIdle();
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
266
418
|
inflight.delete(m.id);
|
|
419
|
+
if (bootCalls.has(m.id)) {
|
|
420
|
+
const p = bootCalls.get(m.id);
|
|
421
|
+
bootCalls.delete(m.id);
|
|
422
|
+
if (!m.error && !m.result?.isError)
|
|
423
|
+
lastBoot = p;
|
|
424
|
+
}
|
|
425
|
+
if (!initAnswered && clientInit && m.id === clientInit.id) {
|
|
426
|
+
initAnswered = true;
|
|
427
|
+
backendInitDone = true;
|
|
428
|
+
trace('initialize answered', `by the ${backend?.kind ?? 'backend'}`, true);
|
|
429
|
+
}
|
|
430
|
+
if (toolCalls.delete(m.id))
|
|
431
|
+
trace('first tool result', backend?.kind ?? '', true);
|
|
432
|
+
settleIdle();
|
|
433
|
+
}
|
|
267
434
|
toClient(line);
|
|
268
435
|
maybeFinish();
|
|
269
436
|
};
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
437
|
+
/** The daemon said it is stopping: hold new lines until it is gone, and do not wait for it for ever. */
|
|
438
|
+
const stopping = () => {
|
|
439
|
+
if (phase === 'stopping')
|
|
440
|
+
return;
|
|
441
|
+
phase = 'stopping';
|
|
442
|
+
const g = activeGen;
|
|
443
|
+
stoppingTimer = setTimeout(() => {
|
|
444
|
+
stoppingTimer = null;
|
|
445
|
+
if (activeGen !== g || ended)
|
|
446
|
+
return;
|
|
447
|
+
log('the daemon said it was stopping and did not go: leaving it');
|
|
448
|
+
const b = backend;
|
|
449
|
+
void lost();
|
|
450
|
+
void b?.close().catch(() => undefined);
|
|
451
|
+
}, STOPPING_GRACE_MS);
|
|
452
|
+
stoppingTimer.unref();
|
|
274
453
|
};
|
|
275
|
-
const
|
|
454
|
+
const handleClient = (line) => {
|
|
276
455
|
let m = null;
|
|
277
456
|
try {
|
|
278
457
|
m = JSON.parse(line);
|
|
279
458
|
}
|
|
280
459
|
catch { /* the backend will answer the parse error */ }
|
|
281
|
-
if (m
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
460
|
+
if (m && surface) {
|
|
461
|
+
const { answerFromSurface } = surfaceApi;
|
|
462
|
+
const a = answerFromSurface(surface, m);
|
|
463
|
+
if (a) {
|
|
464
|
+
toClient(JSON.stringify(a));
|
|
465
|
+
if (m.method === 'initialize') {
|
|
466
|
+
clientInit = m;
|
|
467
|
+
initAnswered = true;
|
|
468
|
+
trace('initialize answered', 'by the proxy', true);
|
|
469
|
+
if (backend && phase === 'ready' && !backendInitDone)
|
|
470
|
+
handshake();
|
|
471
|
+
}
|
|
472
|
+
maybeFinish();
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
if (isRequest(m) && m.method === 'initialize') {
|
|
477
|
+
clientInit = m;
|
|
478
|
+
initAnswered = false;
|
|
479
|
+
}
|
|
480
|
+
if (m && m.method === 'notifications/initialized' && m.id === undefined) {
|
|
481
|
+
clientInitialized = line;
|
|
482
|
+
// Ours to send, right after the replayed initialize: never ahead of it.
|
|
483
|
+
if (initAnswered && !backendInitDone)
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
if (isRequest(m) && m.method === 'tools/call') {
|
|
487
|
+
toolCalls.add(m.id);
|
|
488
|
+
if (m.params?.name === 'crbro_boot')
|
|
489
|
+
bootCalls.set(m.id, m.params);
|
|
490
|
+
}
|
|
491
|
+
if (ready())
|
|
492
|
+
forward(line);
|
|
289
493
|
else
|
|
290
494
|
waiting.push(line);
|
|
291
495
|
};
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
496
|
+
let surfaceApi = null;
|
|
497
|
+
const fromClient = (line) => {
|
|
498
|
+
if (surface === undefined || (surface && !surfaceApi)) {
|
|
499
|
+
early.push(line);
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
handleClient(line);
|
|
295
503
|
};
|
|
504
|
+
const eventsFor = (g) => ({
|
|
505
|
+
onLine: line => { if (g === activeGen)
|
|
506
|
+
fromBackend(line); },
|
|
507
|
+
onClose: () => {
|
|
508
|
+
if (g === activeGen)
|
|
509
|
+
void lost();
|
|
510
|
+
else if (g === pendingGen) {
|
|
511
|
+
pendingGen = -1;
|
|
512
|
+
pending = null;
|
|
513
|
+
}
|
|
514
|
+
},
|
|
515
|
+
});
|
|
296
516
|
/** No backend of any kind could be had. Say so on every call that is owed an answer, and stop — as the classic server does when it cannot start. */
|
|
297
517
|
const fatal = (err) => {
|
|
298
518
|
if (ended)
|
|
299
519
|
return;
|
|
300
520
|
log(`no backend could be started: ${err.message}`);
|
|
301
|
-
const ids = new Set(inflight);
|
|
302
|
-
for (const l of waiting) {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
ids.add(q.id);
|
|
307
|
-
}
|
|
308
|
-
catch { /* not a request */ }
|
|
521
|
+
const ids = new Set(inflight.keys());
|
|
522
|
+
for (const l of [...waiting, ...early]) {
|
|
523
|
+
const id = requestIdOf(l);
|
|
524
|
+
if (id !== null)
|
|
525
|
+
ids.add(id);
|
|
309
526
|
}
|
|
310
527
|
for (const id of ids)
|
|
311
528
|
toClient(JSON.stringify({ jsonrpc: '2.0', id, error: { code: -32000, message: `CRBRO could not start: ${err.message}` } }));
|
|
312
529
|
inflight.clear();
|
|
313
530
|
waiting.length = 0;
|
|
531
|
+
early.length = 0;
|
|
314
532
|
ended = true;
|
|
533
|
+
for (const t of [drainTimer, probeTimer, stoppingTimer])
|
|
534
|
+
if (t)
|
|
535
|
+
clearTimeout(t);
|
|
315
536
|
rejectClosed(err);
|
|
316
537
|
};
|
|
538
|
+
const activate = (b, g) => {
|
|
539
|
+
backend = b;
|
|
540
|
+
activeGen = g;
|
|
541
|
+
phase = 'none';
|
|
542
|
+
requeueAt = 0;
|
|
543
|
+
backendInitDone = false;
|
|
544
|
+
replayId = null;
|
|
545
|
+
rebootId = null;
|
|
546
|
+
trace(b.kind === 'daemon' ? 'backend connected' : 'in-process fallback', b.kind === 'local' ? 'engine ready' : '', true);
|
|
547
|
+
if (b.kind === 'local')
|
|
548
|
+
scheduleProbe();
|
|
549
|
+
else
|
|
550
|
+
probeDelay = probeMin;
|
|
551
|
+
handshake();
|
|
552
|
+
};
|
|
317
553
|
async function acquire(first) {
|
|
318
|
-
|
|
554
|
+
acquiring = true;
|
|
319
555
|
backend = null;
|
|
556
|
+
activeGen = -1;
|
|
557
|
+
phase = 'none';
|
|
320
558
|
let b = null;
|
|
559
|
+
const g = ++gen;
|
|
321
560
|
// Asked every time, not once at launch: `crbro daemon off` removes the
|
|
322
561
|
// flag and stops the daemon, and a proxy that only knew how to respawn one
|
|
323
562
|
// undid it within 200 ms.
|
|
324
|
-
if (
|
|
325
|
-
const d = await daemonBackend(
|
|
563
|
+
if (daemonsWanted()) {
|
|
564
|
+
const d = await daemonBackend({
|
|
565
|
+
brainRoot: o.brainRoot, build, resumed: !first, deadline: Date.now() + budgetMs,
|
|
566
|
+
spawn: o.spawnDaemon, log, trace,
|
|
567
|
+
}, eventsFor(g));
|
|
326
568
|
if (d === 'unusable')
|
|
327
|
-
|
|
328
|
-
else
|
|
569
|
+
distrust(FLAP_COOLDOWN_MS);
|
|
570
|
+
else if (d)
|
|
329
571
|
b = d;
|
|
330
|
-
|
|
331
|
-
if (!b) {
|
|
332
|
-
if (trustDaemons && isEnabled())
|
|
572
|
+
else
|
|
333
573
|
log('no daemon reachable: serving from this process');
|
|
334
|
-
b = await makeLocal(events, !first);
|
|
335
574
|
}
|
|
575
|
+
if (!b)
|
|
576
|
+
b = await makeLocal(eventsFor(g), !first);
|
|
577
|
+
acquiring = false;
|
|
336
578
|
if (ended) {
|
|
337
579
|
await b.close();
|
|
338
580
|
return;
|
|
339
581
|
}
|
|
340
|
-
|
|
341
|
-
if (!first && initialize) {
|
|
342
|
-
// Same request the client once sent, under an id of our own.
|
|
343
|
-
replayId = `crbro-replay-${++replays}`;
|
|
344
|
-
b.send(JSON.stringify({ ...initialize, id: replayId }));
|
|
345
|
-
return; // `connecting` stays true until the replayed handshake is answered
|
|
346
|
-
}
|
|
347
|
-
connecting = false;
|
|
348
|
-
drain();
|
|
582
|
+
activate(b, g);
|
|
349
583
|
}
|
|
584
|
+
const distrust = (ms) => { distrustUntil = Math.max(distrustUntil, Date.now() + ms); };
|
|
350
585
|
async function lost() {
|
|
351
586
|
if (ended)
|
|
352
587
|
return;
|
|
588
|
+
if (stoppingTimer) {
|
|
589
|
+
clearTimeout(stoppingTimer);
|
|
590
|
+
stoppingTimer = null;
|
|
591
|
+
}
|
|
353
592
|
reconnects++;
|
|
354
593
|
const now = Date.now();
|
|
355
594
|
losses.push(now);
|
|
356
595
|
while (losses.length && now - losses[0] > FLAP_WINDOW_MS)
|
|
357
596
|
losses.shift();
|
|
358
597
|
if (losses.length >= FLAP_LIMIT) {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
// and answering it here too would hand the client two replies to one id.
|
|
365
|
-
const queued = new Set();
|
|
366
|
-
for (const line of waiting) {
|
|
367
|
-
try {
|
|
368
|
-
const q = JSON.parse(line);
|
|
369
|
-
if (q?.id !== undefined && q.id !== null)
|
|
370
|
-
queued.add(q.id);
|
|
371
|
-
}
|
|
372
|
-
catch { /* not a request */ }
|
|
598
|
+
// A daemon that keeps dying is not worth the client's time for a while — a while, not for ever.
|
|
599
|
+
distrust(cooldownMs);
|
|
600
|
+
log(`daemon lost ${losses.length} times in a minute: serving in-process for ${Math.round(cooldownMs / 1000)} s, then looking again`);
|
|
601
|
+
cooldownMs = Math.min(cooldownMs * 2, MAX_COOLDOWN_MS);
|
|
602
|
+
losses.length = 0;
|
|
373
603
|
}
|
|
374
|
-
for
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
604
|
+
// Whatever was waiting for an answer will not get one from a dead process,
|
|
605
|
+
// and nobody can say whether it ran: say exactly that. What never left this
|
|
606
|
+
// proxy (still queued) is not in that position and goes to the next backend.
|
|
607
|
+
for (const id of [...inflight.keys()]) {
|
|
608
|
+
toolCalls.delete(id);
|
|
609
|
+
toClient(JSON.stringify({ jsonrpc: '2.0', id, error: { code: -32000, message: LOST_MESSAGE } }));
|
|
379
610
|
}
|
|
611
|
+
inflight.clear();
|
|
380
612
|
replayId = null;
|
|
613
|
+
rebootId = null;
|
|
381
614
|
backend = null;
|
|
615
|
+
activeGen = -1;
|
|
616
|
+
phase = 'none';
|
|
617
|
+
backendInitDone = false;
|
|
618
|
+
settleIdle();
|
|
382
619
|
// The client has gone and is owed nothing more: a replacement would serve nobody.
|
|
383
|
-
if (inputEnded &&
|
|
384
|
-
connecting = false;
|
|
620
|
+
if (inputEnded && !owed()) {
|
|
385
621
|
void shutdown();
|
|
386
622
|
return;
|
|
387
623
|
}
|
|
@@ -392,7 +628,98 @@ function runProxy(o) {
|
|
|
392
628
|
fatal(err);
|
|
393
629
|
}
|
|
394
630
|
}
|
|
395
|
-
|
|
631
|
+
// ── Serving in-process: keep looking for a daemon, and move back to one ──
|
|
632
|
+
function scheduleProbe() {
|
|
633
|
+
if (probeTimer || ended)
|
|
634
|
+
return;
|
|
635
|
+
probeTimer = setTimeout(() => { probeTimer = null; void probe(); }, probeDelay);
|
|
636
|
+
probeTimer.unref();
|
|
637
|
+
probeDelay = Math.min(probeDelay * 2, probeMax);
|
|
638
|
+
}
|
|
639
|
+
async function probe() {
|
|
640
|
+
if (ended || !backend || backend.kind !== 'local' || switching)
|
|
641
|
+
return;
|
|
642
|
+
if (!daemonsWanted()) {
|
|
643
|
+
scheduleProbe();
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
const g = ++gen;
|
|
647
|
+
pendingGen = g;
|
|
648
|
+
const d = await daemonBackend({
|
|
649
|
+
brainRoot: o.brainRoot, build, resumed: true, deadline: Date.now() + PROBE_BUDGET_MS,
|
|
650
|
+
spawn: null, log, trace, quiet: true,
|
|
651
|
+
}, eventsFor(g)).catch(() => null);
|
|
652
|
+
if (!d || d === 'unusable' || pendingGen !== g || ended || !backend || backend.kind !== 'local') {
|
|
653
|
+
if (pendingGen === g)
|
|
654
|
+
pendingGen = -1;
|
|
655
|
+
if (d && d !== 'unusable')
|
|
656
|
+
await d.close();
|
|
657
|
+
if (d === 'unusable')
|
|
658
|
+
distrust(FLAP_COOLDOWN_MS);
|
|
659
|
+
scheduleProbe();
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
pending = d;
|
|
663
|
+
await switchTo(d, g);
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Move the conversation from this process's server to the daemon. Only when
|
|
667
|
+
* nothing is running here (new lines wait meanwhile), and the in-process
|
|
668
|
+
* index is flushed BEFORE the daemon serves: the daemon catches up with
|
|
669
|
+
* what this process wrote (it refreshes from disk for a resumed
|
|
670
|
+
* connection), and its own later save is a superset, never overwritten by
|
|
671
|
+
* ours. The daemon's tally of this conversation starts empty, as after a
|
|
672
|
+
* lost daemon: consolidate says so (`resumed`).
|
|
673
|
+
*/
|
|
674
|
+
async function switchTo(d, g) {
|
|
675
|
+
switching = true;
|
|
676
|
+
await whenIdle();
|
|
677
|
+
const old = backend;
|
|
678
|
+
const stillThere = () => pendingGen === g && !ended;
|
|
679
|
+
if (!stillThere() || !old) {
|
|
680
|
+
switching = false;
|
|
681
|
+
if (pendingGen === g) {
|
|
682
|
+
pendingGen = -1;
|
|
683
|
+
pending = null;
|
|
684
|
+
}
|
|
685
|
+
await d.close().catch(() => undefined);
|
|
686
|
+
if (!ended) {
|
|
687
|
+
drain();
|
|
688
|
+
scheduleProbe();
|
|
689
|
+
}
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
activeGen = -1; // nothing is in flight: nothing more is owed by the old server
|
|
693
|
+
await old.close().catch(() => undefined);
|
|
694
|
+
const linkAlive = pendingGen === g;
|
|
695
|
+
pendingGen = -1;
|
|
696
|
+
pending = null;
|
|
697
|
+
switching = false;
|
|
698
|
+
backend = null;
|
|
699
|
+
if (ended) {
|
|
700
|
+
await d.close().catch(() => undefined);
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
if (!linkAlive) {
|
|
704
|
+
// The daemon went away while we flushed: back to the start, as after any lost backend.
|
|
705
|
+
try {
|
|
706
|
+
await acquire(false);
|
|
707
|
+
}
|
|
708
|
+
catch (err) {
|
|
709
|
+
fatal(err);
|
|
710
|
+
}
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
switches++;
|
|
714
|
+
log('a daemon answers again: this conversation moved back to it from the in-process server');
|
|
715
|
+
trace('moved back to the daemon');
|
|
716
|
+
activate(d, g);
|
|
717
|
+
}
|
|
718
|
+
const reader = new lines_js_1.LineReader(fromClient, head => {
|
|
719
|
+
const id = (0, lines_js_1.idFromHead)(head);
|
|
720
|
+
log(`refused a message larger than ${lines_js_1.MAX_LINE_BYTES / 1048576} MB${id !== null ? ` (id ${JSON.stringify(id)})` : ''}`);
|
|
721
|
+
toClient(JSON.stringify({ jsonrpc: '2.0', id, error: { code: -32600, message: `Invalid Request: a message larger than ${lines_js_1.MAX_LINE_BYTES / 1048576} MB was not read.` } }));
|
|
722
|
+
});
|
|
396
723
|
o.input.on('data', (chunk) => reader.push(typeof chunk === 'string' ? Buffer.from(chunk, 'utf8') : chunk));
|
|
397
724
|
const onInputEnd = () => {
|
|
398
725
|
if (inputEnded)
|
|
@@ -406,7 +733,35 @@ function runProxy(o) {
|
|
|
406
733
|
};
|
|
407
734
|
o.input.on('end', onInputEnd);
|
|
408
735
|
o.input.on('close', onInputEnd);
|
|
409
|
-
|
|
410
|
-
|
|
736
|
+
// ── The surface: given, or found now; lines that came meanwhile are handled in order ──
|
|
737
|
+
const surfaceReady = (async () => {
|
|
738
|
+
try {
|
|
739
|
+
surfaceApi = await import('./surface.js');
|
|
740
|
+
if (surface === undefined)
|
|
741
|
+
surface = await surfaceApi.getSurface(build);
|
|
742
|
+
}
|
|
743
|
+
catch {
|
|
744
|
+
if (surface === undefined)
|
|
745
|
+
surface = null;
|
|
746
|
+
}
|
|
747
|
+
if (!surfaceApi)
|
|
748
|
+
surface = null;
|
|
749
|
+
for (const line of early.splice(0))
|
|
750
|
+
handleClient(line);
|
|
751
|
+
maybeFinish();
|
|
752
|
+
})();
|
|
753
|
+
void acquire(true).catch(async (err) => {
|
|
754
|
+
// initialize is the proxy's to answer even when nothing can serve: a
|
|
755
|
+
// backend that fails faster than the surface is computed in-process (a
|
|
756
|
+
// build without its snapshot) must not turn that answer into an error.
|
|
757
|
+
await Promise.race([surfaceReady, new Promise(r => { const t = setTimeout(r, 5_000); t.unref(); })]);
|
|
758
|
+
fatal(err);
|
|
759
|
+
});
|
|
760
|
+
return {
|
|
761
|
+
closed,
|
|
762
|
+
backendKind: () => (backend ? backend.kind : 'none'),
|
|
763
|
+
reconnects: () => reconnects,
|
|
764
|
+
switches: () => switches,
|
|
765
|
+
};
|
|
411
766
|
}
|
|
412
767
|
//# sourceMappingURL=proxy.js.map
|