instar 1.3.508 → 1.3.510

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.
Files changed (80) hide show
  1. package/dist/commands/server.d.ts.map +1 -1
  2. package/dist/commands/server.js +156 -2
  3. package/dist/commands/server.js.map +1 -1
  4. package/dist/core/AutonomousSessions.d.ts +8 -0
  5. package/dist/core/AutonomousSessions.d.ts.map +1 -1
  6. package/dist/core/AutonomousSessions.js +34 -0
  7. package/dist/core/AutonomousSessions.js.map +1 -1
  8. package/dist/core/MeshRpc.d.ts +6 -1
  9. package/dist/core/MeshRpc.d.ts.map +1 -1
  10. package/dist/core/MeshRpc.js +7 -0
  11. package/dist/core/MeshRpc.js.map +1 -1
  12. package/dist/core/OwnershipReconciler.d.ts.map +1 -1
  13. package/dist/core/OwnershipReconciler.js +17 -0
  14. package/dist/core/OwnershipReconciler.js.map +1 -1
  15. package/dist/core/SessionDrainRunner.d.ts +116 -0
  16. package/dist/core/SessionDrainRunner.d.ts.map +1 -0
  17. package/dist/core/SessionDrainRunner.js +162 -0
  18. package/dist/core/SessionDrainRunner.js.map +1 -0
  19. package/dist/core/SessionManager.d.ts +4 -0
  20. package/dist/core/SessionManager.d.ts.map +1 -1
  21. package/dist/core/SessionManager.js +3 -0
  22. package/dist/core/SessionManager.js.map +1 -1
  23. package/dist/core/SessionOwnership.d.ts +22 -1
  24. package/dist/core/SessionOwnership.d.ts.map +1 -1
  25. package/dist/core/SessionOwnership.js +17 -1
  26. package/dist/core/SessionOwnership.js.map +1 -1
  27. package/dist/core/TopicProfileOrchestrator.d.ts +3 -1
  28. package/dist/core/TopicProfileOrchestrator.d.ts.map +1 -1
  29. package/dist/core/TopicProfileOrchestrator.js +8 -3
  30. package/dist/core/TopicProfileOrchestrator.js.map +1 -1
  31. package/dist/core/TopicProfileResolver.d.ts +9 -1
  32. package/dist/core/TopicProfileResolver.d.ts.map +1 -1
  33. package/dist/core/TopicProfileResolver.js +38 -2
  34. package/dist/core/TopicProfileResolver.js.map +1 -1
  35. package/dist/core/TopicProfileStore.d.ts +8 -1
  36. package/dist/core/TopicProfileStore.d.ts.map +1 -1
  37. package/dist/core/TopicProfileStore.js +2 -2
  38. package/dist/core/TopicProfileStore.js.map +1 -1
  39. package/dist/core/classifyProfileChange.d.ts.map +1 -1
  40. package/dist/core/classifyProfileChange.js +32 -1
  41. package/dist/core/classifyProfileChange.js.map +1 -1
  42. package/dist/core/frameworkSessionLaunch.d.ts +28 -1
  43. package/dist/core/frameworkSessionLaunch.d.ts.map +1 -1
  44. package/dist/core/frameworkSessionLaunch.js +35 -3
  45. package/dist/core/frameworkSessionLaunch.js.map +1 -1
  46. package/dist/core/seamlessnessConfig.d.ts +8 -1
  47. package/dist/core/seamlessnessConfig.d.ts.map +1 -1
  48. package/dist/core/seamlessnessConfig.js +8 -1
  49. package/dist/core/seamlessnessConfig.js.map +1 -1
  50. package/dist/core/topicProfileIngress.d.ts.map +1 -1
  51. package/dist/core/topicProfileIngress.js +14 -0
  52. package/dist/core/topicProfileIngress.js.map +1 -1
  53. package/dist/core/topicProfileValidation.d.ts +12 -0
  54. package/dist/core/topicProfileValidation.d.ts.map +1 -1
  55. package/dist/core/topicProfileValidation.js +30 -0
  56. package/dist/core/topicProfileValidation.js.map +1 -1
  57. package/dist/core/topicProfileWriteSurface.d.ts.map +1 -1
  58. package/dist/core/topicProfileWriteSurface.js +3 -2
  59. package/dist/core/topicProfileWriteSurface.js.map +1 -1
  60. package/dist/core/types.d.ts +6 -0
  61. package/dist/core/types.d.ts.map +1 -1
  62. package/dist/core/types.js.map +1 -1
  63. package/dist/server/AgentServer.d.ts +8 -0
  64. package/dist/server/AgentServer.d.ts.map +1 -1
  65. package/dist/server/AgentServer.js +1 -0
  66. package/dist/server/AgentServer.js.map +1 -1
  67. package/dist/server/middleware.d.ts +14 -0
  68. package/dist/server/middleware.d.ts.map +1 -1
  69. package/dist/server/middleware.js +17 -0
  70. package/dist/server/middleware.js.map +1 -1
  71. package/dist/server/routes.d.ts +11 -0
  72. package/dist/server/routes.d.ts.map +1 -1
  73. package/dist/server/routes.js +50 -3
  74. package/dist/server/routes.js.map +1 -1
  75. package/package.json +1 -1
  76. package/src/data/builtin-manifest.json +48 -48
  77. package/upgrades/1.3.509.md +74 -0
  78. package/upgrades/1.3.510.md +54 -0
  79. package/upgrades/side-effects/multi-machine-seamlessness-ws12b-drain.md +205 -0
  80. package/upgrades/side-effects/topic-effort-pin.md +117 -0
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAgCH,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAS3D,OAAO,EAAE,eAAe,EAAiC,MAAM,iCAAiC,CAAC;AAuBjG,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAGvD,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AA0G7D,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAsBtD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,gBAAgB,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC1C,OAAO,CAUT;AAyID,UAAU,YAAY;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;2DACuD;IACvD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AA4zCD,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,eAAe,EACzB,cAAc,EAAE,cAAc,EAC9B,YAAY,CAAC,EAAE,YAAY,EAC3B,WAAW,CAAC,EAAE,WAAW,EACzB,WAAW,CAAC,EAAE,WAAW,EACzB,iBAAiB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,EAGvE,UAAU,CAAC,EAAE,MAAM,OAAO,8BAA8B,EAAE,WAAW,GAAG,IAAI,EAK5E,qBAAqB,CAAC,EAAE,MAAM,OAAO,gCAAgC,EAAE,kBAAkB,GAAG,IAAI,EAKhG,mBAAmB,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,GAAG,SAAS,GACpD,IAAI,CA8eN;AA2lBD,wBAAsB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA8pYtE;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;AAgCH,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAS3D,OAAO,EAAE,eAAe,EAAiC,MAAM,iCAAiC,CAAC;AAuBjG,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAGvD,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AA0G7D,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAsBtD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,gBAAgB,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC1C,OAAO,CAUT;AAyID,UAAU,YAAY;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;2DACuD;IACvD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AA80CD,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,eAAe,EACzB,cAAc,EAAE,cAAc,EAC9B,YAAY,CAAC,EAAE,YAAY,EAC3B,WAAW,CAAC,EAAE,WAAW,EACzB,WAAW,CAAC,EAAE,WAAW,EACzB,iBAAiB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,EAGvE,UAAU,CAAC,EAAE,MAAM,OAAO,8BAA8B,EAAE,WAAW,GAAG,IAAI,EAK5E,qBAAqB,CAAC,EAAE,MAAM,OAAO,gCAAgC,EAAE,kBAAkB,GAAG,IAAI,EAKhG,mBAAmB,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,GAAG,SAAS,GACpD,IAAI,CA8eN;AA2lBD,wBAAsB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAgyYtE;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"}
@@ -385,6 +385,10 @@ let _inboundQueueStop = null;
385
385
  * (forward/spawn on another machine → must NOT also dispatch locally) from a
386
386
  * self placement. Set once in startServer()'s mesh block. */
387
387
  let _meshSelfId = null;
388
+ /** WS1.2: the owner-side drain runner (null = pool dark / deps unavailable).
389
+ * Presence IS the heartbeat-advertised ws12DrainReceive capability — a
390
+ * machine only advertises what it can actually execute. */
391
+ let _drainRunner = null;
388
392
  /** WS1.1: read-only ownership lookup for the drain's spawn-boundary re-check
389
393
  * (the registry is constructed later in startServer's pool block). */
390
394
  let _ownershipReadForDrain = null;
@@ -396,6 +400,10 @@ let _resolveRouterUrl = null;
396
400
  /** Every OTHER active machine with a known URL — backs GET /sessions?scope=pool
397
401
  * (pool-wide session aggregation for the dashboard). Set in the same mesh block. */
398
402
  let _resolvePeerUrls = null;
403
+ /** WS1.2 sender leg: order a REMOTE owner to drain `sessionKey` for a transfer
404
+ * to `target` (signed mesh `drain` verb). Set in the mesh-client block; null =
405
+ * pool dark / no client — the transfer route degrades to today's pin path. */
406
+ let _sendDrain = null;
399
407
  let _listPoolMachines = null;
400
408
  /** Multi-Machine Session Pool §L4: per-topic placement pin store ("move this to <nickname>"). */
401
409
  let _topicPinStore = null;
@@ -797,6 +805,9 @@ spawnOpts) {
797
805
  ? resolvedProfile.model
798
806
  : undefined;
799
807
  const profileThinkingMode = resolvedProfile?.thinkingMode;
808
+ // Topic Profile — per-topic Claude `--effort` pin (already enum-clamped /
809
+ // fail-open in the resolver; undefined = no --effort, today's behavior).
810
+ const profileEffort = resolvedProfile?.effort;
800
811
  const newSessionName = await sessionManager.spawnInteractiveSession(bootstrapMessage, sessionName, {
801
812
  telegramTopicId: topicId,
802
813
  resumeSessionId,
@@ -805,6 +816,7 @@ spawnOpts) {
805
816
  ...(codexLocalModelOverride ? { defaultModel: codexLocalModelOverride } : {}),
806
817
  ...(profileModel ? { defaultModel: profileModel } : {}),
807
818
  ...(profileThinkingMode ? { thinkingMode: profileThinkingMode } : {}),
819
+ ...(profileEffort ? { effort: profileEffort } : {}),
808
820
  // Subscription & Auth Standard P1.3 (additive): account-swap — launch under
809
821
  // this account's config home + record its id. Unset = unchanged.
810
822
  ...(accountSwap?.configHome ? { configHome: accountSwap.configHome } : {}),
@@ -1268,13 +1280,16 @@ function wireTelegramCallbacks(telegram, sessionManager, state, quotaTracker, ac
1268
1280
  else if (head === 'thinking' && parts.length === 2) {
1269
1281
  patch = { thinkingMode: parts[1].toLowerCase() };
1270
1282
  }
1283
+ else if (head === 'effort' && parts.length === 2) {
1284
+ patch = { effort: parts[1].toLowerCase() };
1285
+ }
1271
1286
  else if (head === 'escalation' && parts.length === 2) {
1272
1287
  patch = { escalationOverride: parts[1].toLowerCase() };
1273
1288
  }
1274
1289
  if (!patch) {
1275
1290
  return {
1276
1291
  ok: false,
1277
- message: 'Usage: /topic [status] · /topic <framework> · /topic model <id> · /topic tier <default|escalated> · /topic thinking <off|low|medium|high|max> · /topic escalation <inherit|suppress> · /topic clear · /topic undo · /topic re-apply — or just tell me in plain words.',
1292
+ message: 'Usage: /topic [status] · /topic <framework> · /topic model <id> · /topic tier <default|escalated> · /topic thinking <off|low|medium|high|max> · /topic effort <low|medium|high|xhigh|max> · /topic escalation <inherit|suppress> · /topic clear · /topic undo · /topic re-apply — or just tell me in plain words.',
1278
1293
  };
1279
1294
  }
1280
1295
  const result = await surface.applyWrite({
@@ -11929,7 +11944,7 @@ export async function startServer(options) {
11929
11944
  // WS1.1 capability advertisement (spec invariant 5): a bounded
11930
11945
  // fixed-size summary, never an inventory. Reported live each
11931
11946
  // heartbeat so a queue going dark withdraws the capability.
11932
- seamlessnessFlags: { ws11DeliverReceive: !!_inboundQueue },
11947
+ seamlessnessFlags: { ws11DeliverReceive: !!_inboundQueue, ws12DrainReceive: !!_drainRunner },
11933
11948
  // Durable Inbound Message Queue §5.1: depth + oldest + tenure +
11934
11949
  // bounded top-K — the survivor's loss-SUSPECTED item, capped
11935
11950
  // re-placement arm, and supersede-dedupe key all read these.
@@ -12319,6 +12334,85 @@ export async function startServer(options) {
12319
12334
  });
12320
12335
  },
12321
12336
  });
12337
+ // ── WS1.2 owner-side drain runner (MULTI-MACHINE-SEAMLESSNESS-SPEC) ──
12338
+ // Constructed here because every dep lives in this scope. Presence IS
12339
+ // the heartbeat-advertised ws12DrainReceive capability. All I/O
12340
+ // injected; the runner itself is the unit-tested pure sequence.
12341
+ try {
12342
+ const drainMod = await import('../core/SessionDrainRunner.js');
12343
+ const autoMod = await import('../core/AutonomousSessions.js');
12344
+ let drainNonce = 0;
12345
+ _drainRunner = new drainMod.SessionDrainRunner({
12346
+ selfMachineId: meshSelfId,
12347
+ readOwnership: (sk) => ownReg.read(sk),
12348
+ cas: (action, c) => {
12349
+ const prev = ownReg.read(c.sessionKey)?.ownerMachineId;
12350
+ const r = ownReg.cas(action, c);
12351
+ // Journal pairing (§3.3): the drain's transfer/abort/claim CASes
12352
+ // record placement history like every other CAS site.
12353
+ emitPlacement(c.sessionKey, r, 'user-move', prev);
12354
+ return r;
12355
+ },
12356
+ suspendAutonomousRun: (topic, target) => autoMod.suspendAutonomousTopicForMove(config.stateDir, topic, target, coherenceJournal ?? undefined),
12357
+ sessionQuiet: (sk) => {
12358
+ const topicNum = Number(sk);
12359
+ if (!Number.isFinite(topicNum) || !telegram)
12360
+ return true; // nothing local to drain
12361
+ const tmux = telegram.getSessionForTopic(topicNum);
12362
+ if (!tmux || !sessionManager.isSessionAlive(tmux))
12363
+ return true;
12364
+ return !sessionManager.isSessionActivelyWorking(tmux);
12365
+ },
12366
+ // Emergency stop: the durable flag file freshly touched (within the
12367
+ // drain window's scale) — a stale flag from an old stop must not
12368
+ // permanently veto every future transfer.
12369
+ emergencyStopActive: () => {
12370
+ try {
12371
+ const flagAt = fs.statSync(path.join(config.projectDir, '.instar', 'autonomous-emergency-stop')).mtimeMs;
12372
+ return Date.now() - flagAt < 120_000;
12373
+ }
12374
+ catch { /* @silent-fallback-ok — no flag file = no emergency stop (the defined absent-state, not a degradation) */
12375
+ return false;
12376
+ }
12377
+ },
12378
+ terminateSession: async (sk, reason, opts) => {
12379
+ const topicNum = Number(sk);
12380
+ const tmux = Number.isFinite(topicNum) ? telegram?.getSessionForTopic(topicNum) : null;
12381
+ const rec = tmux ? state.listSessions().find((s) => s.tmuxSession === tmux) : undefined;
12382
+ if (!rec)
12383
+ return { terminated: false, skipped: 'no-local-session' };
12384
+ return sessionManager.terminateSession(rec.id, reason, {
12385
+ origin: 'autonomous',
12386
+ via: 'ws12-drain',
12387
+ bypassActiveProcessKeep: opts.force,
12388
+ workEvidence: opts.force ? ['active-build-or-autonomous-run'] : undefined,
12389
+ });
12390
+ },
12391
+ markInterrupted: (topic) => { autoMod.markAutonomousInterruptedMidTask(config.stateDir, topic); },
12392
+ notifyInterrupted: (topic, target, detail) => {
12393
+ const topicNum = Number(topic);
12394
+ if (!telegram || !Number.isFinite(topicNum))
12395
+ return;
12396
+ void telegram
12397
+ .sendToTopic(topicNum, `This conversation moved machines mid-task (${detail}). Work resumes on the new machine — the final turn before the move may be partial.`)
12398
+ .catch(() => { });
12399
+ },
12400
+ audit: (event) => {
12401
+ try {
12402
+ console.log(pc.dim(` [ws12-drain] ${JSON.stringify(event)}`));
12403
+ }
12404
+ catch { /* @silent-fallback-ok — observability only */ }
12405
+ },
12406
+ now: () => Date.now(),
12407
+ sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
12408
+ nonce: () => `${meshSelfId}:drain:${Date.now()}:${++drainNonce}`,
12409
+ });
12410
+ console.log(pc.dim(' [ws12-drain] owner-side drain runner wired (capability advertised)'));
12411
+ }
12412
+ catch (err) {
12413
+ // @silent-fallback-ok — runner stays null (capability un-advertised → no peer sends a drain order; the transfer route degrades to today's pin path). Logged with context; not a degradation worth a report.
12414
+ console.log(pc.dim(` [ws12-drain] runner not wired: ${err instanceof Error ? err.message : String(err)}`));
12415
+ }
12322
12416
  // ── Secret-sync inbound handler (cross-machine secret distribution, spec Phase 4) ──
12323
12417
  // Dark by default; live on the dev agent via the developmentAgent gate. An inbound
12324
12418
  // `secret-share` command is decrypted to THIS machine's X25519 key and stored in the
@@ -12508,6 +12602,30 @@ export async function startServer(options) {
12508
12602
  transfer: ownAction,
12509
12603
  release: ownAction,
12510
12604
  deliverMessage: deliverMessageHandler,
12605
+ // WS1.2 owner-side drain (MULTI-MACHINE-SEAMLESSNESS-SPEC): the
12606
+ // router orders THIS machine — the topic's current owner — to
12607
+ // finish the live turn (bounded), close the local session, and
12608
+ // land the target's claim, releasing the queue's barrier exactly
12609
+ // at drain completion. RBAC is router-only ('drain-unauthorized');
12610
+ // the runner re-validates ownership + epoch at its CAS fence
12611
+ // regardless (reach ≠ authority). Answers 'drain disabled' until
12612
+ // the runner is constructed (pool dark / deps unavailable) — the
12613
+ // sender's capability gate means a doomed order is never sent to
12614
+ // a machine that advertises the flag honestly.
12615
+ drain: async (cmd) => {
12616
+ const c = cmd;
12617
+ if (!_drainRunner)
12618
+ return { ok: false, reason: 'drain disabled' };
12619
+ const outcome = await _drainRunner.run({
12620
+ sessionKey: c.session,
12621
+ target: c.target,
12622
+ senderObservedEpoch: c.ownershipEpoch,
12623
+ });
12624
+ return {
12625
+ ok: outcome.status === 'drained' || outcome.status === 'drained-interrupted',
12626
+ ...outcome,
12627
+ };
12628
+ },
12511
12629
  'secret-share': (cmd, sender) => _secretShareHandler
12512
12630
  ? _secretShareHandler.handle(cmd, sender)
12513
12631
  : { ok: false, reason: 'secret-sync disabled' },
@@ -12532,6 +12650,42 @@ export async function startServer(options) {
12532
12650
  });
12533
12651
  const peerUrl = (machineId) => meshIdMgr.getActiveMachines().find((m) => m.machineId === machineId)?.entry.lastKnownUrl ?? null;
12534
12652
  _meshSelfId = meshSelfId;
12653
+ // WS1.2 sender leg: signed drain order to a remote owner. Bounded by
12654
+ // the drain bound + slack so a clean drain (≤30s wait + close) can
12655
+ // complete within one call; every failure shape maps to an explicit
12656
+ // outcome — the transfer route NEVER hangs on this and degrades to
12657
+ // today's pin path on anything but a real abort.
12658
+ _sendDrain = async (ownerMachineId, sessionKey, target, ownershipEpoch) => {
12659
+ // Local-owner arm: the live swap topology (owner == holder == this
12660
+ // machine) drains via the SAME runner the mesh handler uses — one
12661
+ // code path, no HTTP round-trip to ourselves.
12662
+ if (ownerMachineId === meshSelfId) {
12663
+ if (!_drainRunner)
12664
+ return { ok: false, reason: 'drain disabled' };
12665
+ const o = await _drainRunner.run({ sessionKey, target, senderObservedEpoch: ownershipEpoch });
12666
+ return {
12667
+ ok: o.status === 'drained' || o.status === 'drained-interrupted',
12668
+ status: o.status,
12669
+ reason: o.detail,
12670
+ runSuspended: o.autonomousRunSuspended,
12671
+ };
12672
+ }
12673
+ const url = peerUrl(ownerMachineId);
12674
+ if (!url)
12675
+ return { ok: false, reason: 'no-peer-url' };
12676
+ try {
12677
+ const res = await meshClient.send({ machineId: ownerMachineId, url }, { type: 'drain', session: sessionKey, target, ownershipEpoch }, ownershipEpoch, { timeoutMs: 50_000 });
12678
+ if (res.ok) {
12679
+ const r = (res.result ?? {});
12680
+ return { ok: r.ok === true, status: typeof r.status === 'string' ? r.status : undefined, reason: typeof r.reason === 'string' ? r.reason : undefined };
12681
+ }
12682
+ return { ok: false, reason: res.reason ?? `status-${res.status}`, noHandler: res.status === 501 };
12683
+ }
12684
+ catch (err) {
12685
+ // @silent-fallback-ok — a failed drain RPC returns ok:false to the route, which records it and degrades to today's pin path (never a stuck/half transfer); the reason is surfaced in the response's drain field.
12686
+ return { ok: false, reason: err instanceof Error ? err.message : String(err) };
12687
+ }
12688
+ };
12535
12689
  _resolveRouterUrl = () => {
12536
12690
  const h = coordinator.getSyncStatus().leaseHolder;
12537
12691
  return h && h !== meshSelfId ? peerUrl(h) : null;