instar 1.3.103 → 1.3.105

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.
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAmSH,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,CAu3OtE;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"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAmSH,UAAU,YAAY;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;2DACuD;IACvD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAwvDD,wBAAsB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA+7OtE;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"}
@@ -308,6 +308,10 @@ let _topicResumeMap = null;
308
308
  // is byte-identical to today's always-local dispatch. Set once in startServer().
309
309
  let _sessionRouter = null;
310
310
  let _sessionPoolStage = () => 'dark';
311
+ /** Multi-Machine Session Pool §L4: per-topic placement pin store ("move this to <nickname>"). */
312
+ let _topicPinStore = null;
313
+ /** Recognize + apply a "move/run this on <nickname>" relocation on inbound; returns handled=true when the message WAS a relocation command (so it must not also be dispatched). */
314
+ let _tryNicknameRelocation = null;
311
315
  /** Per-topic framework override (claude-code | codex-cli). Populated from
312
316
  * `config.topicFrameworks` at server boot. Boot-immutable; runtime
313
317
  * mutations go through `_topicFrameworksStore` instead so they persist
@@ -1173,12 +1177,29 @@ getHubDeps) {
1173
1177
  // entirely → byte-identical to single-machine dispatch. Any error falls back
1174
1178
  // to the local path below (fail-safe). The live-transfer behavior is gated by
1175
1179
  // the staged rollout (StageAdvancer); shadow records ownership but stays local.
1180
+ //
1181
+ // §L4 transfer-by-nickname: FIRST, intercept an explicit "move/run this on
1182
+ // <nickname>" relocation command. If recognized, it sets the topic's
1183
+ // placement pin (+ releases local ownership) and is fully handled here — the
1184
+ // command message itself is NOT dispatched to a session. Subsequent messages
1185
+ // for the topic then re-place onto the pinned machine via route() below.
1186
+ if (_tryNicknameRelocation && _sessionPoolStage() !== 'dark') {
1187
+ try {
1188
+ const relo = await _tryNicknameRelocation(topicId, text);
1189
+ if (relo.handled)
1190
+ return;
1191
+ }
1192
+ catch (err) {
1193
+ console.warn(`[session-pool] nickname relocation error for topic ${topicId}: ${err instanceof Error ? err.message : String(err)}`);
1194
+ }
1195
+ }
1176
1196
  if (_sessionRouter && _sessionPoolStage() !== 'dark') {
1177
1197
  try {
1178
1198
  const outcome = await _sessionRouter.route({
1179
1199
  sessionKey: String(topicId),
1180
1200
  messageId: String(msg.id),
1181
1201
  payload: text,
1202
+ topicMetadata: _topicPinStore?.asTopicMetadata(String(topicId)),
1182
1203
  });
1183
1204
  if (outcome.action === 'forwarded' || outcome.action === 'duplicate') {
1184
1205
  console.log(`[session-pool] topic ${topicId} handled by owner ${outcome.owner ?? '?'} (${outcome.action}) — not dispatching locally`);
@@ -2193,9 +2214,20 @@ export async function startServer(options) {
2193
2214
  let localSigningKeyPem = '';
2194
2215
  if (coordinator.enabled && coordinator.identity) {
2195
2216
  try {
2196
- const keyPath = path.join(config.stateDir, 'machine', 'signing-private.pem');
2197
- if (fs.existsSync(keyPath)) {
2198
- localSigningKeyPem = fs.readFileSync(keyPath, 'utf-8');
2217
+ // The canonical signing-key filename is `signing-key.pem` (MachineIdentity.SIGNING_KEY_FILE,
2218
+ // what every normally-created install has + what idMgr.loadSigningKey reads). This loader
2219
+ // historically hard-coded `signing-private.pem`, which only EXISTS on installs that were
2220
+ // propagated with that (non-canonical) name — so a normally-created machine got an EMPTY
2221
+ // localSigningKeyPem, its MeshRpcClient then signed every cross-machine command with no key,
2222
+ // the send threw, and it could not PULL/transfer to any peer (while still RECEIVING fine,
2223
+ // since receive verifies the OTHER machine's key). Try the canonical name first, then fall
2224
+ // back to the legacy/propagated name so both layouts work.
2225
+ for (const name of ['signing-key.pem', 'signing-private.pem']) {
2226
+ const keyPath = path.join(config.stateDir, 'machine', name);
2227
+ if (fs.existsSync(keyPath)) {
2228
+ localSigningKeyPem = fs.readFileSync(keyPath, 'utf-8');
2229
+ break;
2230
+ }
2199
2231
  }
2200
2232
  }
2201
2233
  catch { /* @silent-fallback-ok — signing key optional */ }
@@ -8507,6 +8539,66 @@ export async function startServer(options) {
8507
8539
  }
8508
8540
  };
8509
8541
  console.log(pc.green(' SessionRouter wired (L4) — inert until rollout stage advances past dark'));
8542
+ // ── L4 transfer-by-nickname activation: the "move/run this on <nickname>"
8543
+ // trigger. The recognizer + planner are pure units; this wires them to the
8544
+ // pin store + ownership so a recognized command pins the topic to the named
8545
+ // machine and (if we currently own it) releases it, so the topic's next
8546
+ // routed message re-places onto the pinned machine via the already-wired
8547
+ // placeAndClaim → spawnOnMachine → owner-side onAccepted resume path.
8548
+ // Inert unless the rollout stage is past 'dark' (gated at the call site).
8549
+ const nickMod = await import('../core/NicknameCommand.js');
8550
+ const transferMod = await import('../core/TransferByNickname.js');
8551
+ const pinMod = await import('../core/TopicPlacementPinStore.js');
8552
+ _topicPinStore = new pinMod.TopicPlacementPinStore({
8553
+ filePath: path.join(config.stateDir, 'session-pool', 'topic-pins.json'),
8554
+ });
8555
+ _tryNicknameRelocation = async (topicId, text) => {
8556
+ const sessionKey = String(topicId);
8557
+ const caps = machinePoolRegistry?.getCapacities() ?? [];
8558
+ const nickToMachine = new Map();
8559
+ for (const c of caps)
8560
+ if (c.nickname)
8561
+ nickToMachine.set(c.nickname.toLowerCase(), c.machineId);
8562
+ const knownNicknames = caps.map((c) => c.nickname).filter((n) => !!n);
8563
+ const cmd = nickMod.recognizeNicknameCommand(text, knownNicknames);
8564
+ if (!cmd)
8565
+ return { handled: false };
8566
+ const plan = transferMod.planTransferByNickname(cmd, {
8567
+ resolveNickname: (n) => nickToMachine.get(n.toLowerCase()) ?? null,
8568
+ validNicknames: () => knownNicknames,
8569
+ isOnline: (m) => machinePoolRegistry?.getCapacity(m)?.online ?? false,
8570
+ currentOwnerOf: (sk) => ownReg.ownerOf(sk),
8571
+ isMidReply: () => false, // best-effort; the pin takes effect on the next routed message
8572
+ lastPlacementUpdateAt: (sk) => _topicPinStore?.lastUpdatedAtMs(sk) ?? null,
8573
+ now: () => Date.now(),
8574
+ }, sessionKey);
8575
+ if (plan.action === 'transfer' || plan.action === 'noop') {
8576
+ const target = plan.targetMachine;
8577
+ _topicPinStore.set(sessionKey, target, plan.setPin ?? true);
8578
+ // If THIS machine actively owns the topic, release so the next message re-places to the pin.
8579
+ try {
8580
+ if (ownReg.ownerOf(sessionKey) === meshSelfId) {
8581
+ ownReg.cas({ type: 'release', machineId: meshSelfId }, { sessionKey, sender: meshSelfId, nonce: `${meshSelfId}:rel:${sessionKey}:${Math.round(performance.now())}` });
8582
+ }
8583
+ }
8584
+ catch { /* best-effort; route() re-places regardless once the owner is cleared */ }
8585
+ await telegram?.sendToTopic(topicId, plan.action === 'noop'
8586
+ ? `This conversation is already pinned to ${cmd.nickname} — it'll keep running there.`
8587
+ : `Moving this conversation to ${cmd.nickname} — it'll pick up there on your next message.`).catch(() => { });
8588
+ console.log(pc.green(` [session-pool] topic ${topicId} pinned to ${target} (${plan.action}) via "${cmd.matchedVerb}"`));
8589
+ return { handled: true };
8590
+ }
8591
+ if (plan.action === 'confirm-required') {
8592
+ await telegram?.sendToTopic(topicId, plan.confirmationPrompt ?? `That machine isn't reachable right now — say "yes, move it" to confirm.`).catch(() => { });
8593
+ return { handled: true };
8594
+ }
8595
+ // reject
8596
+ const validList = (plan.validNicknames ?? []).join(', ');
8597
+ await telegram?.sendToTopic(topicId, plan.rejectReason === 'unknown-machine-nickname'
8598
+ ? `I don't know a machine called "${plan.detail}". I can move this to: ${validList || '(no other machines available)'}.`
8599
+ : `I can't move this right now (${plan.rejectReason}).`).catch(() => { });
8600
+ return { handled: true };
8601
+ };
8510
8602
  }
8511
8603
  catch (err) {
8512
8604
  console.log(pc.dim(` [session-router] not wired: ${err instanceof Error ? err.message : String(err)}`));