@cortexkit/aft-bridge 0.49.4 → 0.50.1
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/bridge.d.ts +7 -1
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.js +66 -4
- package/dist/bridge.js.map +1 -1
- package/dist/downloader.d.ts +6 -1
- package/dist/downloader.d.ts.map +1 -1
- package/dist/downloader.js +149 -29
- package/dist/downloader.js.map +1 -1
- package/dist/error-contract.d.ts +15 -2
- package/dist/error-contract.d.ts.map +1 -1
- package/dist/error-contract.js +54 -5
- package/dist/error-contract.js.map +1 -1
- package/dist/path-aliases.js +21 -0
- package/dist/path-aliases.js.map +1 -1
- package/dist/pool.d.ts +7 -7
- package/dist/pool.d.ts.map +1 -1
- package/dist/pool.js +36 -9
- package/dist/pool.js.map +1 -1
- package/dist/revivable-transport.d.ts +1 -0
- package/dist/revivable-transport.d.ts.map +1 -1
- package/dist/revivable-transport.js +21 -2
- package/dist/revivable-transport.js.map +1 -1
- package/dist/subc-transport.d.ts +18 -3
- package/dist/subc-transport.d.ts.map +1 -1
- package/dist/subc-transport.js +237 -70
- package/dist/subc-transport.js.map +1 -1
- package/package.json +1 -1
package/dist/subc-transport.js
CHANGED
|
@@ -40,6 +40,8 @@ const MAX_CONSECUTIVE_TRANSPORT_FAILURES = 3;
|
|
|
40
40
|
* (otherwise a permanently-failing route resubscribes in a 100ms hot loop). (B-#2.)
|
|
41
41
|
*/
|
|
42
42
|
const BG_STABLE_MS = 5_000;
|
|
43
|
+
const BG_LIFECYCLE_LOG_INTERVAL_MS = 60_000;
|
|
44
|
+
const BG_DISPATCH_PROBE_INTERVAL_MS = 60_000;
|
|
43
45
|
/**
|
|
44
46
|
* Session fallback when a tool runtime carries no session id, mirroring the Rust
|
|
45
47
|
* `DEFAULT_SESSION_ID` (`protocol.rs`). Keeps undo/checkpoint/bash namespacing
|
|
@@ -104,19 +106,12 @@ function identityKey(identity) {
|
|
|
104
106
|
return `${identity.project_root}\u0000${identity.harness}\u0000${identity.session}`;
|
|
105
107
|
}
|
|
106
108
|
/**
|
|
107
|
-
* One session's held-open bg_events subscription with its
|
|
109
|
+
* One session's held-open bg_events subscription with its own reconnect driver.
|
|
108
110
|
*
|
|
109
|
-
* The
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
* call. So an idle agent (no tool traffic) whose connection drops is still woken
|
|
114
|
-
* for a completion that landed while disconnected (the durable Rust registry holds
|
|
115
|
-
* it until acked; resubscribe + the immediate forced-drain replay it).
|
|
116
|
-
*
|
|
117
|
-
* The loop is a single sequential async task (only one attempt in flight at a
|
|
118
|
-
* time). `stopped`, the captured root-generation check, and one instance per
|
|
119
|
-
* identity prevent duplicate or stale subscriptions.
|
|
111
|
+
* The reconnect loop is independent of tool calls, including provider StreamEnd:
|
|
112
|
+
* the module emits StreamEnd when a route is replaced, so only `stop()` proves
|
|
113
|
+
* that a clean end was requested by this consumer. Reopening the dedicated route
|
|
114
|
+
* also forces an outbox drain, recovering completions queued while disconnected.
|
|
120
115
|
*/
|
|
121
116
|
class BgSubscription {
|
|
122
117
|
identity;
|
|
@@ -128,13 +123,16 @@ class BgSubscription {
|
|
|
128
123
|
canAttach;
|
|
129
124
|
onRootAttachFailure;
|
|
130
125
|
onDormant;
|
|
126
|
+
dispatchProbeIntervalMs;
|
|
131
127
|
nudgeRef;
|
|
132
128
|
isCurrent;
|
|
133
129
|
stopped = false;
|
|
134
130
|
/** The live subscription handle, read by stop() to wake the loop's `await closed`. */
|
|
135
131
|
current = null;
|
|
136
132
|
loop;
|
|
137
|
-
|
|
133
|
+
lifecycleLogState = new Map();
|
|
134
|
+
nudgeReceiptLogState = null;
|
|
135
|
+
constructor(identity, acquireClient, dropClient, consumerIdentity, onNudge, sleep, canAttach, onRootAttachFailure, onDormant, dispatchProbeIntervalMs, nudgeRef, isCurrent = () => true) {
|
|
138
136
|
this.identity = identity;
|
|
139
137
|
this.acquireClient = acquireClient;
|
|
140
138
|
this.dropClient = dropClient;
|
|
@@ -144,17 +142,15 @@ class BgSubscription {
|
|
|
144
142
|
this.canAttach = canAttach;
|
|
145
143
|
this.onRootAttachFailure = onRootAttachFailure;
|
|
146
144
|
this.onDormant = onDormant;
|
|
145
|
+
this.dispatchProbeIntervalMs = dispatchProbeIntervalMs;
|
|
147
146
|
this.nudgeRef = nudgeRef;
|
|
148
147
|
this.isCurrent = isCurrent;
|
|
149
148
|
this.loop = this.run();
|
|
150
149
|
}
|
|
151
150
|
async stop() {
|
|
152
151
|
this.stopped = true;
|
|
153
|
-
// Wake a live `await sub.closed
|
|
154
|
-
//
|
|
155
|
-
// is closed exactly once, never double-closed by stop() + the loop). If the
|
|
156
|
-
// loop is between routeOpen and subscribe, its post-subscribe `stopped`
|
|
157
|
-
// re-check (or the pre-subscribe check) closes/returns instead.
|
|
152
|
+
// Wake a live `await sub.closed`; the loop's `finally` remains the sole
|
|
153
|
+
// owner of closeRouteChannel, avoiding a double close from stop() + run().
|
|
158
154
|
const sub = this.current;
|
|
159
155
|
if (sub) {
|
|
160
156
|
try {
|
|
@@ -166,98 +162,200 @@ class BgSubscription {
|
|
|
166
162
|
}
|
|
167
163
|
await this.loop.catch(() => undefined);
|
|
168
164
|
}
|
|
165
|
+
info(kind, message) {
|
|
166
|
+
const now = Date.now();
|
|
167
|
+
const state = this.lifecycleLogState.get(kind);
|
|
168
|
+
if (state && now - state.lastEmittedAt < BG_LIFECYCLE_LOG_INTERVAL_MS) {
|
|
169
|
+
state.suppressed += 1;
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const suppressed = state?.suppressed ?? 0;
|
|
173
|
+
this.lifecycleLogState.set(kind, { lastEmittedAt: now, suppressed: 0 });
|
|
174
|
+
const suffix = suppressed > 0 ? ` suppressed=${suppressed}` : "";
|
|
175
|
+
log(`subc bg_events: ${message}${suffix}`, { sessionId: this.identity.session });
|
|
176
|
+
}
|
|
177
|
+
routeId(route) {
|
|
178
|
+
return `${route.channel}@${route.epoch}`;
|
|
179
|
+
}
|
|
180
|
+
recordNudgeReceipt(routeId) {
|
|
181
|
+
const now = Date.now();
|
|
182
|
+
const state = this.nudgeReceiptLogState;
|
|
183
|
+
if (state && now - state.lastEmittedAt < BG_LIFECYCLE_LOG_INTERVAL_MS) {
|
|
184
|
+
state.count += 1;
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const count = (state?.count ?? 0) + 1;
|
|
188
|
+
this.nudgeReceiptLogState = { lastEmittedAt: now, count: 0 };
|
|
189
|
+
log(`subc bg_events: nudge received channel=${routeId} count=${count}`, {
|
|
190
|
+
sessionId: this.identity.session,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
errorText(error) {
|
|
194
|
+
return error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
|
195
|
+
}
|
|
196
|
+
startDispatchProbe(client, routeId) {
|
|
197
|
+
const initial = client.droppedIngressFrames;
|
|
198
|
+
if (typeof initial !== "number")
|
|
199
|
+
return () => undefined;
|
|
200
|
+
let previous = initial;
|
|
201
|
+
const timer = setInterval(() => {
|
|
202
|
+
const total = client.droppedIngressFrames;
|
|
203
|
+
if (typeof total !== "number" || total <= previous)
|
|
204
|
+
return;
|
|
205
|
+
const delta = total - previous;
|
|
206
|
+
previous = total;
|
|
207
|
+
this.info("dispatch-epoch-drop", `client ingress epoch drops scope=client observed_while_channel=${routeId} delta=${delta} total=${total}`);
|
|
208
|
+
}, this.dispatchProbeIntervalMs);
|
|
209
|
+
timer.unref?.();
|
|
210
|
+
return () => clearInterval(timer);
|
|
211
|
+
}
|
|
169
212
|
async run() {
|
|
170
|
-
let
|
|
213
|
+
let backoffAttempt = 0;
|
|
214
|
+
let reconnectAttempt = 0;
|
|
215
|
+
let reconnecting = false;
|
|
216
|
+
const beginReconnect = () => {
|
|
217
|
+
reconnecting = true;
|
|
218
|
+
reconnectAttempt = reconnectAttempt === 0 ? 1 : reconnectAttempt + 1;
|
|
219
|
+
};
|
|
220
|
+
const giveUp = (reason) => {
|
|
221
|
+
this.info("reconnect-gave-up", `reconnect gave-up attempt=${reconnectAttempt} reason=${reason}`);
|
|
222
|
+
};
|
|
171
223
|
while (!this.stopped) {
|
|
172
|
-
if (!this.isCurrent())
|
|
224
|
+
if (!this.isCurrent()) {
|
|
225
|
+
if (reconnecting)
|
|
226
|
+
giveUp("stale-session");
|
|
173
227
|
return;
|
|
228
|
+
}
|
|
174
229
|
if (!this.canAttach()) {
|
|
230
|
+
if (reconnecting)
|
|
231
|
+
giveUp("root-dormant");
|
|
175
232
|
this.onDormant();
|
|
176
233
|
return;
|
|
177
234
|
}
|
|
235
|
+
if (reconnecting) {
|
|
236
|
+
this.info("reconnect-attempt", `reconnect attempt=${reconnectAttempt}`);
|
|
237
|
+
}
|
|
178
238
|
let client;
|
|
179
239
|
try {
|
|
180
240
|
client = await this.acquireClient();
|
|
181
241
|
}
|
|
182
|
-
catch {
|
|
183
|
-
|
|
242
|
+
catch (err) {
|
|
243
|
+
if (!reconnecting)
|
|
244
|
+
beginReconnect();
|
|
245
|
+
this.info("reconnect-error", `reconnect error attempt=${reconnectAttempt} error=${this.errorText(err)}`);
|
|
246
|
+
await this.backoff(backoffAttempt++);
|
|
247
|
+
if (reconnecting)
|
|
248
|
+
reconnectAttempt += 1;
|
|
184
249
|
continue;
|
|
185
250
|
}
|
|
186
|
-
if (this.stopped
|
|
251
|
+
if (this.stopped) {
|
|
252
|
+
if (reconnecting)
|
|
253
|
+
giveUp("stopped");
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (!this.isCurrent()) {
|
|
257
|
+
if (reconnecting)
|
|
258
|
+
giveUp("stale-session");
|
|
187
259
|
return;
|
|
260
|
+
}
|
|
188
261
|
if (!this.canAttach()) {
|
|
262
|
+
if (reconnecting)
|
|
263
|
+
giveUp("root-dormant");
|
|
189
264
|
this.onDormant();
|
|
190
265
|
return;
|
|
191
266
|
}
|
|
192
267
|
let route;
|
|
193
268
|
try {
|
|
194
|
-
// A
|
|
269
|
+
// A dedicated route keeps bg_events independent of the tool route's
|
|
195
270
|
// credit window while preserving the client's connection-bound handle.
|
|
196
271
|
route = await client.routeOpen({ kind: "tool_provider", module_id: AFT_MODULE_ID }, this.identity, { consumerIdentity: this.consumerIdentity });
|
|
197
272
|
}
|
|
198
273
|
catch (err) {
|
|
199
274
|
if (this.isCurrent() && this.onRootAttachFailure(err)) {
|
|
275
|
+
if (reconnecting)
|
|
276
|
+
giveUp("root-dormant");
|
|
200
277
|
this.onDormant();
|
|
201
278
|
return;
|
|
202
279
|
}
|
|
203
|
-
// routeOpen failed. If it signals a dead CONNECTION, drop the shared
|
|
204
|
-
// client so the next `acquireClient` reconnects fresh — the idle-stranding
|
|
205
|
-
// fix (B-#1): `acquireClient` returns the cached client, so without this an
|
|
206
|
-
// idle bg loop would resubscribe forever onto the same dead socket.
|
|
207
280
|
if (isConsumerReconnectTransient(err))
|
|
208
281
|
this.dropClient(client);
|
|
209
|
-
|
|
282
|
+
if (!reconnecting)
|
|
283
|
+
beginReconnect();
|
|
284
|
+
this.info("reconnect-error", `reconnect error attempt=${reconnectAttempt} error=${this.errorText(err)}`);
|
|
285
|
+
await this.backoff(backoffAttempt++);
|
|
286
|
+
if (reconnecting)
|
|
287
|
+
reconnectAttempt += 1;
|
|
210
288
|
continue;
|
|
211
289
|
}
|
|
212
290
|
if (this.stopped || !this.isCurrent()) {
|
|
213
291
|
safeCloseRoute(client, route);
|
|
292
|
+
if (reconnecting)
|
|
293
|
+
giveUp(this.stopped ? "stopped" : "stale-session");
|
|
214
294
|
return;
|
|
215
295
|
}
|
|
216
|
-
// Route lifetime: the `finally` guarantees closeRouteChannel on every exit
|
|
217
|
-
// path from here (StreamEnd return, drop+resubscribe, stopped) so the
|
|
218
|
-
// dedicated route never leaks (B-#2).
|
|
219
296
|
const subscribedAt = Date.now();
|
|
297
|
+
const routeId = this.routeId(route);
|
|
298
|
+
let stopDispatchProbe = () => undefined;
|
|
220
299
|
try {
|
|
221
300
|
const sub = client.subscribe(route, { op: "bg_events" }, () => {
|
|
222
|
-
if (
|
|
223
|
-
this.
|
|
301
|
+
if (this.stopped) {
|
|
302
|
+
this.info("nudge-drop-stopped", `nudge dropped cause=subscription-stopped channel=${routeId}`);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
this.recordNudgeReceipt(routeId);
|
|
306
|
+
if (!this.isCurrent()) {
|
|
307
|
+
this.info("nudge-stale-carrier", `nudge carried by stale subscription; checking current session channel=${routeId}`);
|
|
308
|
+
}
|
|
309
|
+
this.onNudge();
|
|
224
310
|
});
|
|
225
311
|
this.current = sub;
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
312
|
+
stopDispatchProbe = this.startDispatchProbe(client, routeId);
|
|
313
|
+
this.info("subscription-open", `subscription open channel=${routeId}`);
|
|
314
|
+
if (reconnecting) {
|
|
315
|
+
this.info("reconnect-success", `reconnect success attempt=${reconnectAttempt} channel=${routeId}`);
|
|
316
|
+
reconnecting = false;
|
|
317
|
+
reconnectAttempt = 0;
|
|
318
|
+
}
|
|
319
|
+
// stop() may have fired between routeOpen and subscribe. Unsubscribe so
|
|
320
|
+
// the held `closed` promise resolves and the route can unwind.
|
|
229
321
|
if (this.stopped)
|
|
230
322
|
sub.unsubscribe();
|
|
231
|
-
//
|
|
232
|
-
//
|
|
323
|
+
// A reconnect may have missed completions, so force a drain after every
|
|
324
|
+
// successful subscribe to replay anything queued while the stream was down.
|
|
233
325
|
if (!this.stopped && this.isCurrent())
|
|
234
326
|
this.onNudge();
|
|
235
327
|
await sub.closed;
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
328
|
+
this.info("stream-end", `stream ended channel=${routeId}`);
|
|
329
|
+
if (this.stopped) {
|
|
330
|
+
giveUp("stopped");
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
// Provider StreamEnd is also emitted for route replacement; without an
|
|
334
|
+
// explicit local stop it must reopen rather than strand an idle session.
|
|
335
|
+
beginReconnect();
|
|
239
336
|
}
|
|
240
337
|
catch (err) {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
if (this.stopped)
|
|
338
|
+
const routeId = this.routeId(route);
|
|
339
|
+
this.info("stream-error", `stream error channel=${routeId} error=${this.errorText(err)}`);
|
|
340
|
+
if (this.stopped) {
|
|
341
|
+
giveUp("stopped");
|
|
244
342
|
return;
|
|
245
|
-
|
|
246
|
-
// GOODBYE keeps the client and just re-opens a fresh route.
|
|
343
|
+
}
|
|
247
344
|
if (isConsumerReconnectTransient(err))
|
|
248
345
|
this.dropClient(client);
|
|
249
|
-
// Reset backoff ONLY if the subscription was stable before dropping;
|
|
250
|
-
// otherwise a permanently-failing route would resubscribe in a 100ms hot
|
|
251
|
-
// loop and never reach the cap (B-#2).
|
|
252
346
|
if (Date.now() - subscribedAt >= BG_STABLE_MS)
|
|
253
|
-
|
|
347
|
+
backoffAttempt = 0;
|
|
348
|
+
beginReconnect();
|
|
254
349
|
}
|
|
255
350
|
finally {
|
|
351
|
+
stopDispatchProbe();
|
|
256
352
|
this.current = null;
|
|
257
353
|
safeCloseRoute(client, route);
|
|
258
354
|
}
|
|
259
|
-
await this.backoff(
|
|
355
|
+
await this.backoff(backoffAttempt++);
|
|
260
356
|
}
|
|
357
|
+
if (reconnecting)
|
|
358
|
+
giveUp("stopped");
|
|
261
359
|
}
|
|
262
360
|
async backoff(attempt) {
|
|
263
361
|
const ms = Math.min(100 * 2 ** Math.min(attempt, 6), 2000);
|
|
@@ -399,6 +497,9 @@ class SubcTransport {
|
|
|
399
497
|
this.assertCurrent();
|
|
400
498
|
const { preview, timeoutMs, onProgress } = this.splitOptions(options);
|
|
401
499
|
const body = { name, arguments: rawArgs };
|
|
500
|
+
const editSlotSurvives = this.pool.getEditSlotSurvives();
|
|
501
|
+
if (editSlotSurvives !== undefined)
|
|
502
|
+
body.edit_slot_survives = editSlotSurvives;
|
|
402
503
|
if (preview === true)
|
|
403
504
|
body.preview = true;
|
|
404
505
|
const reply = await this.pool.routeRequest(this.identityFor(sessionId), body, timeoutMs, onProgress, this.generation);
|
|
@@ -423,7 +524,11 @@ class SubcTransport {
|
|
|
423
524
|
}
|
|
424
525
|
const { timeoutMs, onProgress } = this.splitOptions(options);
|
|
425
526
|
const session = typeof params.session_id === "string" ? params.session_id : undefined;
|
|
426
|
-
const
|
|
527
|
+
const body = { name: command, arguments: params };
|
|
528
|
+
const editSlotSurvives = this.pool.getEditSlotSurvives();
|
|
529
|
+
if (editSlotSurvives !== undefined)
|
|
530
|
+
body.edit_slot_survives = editSlotSurvives;
|
|
531
|
+
const reply = await this.pool.routeRequest(this.identityFor(session), body, timeoutMs, onProgress, this.generation);
|
|
427
532
|
const response = reliftReply(reply);
|
|
428
533
|
this.captureStatusBar(response);
|
|
429
534
|
return response;
|
|
@@ -455,6 +560,7 @@ export class SubcTransportPool {
|
|
|
455
560
|
onBgEventsNudge;
|
|
456
561
|
onBgEventsNudgeRef;
|
|
457
562
|
bgBackoffSleep;
|
|
563
|
+
bgDispatchProbeIntervalMs;
|
|
458
564
|
lifecycleDemandCheck;
|
|
459
565
|
onLifecycleEvent;
|
|
460
566
|
onBgNudgeRejected;
|
|
@@ -479,8 +585,11 @@ export class SubcTransportPool {
|
|
|
479
585
|
/** Concrete per-root facades, including their captured root generation. */
|
|
480
586
|
transports = new Map();
|
|
481
587
|
generationRejections = new Set();
|
|
588
|
+
nudgeDeliveryLogState = new Map();
|
|
482
589
|
pendingRootCleanups = new Set();
|
|
483
590
|
shuttingDown = false;
|
|
591
|
+
editSlotSurvives;
|
|
592
|
+
editSlotSurvivesCaptured = false;
|
|
484
593
|
constructor(options) {
|
|
485
594
|
this.connectionFile = options.connectionFile;
|
|
486
595
|
this.harness = options.harness;
|
|
@@ -491,6 +600,8 @@ export class SubcTransportPool {
|
|
|
491
600
|
this.onBgEventsNudgeRef = options.onBgEventsNudgeRef;
|
|
492
601
|
this.bgBackoffSleep =
|
|
493
602
|
options.bgBackoffSleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
603
|
+
this.bgDispatchProbeIntervalMs =
|
|
604
|
+
options.bgDispatchProbeIntervalMs ?? BG_DISPATCH_PROBE_INTERVAL_MS;
|
|
494
605
|
const lifecycle = options.lifecycle;
|
|
495
606
|
const demandCheck = options.lifecycleDemandCheck ?? options.demandCheck ?? lifecycle?.demandCheck;
|
|
496
607
|
this.lifecycleDemandCheck = demandCheck;
|
|
@@ -852,6 +963,35 @@ export class SubcTransportPool {
|
|
|
852
963
|
isCurrentSession(key, record) {
|
|
853
964
|
return this.sessions.get(key) === record && !record.closed;
|
|
854
965
|
}
|
|
966
|
+
currentSessionForNudge(identity) {
|
|
967
|
+
const current = this.sessions.get(identityKey(identity));
|
|
968
|
+
return current && !current.closed ? current : null;
|
|
969
|
+
}
|
|
970
|
+
nudgeRefFor(record) {
|
|
971
|
+
const poolId = this.currentPoolId();
|
|
972
|
+
const generation = record.generation;
|
|
973
|
+
if (poolId === undefined || generation === undefined)
|
|
974
|
+
return undefined;
|
|
975
|
+
return {
|
|
976
|
+
canonicalRoot: record.canonicalRoot,
|
|
977
|
+
session: record.identity.session,
|
|
978
|
+
concretePoolId: poolId,
|
|
979
|
+
generation,
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
logNudgeDelivery(kind, record, message) {
|
|
983
|
+
const key = `${kind}\u0000${record.identityKey}`;
|
|
984
|
+
const now = Date.now();
|
|
985
|
+
const state = this.nudgeDeliveryLogState.get(key);
|
|
986
|
+
if (state && now - state.lastEmittedAt < BG_LIFECYCLE_LOG_INTERVAL_MS) {
|
|
987
|
+
state.suppressed += 1;
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
const suppressed = state?.suppressed ?? 0;
|
|
991
|
+
this.nudgeDeliveryLogState.set(key, { lastEmittedAt: now, suppressed: 0 });
|
|
992
|
+
const suffix = suppressed > 0 ? ` suppressed=${suppressed}` : "";
|
|
993
|
+
log(`subc bg_events: ${message}${suffix}`, { sessionId: record.identity.session });
|
|
994
|
+
}
|
|
855
995
|
removeIndexMembership(record) {
|
|
856
996
|
const keys = this.rootIndex.get(record.canonicalRoot);
|
|
857
997
|
if (!keys)
|
|
@@ -1146,22 +1286,32 @@ export class SubcTransportPool {
|
|
|
1146
1286
|
return;
|
|
1147
1287
|
if (record.bgSub)
|
|
1148
1288
|
return;
|
|
1149
|
-
const
|
|
1150
|
-
const generation = record.generation;
|
|
1151
|
-
const nudgeRef = poolId !== undefined && generation !== undefined
|
|
1152
|
-
? {
|
|
1153
|
-
canonicalRoot: record.canonicalRoot,
|
|
1154
|
-
session: identity.session,
|
|
1155
|
-
concretePoolId: poolId,
|
|
1156
|
-
generation,
|
|
1157
|
-
}
|
|
1158
|
-
: undefined;
|
|
1289
|
+
const nudgeRef = this.nudgeRefFor(record);
|
|
1159
1290
|
const onNudge = () => {
|
|
1160
|
-
|
|
1291
|
+
const currentRecord = this.currentSessionForNudge(identity);
|
|
1292
|
+
if (!currentRecord) {
|
|
1293
|
+
this.logNudgeDelivery("drop-no-current-session", record, `nudge dropped cause=no-current-session root=${record.canonicalRoot}`);
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
if (currentRecord !== record) {
|
|
1297
|
+
this.logNudgeDelivery("forward-superseded-carrier", currentRecord, `nudge forwarding cause=superseded-carrying-record root=${currentRecord.canonicalRoot}`);
|
|
1298
|
+
}
|
|
1299
|
+
const currentRef = this.nudgeRefFor(currentRecord);
|
|
1300
|
+
let delivered = false;
|
|
1301
|
+
if (currentRef && this.onBgEventsNudgeRef) {
|
|
1302
|
+
this.onBgEventsNudgeRef(currentRef);
|
|
1303
|
+
delivered = true;
|
|
1304
|
+
}
|
|
1305
|
+
if (this.onBgEventsNudge) {
|
|
1306
|
+
if (!currentRef && this.onBgEventsNudgeRef) {
|
|
1307
|
+
this.logNudgeDelivery("fallback-missing-generation", currentRecord, `nudge dispatch fallback=root-session-handler cause=generation-provenance-unavailable root=${currentRecord.canonicalRoot}`);
|
|
1308
|
+
}
|
|
1309
|
+
this.onBgEventsNudge(currentRecord.identity.project_root, currentRecord.identity.session);
|
|
1310
|
+
delivered = true;
|
|
1311
|
+
}
|
|
1312
|
+
if (delivered)
|
|
1161
1313
|
return;
|
|
1162
|
-
this.
|
|
1163
|
-
if (nudgeRef)
|
|
1164
|
-
this.onBgEventsNudgeRef?.(nudgeRef);
|
|
1314
|
+
this.logNudgeDelivery("drop-no-compatible-handler", currentRecord, `nudge dropped cause=generation-provenance-unavailable-and-root-session-handler-unwired root=${currentRecord.canonicalRoot}`);
|
|
1165
1315
|
};
|
|
1166
1316
|
let sub = null;
|
|
1167
1317
|
const clearDormantSubscription = () => {
|
|
@@ -1173,7 +1323,7 @@ export class SubcTransportPool {
|
|
|
1173
1323
|
return false;
|
|
1174
1324
|
this.markRootDormant(record.canonicalRoot, error);
|
|
1175
1325
|
return true;
|
|
1176
|
-
}, clearDormantSubscription, nudgeRef, () => this.isCurrentSession(record.identityKey, record) &&
|
|
1326
|
+
}, clearDormantSubscription, this.bgDispatchProbeIntervalMs, nudgeRef, () => this.isCurrentSession(record.identityKey, record) &&
|
|
1177
1327
|
this.isCurrentLiveGeneration(record.canonicalRoot, record.generation));
|
|
1178
1328
|
record.bgSub = sub;
|
|
1179
1329
|
if (!this.rootCanAttach(record.canonicalRoot)) {
|
|
@@ -1250,8 +1400,25 @@ export class SubcTransportPool {
|
|
|
1250
1400
|
return Promise.resolve();
|
|
1251
1401
|
return registry.requestProjectRootClose(registration.concretePoolId, root, generation, cause);
|
|
1252
1402
|
}
|
|
1253
|
-
/**
|
|
1254
|
-
|
|
1403
|
+
/**
|
|
1404
|
+
* Subc reads config locally, but plugin registration facts are process state
|
|
1405
|
+
* and must accompany route requests because RouteBind has no such field.
|
|
1406
|
+
*/
|
|
1407
|
+
setConfigureOverride(key, value) {
|
|
1408
|
+
if (key !== "edit_slot_survives")
|
|
1409
|
+
return;
|
|
1410
|
+
if (typeof value !== "boolean") {
|
|
1411
|
+
throw new Error("edit_slot_survives must be set once to a boolean");
|
|
1412
|
+
}
|
|
1413
|
+
if (this.editSlotSurvivesCaptured) {
|
|
1414
|
+
throw new Error("edit_slot_survives is write-once and was already captured");
|
|
1415
|
+
}
|
|
1416
|
+
this.editSlotSurvives = value;
|
|
1417
|
+
this.editSlotSurvivesCaptured = true;
|
|
1418
|
+
}
|
|
1419
|
+
getEditSlotSurvives() {
|
|
1420
|
+
return this.editSlotSurvives;
|
|
1421
|
+
}
|
|
1255
1422
|
/** No-op over subc: the daemon owns the live module's configure lifecycle. */
|
|
1256
1423
|
async reconfigure(_projectRoot, _overrides) { }
|
|
1257
1424
|
/** No-op over subc: the daemon supervises the binary, not the plugin. */
|