instar 1.3.92 → 1.3.93
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/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +130 -0
- package/dist/commands/server.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +2 -2
- package/upgrades/1.3.92.md +14 -50
- package/upgrades/1.3.93.md +28 -0
- package/upgrades/side-effects/session-pool-activation-wiring.md +22 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAkSH,UAAU,YAAY;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;2DACuD;IACvD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAkSH,UAAU,YAAY;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;2DACuD;IACvD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAquDD,wBAAsB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA8yOtE;AAED,wBAAsB,UAAU,CAAC,OAAO,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAsDzE;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAuD5E"}
|
package/dist/commands/server.js
CHANGED
|
@@ -298,6 +298,15 @@ let _fixDeps = null;
|
|
|
298
298
|
// Module-level reference for session resume mapping.
|
|
299
299
|
// Set once in startServer() and used by spawnSessionForTopic/respawnSessionForTopic.
|
|
300
300
|
let _topicResumeMap = null;
|
|
301
|
+
// ── Multi-Machine Session Pool (§L4) activation refs ──────────────────────
|
|
302
|
+
// The SessionRouter is constructed in startServer()'s mesh block, but the inbound
|
|
303
|
+
// dispatch handler that consults it is defined ABOVE startServer — so the router is
|
|
304
|
+
// shared via this module-level ref (the same pattern as _orphanReaper/_topicResumeMap).
|
|
305
|
+
// `_sessionPoolStage()` reads the live rollout stage; the inbound interception only
|
|
306
|
+
// routes through the pool when it returns a non-'dark' stage, so DARK (the default)
|
|
307
|
+
// is byte-identical to today's always-local dispatch. Set once in startServer().
|
|
308
|
+
let _sessionRouter = null;
|
|
309
|
+
let _sessionPoolStage = () => 'dark';
|
|
301
310
|
/** Per-topic framework override (claude-code | codex-cli). Populated from
|
|
302
311
|
* `config.topicFrameworks` at server boot. Boot-immutable; runtime
|
|
303
312
|
* mutations go through `_topicFrameworksStore` instead so they persist
|
|
@@ -1156,6 +1165,31 @@ getHubDeps) {
|
|
|
1156
1165
|
const pipeline = messageToPipeline(msg, storedTopicName);
|
|
1157
1166
|
// Route message to corresponding session
|
|
1158
1167
|
const targetSession = telegram.getSessionForTopic(topicId);
|
|
1168
|
+
// ── Multi-Machine Session Pool (§L4): route through the pool when active ──
|
|
1169
|
+
// When the rollout stage is past 'dark', consult the SessionRouter: it may
|
|
1170
|
+
// forward this topic's message to the machine that OWNS the session (over the
|
|
1171
|
+
// mesh) instead of handling it locally. DARK (the default) skips this block
|
|
1172
|
+
// entirely → byte-identical to single-machine dispatch. Any error falls back
|
|
1173
|
+
// to the local path below (fail-safe). The live-transfer behavior is gated by
|
|
1174
|
+
// the staged rollout (StageAdvancer); shadow records ownership but stays local.
|
|
1175
|
+
if (_sessionRouter && _sessionPoolStage() !== 'dark') {
|
|
1176
|
+
try {
|
|
1177
|
+
const outcome = await _sessionRouter.route({
|
|
1178
|
+
sessionKey: String(topicId),
|
|
1179
|
+
messageId: String(msg.id),
|
|
1180
|
+
payload: text,
|
|
1181
|
+
});
|
|
1182
|
+
if (outcome.action === 'forwarded' || outcome.action === 'duplicate') {
|
|
1183
|
+
console.log(`[session-pool] topic ${topicId} handled by owner ${outcome.owner ?? '?'} (${outcome.action}) — not dispatching locally`);
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
// 'handled-locally' / 'spawned'(self) / 'owner-dead-replaced' / 'queued' /
|
|
1187
|
+
// 'placement-blocked' → fall through to the existing local dispatch below.
|
|
1188
|
+
}
|
|
1189
|
+
catch (err) {
|
|
1190
|
+
console.warn(`[session-pool] route error for topic ${topicId} — falling back to local dispatch: ${err instanceof Error ? err.message : String(err)}`);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1159
1193
|
if (targetSession) {
|
|
1160
1194
|
// Session is mapped — check if it's alive, inject or respawn
|
|
1161
1195
|
if (sessionManager.isSessionAlive(targetSession)) {
|
|
@@ -8283,6 +8317,32 @@ export async function startServer(options) {
|
|
|
8283
8317
|
deliverSeenFallback.add(messageId);
|
|
8284
8318
|
return true;
|
|
8285
8319
|
},
|
|
8320
|
+
// Owner-side bridge (§L4 handoff): a forwarded message landed → spawn/resume
|
|
8321
|
+
// the local session for the topic so the conversation continues on THIS machine.
|
|
8322
|
+
// Only fires for a FIRST-seen forwarded deliverMessage (the ledger dedupes
|
|
8323
|
+
// redeliveries), and a deliverMessage only arrives from a router peer — but we
|
|
8324
|
+
// double-gate on stage!=='dark' to be safe. Fire-and-forget: the durable receipt
|
|
8325
|
+
// is already recorded + ACKed before this runs.
|
|
8326
|
+
onAccepted: (cmd) => {
|
|
8327
|
+
if (_sessionPoolStage() === 'dark' || !telegram)
|
|
8328
|
+
return;
|
|
8329
|
+
const tg = telegram;
|
|
8330
|
+
const topicId = Number(cmd.session);
|
|
8331
|
+
if (!Number.isFinite(topicId))
|
|
8332
|
+
return;
|
|
8333
|
+
const text = typeof cmd.payload === 'string'
|
|
8334
|
+
? cmd.payload
|
|
8335
|
+
: (cmd.payload && typeof cmd.payload === 'object' && 'text' in cmd.payload)
|
|
8336
|
+
? String(cmd.payload.text)
|
|
8337
|
+
: undefined;
|
|
8338
|
+
const sessionName = tg.getSessionForTopic(topicId) ?? `topic-${topicId}`;
|
|
8339
|
+
void spawnSessionForTopic(sessionManager, tg, sessionName, topicId, text, undefined, undefined)
|
|
8340
|
+
.then((name) => {
|
|
8341
|
+
tg.registerTopicSession(topicId, name, sessionName);
|
|
8342
|
+
console.log(pc.green(` [session-pool] owner-side resume for forwarded topic ${topicId} → ${name}`));
|
|
8343
|
+
})
|
|
8344
|
+
.catch((err) => console.warn(` [session-pool] owner-side resume failed for topic ${topicId}: ${err instanceof Error ? err.message : String(err)}`));
|
|
8345
|
+
},
|
|
8286
8346
|
});
|
|
8287
8347
|
meshRpcDispatcher = new meshMod.MeshRpcDispatcher({
|
|
8288
8348
|
verify: {
|
|
@@ -8328,6 +8388,76 @@ export async function startServer(options) {
|
|
|
8328
8388
|
},
|
|
8329
8389
|
logger: (m) => console.log(pc.dim(` ${m}`)),
|
|
8330
8390
|
});
|
|
8391
|
+
// ── L4 SessionRouter (activation transport) — shared via _sessionRouter ──
|
|
8392
|
+
// Constructed with the real registry/ownership/placement + the outbound
|
|
8393
|
+
// MeshRpcClient so it can reach peers. INERT until the rollout stage advances
|
|
8394
|
+
// past 'dark': the inbound dispatch interception only consults it when
|
|
8395
|
+
// _sessionPoolStage() !== 'dark'. Single-machine → placement keeps everything local.
|
|
8396
|
+
try {
|
|
8397
|
+
const routerMod = await import('../core/SessionRouter.js');
|
|
8398
|
+
const placeMod = await import('../core/PlacementExecutor.js');
|
|
8399
|
+
const clientMod = await import('../core/MeshRpcClient.js');
|
|
8400
|
+
let routerNonce = 0;
|
|
8401
|
+
const meshClient = new clientMod.MeshRpcClient({
|
|
8402
|
+
selfMachineId: meshSelfId,
|
|
8403
|
+
sign: (c) => idMod.sign(c, localSigningKeyPem),
|
|
8404
|
+
nonce: () => `${meshSelfId}:r:${Date.now()}:${++routerNonce}`,
|
|
8405
|
+
now: () => Date.now(),
|
|
8406
|
+
});
|
|
8407
|
+
const peerUrl = (machineId) => meshIdMgr.getActiveMachines().find((m) => m.machineId === machineId)?.entry.lastKnownUrl ?? null;
|
|
8408
|
+
_sessionRouter = new routerMod.SessionRouter({
|
|
8409
|
+
selfMachineId: meshSelfId,
|
|
8410
|
+
placement: new placeMod.PlacementExecutor(),
|
|
8411
|
+
machineRegistry: () => machinePoolRegistry?.getCapacities() ?? [],
|
|
8412
|
+
resolveOwnership: (sk) => {
|
|
8413
|
+
const r = ownReg.read(sk);
|
|
8414
|
+
if (!r)
|
|
8415
|
+
return { owner: null, epoch: 0, status: null };
|
|
8416
|
+
const status = r.status === 'released' ? null : r.status;
|
|
8417
|
+
return { owner: ownReg.ownerOf(sk), epoch: r.ownershipEpoch, status, target: ownReg.placementTargetOf(sk) ?? undefined };
|
|
8418
|
+
},
|
|
8419
|
+
isMachineAlive: (m) => m === meshSelfId || (machinePoolRegistry?.getCapacity(m)?.online ?? false),
|
|
8420
|
+
casClaimOwnership: (sk, machineId) => {
|
|
8421
|
+
const r = ownReg.cas({ type: 'place', machineId }, { sessionKey: sk, sender: meshSelfId, nonce: `${meshSelfId}:c:${++routerNonce}` });
|
|
8422
|
+
return { ok: r.ok, epoch: ownReg.read(sk)?.ownershipEpoch ?? 0 };
|
|
8423
|
+
},
|
|
8424
|
+
deliverMessage: async (target, env) => {
|
|
8425
|
+
const url = peerUrl(target);
|
|
8426
|
+
if (!url)
|
|
8427
|
+
throw new Error(`no peer url for ${target}`);
|
|
8428
|
+
const res = await meshClient.send({ machineId: target, url }, { type: 'deliverMessage', session: env.sessionKey, messageId: env.messageId, payload: env.payload, ownershipEpoch: env.ownershipEpoch }, env.ownershipEpoch);
|
|
8429
|
+
if (res.ok && res.result && typeof res.result === 'object' && 'accepted' in res.result) {
|
|
8430
|
+
return res.result;
|
|
8431
|
+
}
|
|
8432
|
+
throw new Error(`deliverMessage rejected: ${res.status} ${res.reason ?? ''}`);
|
|
8433
|
+
},
|
|
8434
|
+
spawnOnMachine: async (machineId, msg) => {
|
|
8435
|
+
const url = peerUrl(machineId);
|
|
8436
|
+
if (!url)
|
|
8437
|
+
throw new Error(`no peer url for ${machineId}`);
|
|
8438
|
+
await meshClient.send({ machineId, url }, { type: 'deliverMessage', session: msg.sessionKey, messageId: msg.messageId, payload: msg.payload, ownershipEpoch: 0 }, 0);
|
|
8439
|
+
},
|
|
8440
|
+
handleLocally: async () => { },
|
|
8441
|
+
queueMessage: () => { },
|
|
8442
|
+
raiseAttention: (title, body) => console.log(pc.dim(` [session-router] attention: ${title} — ${body}`)),
|
|
8443
|
+
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
8444
|
+
log: (line) => console.log(pc.dim(` [session-router] ${line}`)),
|
|
8445
|
+
});
|
|
8446
|
+
_sessionPoolStage = () => {
|
|
8447
|
+
try {
|
|
8448
|
+
const fallback = (config.multiMachine?.sessionPool ?? {});
|
|
8449
|
+
const live = liveConfig.get('multiMachine.sessionPool', fallback);
|
|
8450
|
+
return live?.enabled && live?.stage ? String(live.stage) : 'dark';
|
|
8451
|
+
}
|
|
8452
|
+
catch {
|
|
8453
|
+
return 'dark';
|
|
8454
|
+
}
|
|
8455
|
+
};
|
|
8456
|
+
console.log(pc.green(' SessionRouter wired (L4) — inert until rollout stage advances past dark'));
|
|
8457
|
+
}
|
|
8458
|
+
catch (err) {
|
|
8459
|
+
console.log(pc.dim(` [session-router] not wired: ${err instanceof Error ? err.message : String(err)}`));
|
|
8460
|
+
}
|
|
8331
8461
|
}
|
|
8332
8462
|
}
|
|
8333
8463
|
catch (err) {
|