instar 1.3.507 → 1.3.509

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 (99) hide show
  1. package/dist/commands/server.d.ts.map +1 -1
  2. package/dist/commands/server.js +154 -13
  3. package/dist/commands/server.js.map +1 -1
  4. package/dist/config/ConfigDefaults.d.ts.map +1 -1
  5. package/dist/config/ConfigDefaults.js +16 -0
  6. package/dist/config/ConfigDefaults.js.map +1 -1
  7. package/dist/coordination/AnchoredAuthorization.d.ts +66 -0
  8. package/dist/coordination/AnchoredAuthorization.d.ts.map +1 -0
  9. package/dist/coordination/AnchoredAuthorization.js +71 -0
  10. package/dist/coordination/AnchoredAuthorization.js.map +1 -0
  11. package/dist/core/AutonomousSessions.d.ts +8 -0
  12. package/dist/core/AutonomousSessions.d.ts.map +1 -1
  13. package/dist/core/AutonomousSessions.js +34 -0
  14. package/dist/core/AutonomousSessions.js.map +1 -1
  15. package/dist/core/ExternalOperationGate.d.ts +12 -0
  16. package/dist/core/ExternalOperationGate.d.ts.map +1 -1
  17. package/dist/core/ExternalOperationGate.js +6 -0
  18. package/dist/core/ExternalOperationGate.js.map +1 -1
  19. package/dist/core/MeshRpc.d.ts +6 -1
  20. package/dist/core/MeshRpc.d.ts.map +1 -1
  21. package/dist/core/MeshRpc.js +7 -0
  22. package/dist/core/MeshRpc.js.map +1 -1
  23. package/dist/core/OwnershipReconciler.d.ts.map +1 -1
  24. package/dist/core/OwnershipReconciler.js +17 -0
  25. package/dist/core/OwnershipReconciler.js.map +1 -1
  26. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  27. package/dist/core/PostUpdateMigrator.js +9 -0
  28. package/dist/core/PostUpdateMigrator.js.map +1 -1
  29. package/dist/core/SessionDrainRunner.d.ts +116 -0
  30. package/dist/core/SessionDrainRunner.d.ts.map +1 -0
  31. package/dist/core/SessionDrainRunner.js +162 -0
  32. package/dist/core/SessionDrainRunner.js.map +1 -0
  33. package/dist/core/SessionOwnership.d.ts +22 -1
  34. package/dist/core/SessionOwnership.d.ts.map +1 -1
  35. package/dist/core/SessionOwnership.js +17 -1
  36. package/dist/core/SessionOwnership.js.map +1 -1
  37. package/dist/core/devGatedFeatures.d.ts.map +1 -1
  38. package/dist/core/devGatedFeatures.js +5 -0
  39. package/dist/core/devGatedFeatures.js.map +1 -1
  40. package/dist/core/seamlessnessConfig.d.ts +8 -1
  41. package/dist/core/seamlessnessConfig.d.ts.map +1 -1
  42. package/dist/core/seamlessnessConfig.js +8 -1
  43. package/dist/core/seamlessnessConfig.js.map +1 -1
  44. package/dist/core/types.d.ts +5 -0
  45. package/dist/core/types.d.ts.map +1 -1
  46. package/dist/core/types.js.map +1 -1
  47. package/dist/scaffold/templates.d.ts.map +1 -1
  48. package/dist/scaffold/templates.js +8 -0
  49. package/dist/scaffold/templates.js.map +1 -1
  50. package/dist/server/AgentServer.d.ts +8 -0
  51. package/dist/server/AgentServer.d.ts.map +1 -1
  52. package/dist/server/AgentServer.js +1 -0
  53. package/dist/server/AgentServer.js.map +1 -1
  54. package/dist/server/middleware.d.ts +14 -0
  55. package/dist/server/middleware.d.ts.map +1 -1
  56. package/dist/server/middleware.js +17 -0
  57. package/dist/server/middleware.js.map +1 -1
  58. package/dist/server/routes.d.ts +11 -0
  59. package/dist/server/routes.d.ts.map +1 -1
  60. package/dist/server/routes.js +206 -17
  61. package/dist/server/routes.js.map +1 -1
  62. package/dist/threadline/ContentClassifier.d.ts +15 -0
  63. package/dist/threadline/ContentClassifier.d.ts.map +1 -1
  64. package/dist/threadline/ContentClassifier.js +39 -0
  65. package/dist/threadline/ContentClassifier.js.map +1 -1
  66. package/dist/threadline/ConversationStore.d.ts +32 -0
  67. package/dist/threadline/ConversationStore.d.ts.map +1 -1
  68. package/dist/threadline/ConversationStore.js +74 -0
  69. package/dist/threadline/ConversationStore.js.map +1 -1
  70. package/dist/threadline/NegotiatorGate.d.ts +88 -0
  71. package/dist/threadline/NegotiatorGate.d.ts.map +1 -0
  72. package/dist/threadline/NegotiatorGate.js +148 -0
  73. package/dist/threadline/NegotiatorGate.js.map +1 -0
  74. package/dist/threadline/NegotiatorLease.d.ts +156 -0
  75. package/dist/threadline/NegotiatorLease.d.ts.map +1 -0
  76. package/dist/threadline/NegotiatorLease.js +202 -0
  77. package/dist/threadline/NegotiatorLease.js.map +1 -0
  78. package/dist/threadline/ThreadlineEndpoints.d.ts +2 -1
  79. package/dist/threadline/ThreadlineEndpoints.d.ts.map +1 -1
  80. package/dist/threadline/ThreadlineEndpoints.js +21 -1
  81. package/dist/threadline/ThreadlineEndpoints.js.map +1 -1
  82. package/dist/threadline/ThreadlineMCPServer.d.ts +8 -0
  83. package/dist/threadline/ThreadlineMCPServer.d.ts.map +1 -1
  84. package/dist/threadline/ThreadlineMCPServer.js +14 -1
  85. package/dist/threadline/ThreadlineMCPServer.js.map +1 -1
  86. package/dist/threadline/mcp-http-client.d.ts.map +1 -1
  87. package/dist/threadline/mcp-http-client.js +6 -0
  88. package/dist/threadline/mcp-http-client.js.map +1 -1
  89. package/dist/threadline/recordInboundAck.d.ts +48 -0
  90. package/dist/threadline/recordInboundAck.d.ts.map +1 -0
  91. package/dist/threadline/recordInboundAck.js +46 -0
  92. package/dist/threadline/recordInboundAck.js.map +1 -0
  93. package/package.json +1 -1
  94. package/src/data/builtin-manifest.json +64 -64
  95. package/src/scaffold/templates.ts +8 -0
  96. package/upgrades/1.3.508.md +83 -0
  97. package/upgrades/1.3.509.md +74 -0
  98. package/upgrades/side-effects/multi-machine-seamlessness-ws12b-drain.md +205 -0
  99. package/upgrades/side-effects/threadline-single-negotiator.md +106 -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;AAyG7D,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,CAgqYtE;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;AAw0CD,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"}
@@ -144,6 +144,7 @@ import { WarmSessionPool } from '../threadline/WarmSessionPool.js';
144
144
  import { resolveThreadlineMcpEntry } from '../threadline/mcpEntry.js';
145
145
  import { ThreadResumeMap } from '../threadline/ThreadResumeMap.js';
146
146
  import { ConversationStore } from '../threadline/ConversationStore.js';
147
+ import { recordInboundAck } from '../threadline/recordInboundAck.js';
147
148
  import { WarrantsReplyGate, evaluateAndRecordInbound } from '../threadline/WarrantsReplyGate.js';
148
149
  import { CollaborationSurfacer } from '../threadline/CollaborationSurfacer.js';
149
150
  import { ListenerSessionManager } from '../threadline/ListenerSessionManager.js';
@@ -384,6 +385,10 @@ let _inboundQueueStop = null;
384
385
  * (forward/spawn on another machine → must NOT also dispatch locally) from a
385
386
  * self placement. Set once in startServer()'s mesh block. */
386
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;
387
392
  /** WS1.1: read-only ownership lookup for the drain's spawn-boundary re-check
388
393
  * (the registry is constructed later in startServer's pool block). */
389
394
  let _ownershipReadForDrain = null;
@@ -395,6 +400,10 @@ let _resolveRouterUrl = null;
395
400
  /** Every OTHER active machine with a known URL — backs GET /sessions?scope=pool
396
401
  * (pool-wide session aggregation for the dashboard). Set in the same mesh block. */
397
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;
398
407
  let _listPoolMachines = null;
399
408
  /** Multi-Machine Session Pool §L4: per-topic placement pin store ("move this to <nickname>"). */
400
409
  let _topicPinStore = null;
@@ -10267,18 +10276,11 @@ export async function startServer(options) {
10267
10276
  // ack (a reply on the thread = processed-ack of our prior send) fires.
10268
10277
  // Same-machine local delivery is recorded separately in the
10269
10278
  // /messages/relay-agent route. Recording-only — never gates routing.
10270
- if (a2aDeliveryTracker) {
10271
- try {
10272
- a2aDeliveryTracker.recordInboundFrom(senderFingerprint, senderName ?? null);
10273
- if (msg.threadId)
10274
- a2aDeliveryTracker.recordAckByThread(msg.threadId);
10275
- }
10276
- catch (err) {
10277
- // @silent-fallback-ok: recording-only — A2A delivery/liveness tracking must
10278
- // never break inbound routing (the message was already accepted above). Logged.
10279
- console.warn(`[relay] A2A inbound record failed (non-fatal): ${err instanceof Error ? err.message : err}`);
10280
- }
10281
- }
10279
+ // Robustness Phase 1 (D-E): funnelled through the shared recordInboundAck
10280
+ // so every inbound-receive path records the ack via one helper. Here the
10281
+ // senderFingerprint is the peer's real routing fingerprint, so liveness
10282
+ // keys correctly without a thread-owner lookup. Recording-only.
10283
+ recordInboundAck({ a2aDeliveryTracker }, { threadId: msg.threadId, senderFingerprint, senderName: senderName ?? null });
10282
10284
  // Threadline → Telegram bridge: mirror inbound message into a per-thread
10283
10285
  // Telegram topic so the user has visibility into agent-to-agent
10284
10286
  // conversations. Relay-only — TelegramBridgeConfig owns the gate
@@ -11935,7 +11937,7 @@ export async function startServer(options) {
11935
11937
  // WS1.1 capability advertisement (spec invariant 5): a bounded
11936
11938
  // fixed-size summary, never an inventory. Reported live each
11937
11939
  // heartbeat so a queue going dark withdraws the capability.
11938
- seamlessnessFlags: { ws11DeliverReceive: !!_inboundQueue },
11940
+ seamlessnessFlags: { ws11DeliverReceive: !!_inboundQueue, ws12DrainReceive: !!_drainRunner },
11939
11941
  // Durable Inbound Message Queue §5.1: depth + oldest + tenure +
11940
11942
  // bounded top-K — the survivor's loss-SUSPECTED item, capped
11941
11943
  // re-placement arm, and supersede-dedupe key all read these.
@@ -12325,6 +12327,85 @@ export async function startServer(options) {
12325
12327
  });
12326
12328
  },
12327
12329
  });
12330
+ // ── WS1.2 owner-side drain runner (MULTI-MACHINE-SEAMLESSNESS-SPEC) ──
12331
+ // Constructed here because every dep lives in this scope. Presence IS
12332
+ // the heartbeat-advertised ws12DrainReceive capability. All I/O
12333
+ // injected; the runner itself is the unit-tested pure sequence.
12334
+ try {
12335
+ const drainMod = await import('../core/SessionDrainRunner.js');
12336
+ const autoMod = await import('../core/AutonomousSessions.js');
12337
+ let drainNonce = 0;
12338
+ _drainRunner = new drainMod.SessionDrainRunner({
12339
+ selfMachineId: meshSelfId,
12340
+ readOwnership: (sk) => ownReg.read(sk),
12341
+ cas: (action, c) => {
12342
+ const prev = ownReg.read(c.sessionKey)?.ownerMachineId;
12343
+ const r = ownReg.cas(action, c);
12344
+ // Journal pairing (§3.3): the drain's transfer/abort/claim CASes
12345
+ // record placement history like every other CAS site.
12346
+ emitPlacement(c.sessionKey, r, 'user-move', prev);
12347
+ return r;
12348
+ },
12349
+ suspendAutonomousRun: (topic, target) => autoMod.suspendAutonomousTopicForMove(config.stateDir, topic, target, coherenceJournal ?? undefined),
12350
+ sessionQuiet: (sk) => {
12351
+ const topicNum = Number(sk);
12352
+ if (!Number.isFinite(topicNum) || !telegram)
12353
+ return true; // nothing local to drain
12354
+ const tmux = telegram.getSessionForTopic(topicNum);
12355
+ if (!tmux || !sessionManager.isSessionAlive(tmux))
12356
+ return true;
12357
+ return !sessionManager.isSessionActivelyWorking(tmux);
12358
+ },
12359
+ // Emergency stop: the durable flag file freshly touched (within the
12360
+ // drain window's scale) — a stale flag from an old stop must not
12361
+ // permanently veto every future transfer.
12362
+ emergencyStopActive: () => {
12363
+ try {
12364
+ const flagAt = fs.statSync(path.join(config.projectDir, '.instar', 'autonomous-emergency-stop')).mtimeMs;
12365
+ return Date.now() - flagAt < 120_000;
12366
+ }
12367
+ catch { /* @silent-fallback-ok — no flag file = no emergency stop (the defined absent-state, not a degradation) */
12368
+ return false;
12369
+ }
12370
+ },
12371
+ terminateSession: async (sk, reason, opts) => {
12372
+ const topicNum = Number(sk);
12373
+ const tmux = Number.isFinite(topicNum) ? telegram?.getSessionForTopic(topicNum) : null;
12374
+ const rec = tmux ? state.listSessions().find((s) => s.tmuxSession === tmux) : undefined;
12375
+ if (!rec)
12376
+ return { terminated: false, skipped: 'no-local-session' };
12377
+ return sessionManager.terminateSession(rec.id, reason, {
12378
+ origin: 'autonomous',
12379
+ via: 'ws12-drain',
12380
+ bypassActiveProcessKeep: opts.force,
12381
+ workEvidence: opts.force ? ['active-build-or-autonomous-run'] : undefined,
12382
+ });
12383
+ },
12384
+ markInterrupted: (topic) => { autoMod.markAutonomousInterruptedMidTask(config.stateDir, topic); },
12385
+ notifyInterrupted: (topic, target, detail) => {
12386
+ const topicNum = Number(topic);
12387
+ if (!telegram || !Number.isFinite(topicNum))
12388
+ return;
12389
+ void telegram
12390
+ .sendToTopic(topicNum, `This conversation moved machines mid-task (${detail}). Work resumes on the new machine — the final turn before the move may be partial.`)
12391
+ .catch(() => { });
12392
+ },
12393
+ audit: (event) => {
12394
+ try {
12395
+ console.log(pc.dim(` [ws12-drain] ${JSON.stringify(event)}`));
12396
+ }
12397
+ catch { /* @silent-fallback-ok — observability only */ }
12398
+ },
12399
+ now: () => Date.now(),
12400
+ sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
12401
+ nonce: () => `${meshSelfId}:drain:${Date.now()}:${++drainNonce}`,
12402
+ });
12403
+ console.log(pc.dim(' [ws12-drain] owner-side drain runner wired (capability advertised)'));
12404
+ }
12405
+ catch (err) {
12406
+ // @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.
12407
+ console.log(pc.dim(` [ws12-drain] runner not wired: ${err instanceof Error ? err.message : String(err)}`));
12408
+ }
12328
12409
  // ── Secret-sync inbound handler (cross-machine secret distribution, spec Phase 4) ──
12329
12410
  // Dark by default; live on the dev agent via the developmentAgent gate. An inbound
12330
12411
  // `secret-share` command is decrypted to THIS machine's X25519 key and stored in the
@@ -12514,6 +12595,30 @@ export async function startServer(options) {
12514
12595
  transfer: ownAction,
12515
12596
  release: ownAction,
12516
12597
  deliverMessage: deliverMessageHandler,
12598
+ // WS1.2 owner-side drain (MULTI-MACHINE-SEAMLESSNESS-SPEC): the
12599
+ // router orders THIS machine — the topic's current owner — to
12600
+ // finish the live turn (bounded), close the local session, and
12601
+ // land the target's claim, releasing the queue's barrier exactly
12602
+ // at drain completion. RBAC is router-only ('drain-unauthorized');
12603
+ // the runner re-validates ownership + epoch at its CAS fence
12604
+ // regardless (reach ≠ authority). Answers 'drain disabled' until
12605
+ // the runner is constructed (pool dark / deps unavailable) — the
12606
+ // sender's capability gate means a doomed order is never sent to
12607
+ // a machine that advertises the flag honestly.
12608
+ drain: async (cmd) => {
12609
+ const c = cmd;
12610
+ if (!_drainRunner)
12611
+ return { ok: false, reason: 'drain disabled' };
12612
+ const outcome = await _drainRunner.run({
12613
+ sessionKey: c.session,
12614
+ target: c.target,
12615
+ senderObservedEpoch: c.ownershipEpoch,
12616
+ });
12617
+ return {
12618
+ ok: outcome.status === 'drained' || outcome.status === 'drained-interrupted',
12619
+ ...outcome,
12620
+ };
12621
+ },
12517
12622
  'secret-share': (cmd, sender) => _secretShareHandler
12518
12623
  ? _secretShareHandler.handle(cmd, sender)
12519
12624
  : { ok: false, reason: 'secret-sync disabled' },
@@ -12538,6 +12643,42 @@ export async function startServer(options) {
12538
12643
  });
12539
12644
  const peerUrl = (machineId) => meshIdMgr.getActiveMachines().find((m) => m.machineId === machineId)?.entry.lastKnownUrl ?? null;
12540
12645
  _meshSelfId = meshSelfId;
12646
+ // WS1.2 sender leg: signed drain order to a remote owner. Bounded by
12647
+ // the drain bound + slack so a clean drain (≤30s wait + close) can
12648
+ // complete within one call; every failure shape maps to an explicit
12649
+ // outcome — the transfer route NEVER hangs on this and degrades to
12650
+ // today's pin path on anything but a real abort.
12651
+ _sendDrain = async (ownerMachineId, sessionKey, target, ownershipEpoch) => {
12652
+ // Local-owner arm: the live swap topology (owner == holder == this
12653
+ // machine) drains via the SAME runner the mesh handler uses — one
12654
+ // code path, no HTTP round-trip to ourselves.
12655
+ if (ownerMachineId === meshSelfId) {
12656
+ if (!_drainRunner)
12657
+ return { ok: false, reason: 'drain disabled' };
12658
+ const o = await _drainRunner.run({ sessionKey, target, senderObservedEpoch: ownershipEpoch });
12659
+ return {
12660
+ ok: o.status === 'drained' || o.status === 'drained-interrupted',
12661
+ status: o.status,
12662
+ reason: o.detail,
12663
+ runSuspended: o.autonomousRunSuspended,
12664
+ };
12665
+ }
12666
+ const url = peerUrl(ownerMachineId);
12667
+ if (!url)
12668
+ return { ok: false, reason: 'no-peer-url' };
12669
+ try {
12670
+ const res = await meshClient.send({ machineId: ownerMachineId, url }, { type: 'drain', session: sessionKey, target, ownershipEpoch }, ownershipEpoch, { timeoutMs: 50_000 });
12671
+ if (res.ok) {
12672
+ const r = (res.result ?? {});
12673
+ return { ok: r.ok === true, status: typeof r.status === 'string' ? r.status : undefined, reason: typeof r.reason === 'string' ? r.reason : undefined };
12674
+ }
12675
+ return { ok: false, reason: res.reason ?? `status-${res.status}`, noHandler: res.status === 501 };
12676
+ }
12677
+ catch (err) {
12678
+ // @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.
12679
+ return { ok: false, reason: err instanceof Error ? err.message : String(err) };
12680
+ }
12681
+ };
12541
12682
  _resolveRouterUrl = () => {
12542
12683
  const h = coordinator.getSyncStatus().leaseHolder;
12543
12684
  return h && h !== meshSelfId ? peerUrl(h) : null;