instar 1.3.714 → 1.3.716

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 (92) hide show
  1. package/dist/commands/server.d.ts.map +1 -1
  2. package/dist/commands/server.js +599 -4
  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 +35 -0
  6. package/dist/config/ConfigDefaults.js.map +1 -1
  7. package/dist/core/CoherenceJournal.d.ts +1 -1
  8. package/dist/core/CoherenceJournal.d.ts.map +1 -1
  9. package/dist/core/CoherenceJournal.js +9 -2
  10. package/dist/core/CoherenceJournal.js.map +1 -1
  11. package/dist/core/FencedLease.d.ts +47 -1
  12. package/dist/core/FencedLease.d.ts.map +1 -1
  13. package/dist/core/FencedLease.js +58 -1
  14. package/dist/core/FencedLease.js.map +1 -1
  15. package/dist/core/LeaseCoordinator.d.ts +25 -1
  16. package/dist/core/LeaseCoordinator.d.ts.map +1 -1
  17. package/dist/core/LeaseCoordinator.js +66 -0
  18. package/dist/core/LeaseCoordinator.js.map +1 -1
  19. package/dist/core/LeaseHandbackReconciler.d.ts +212 -0
  20. package/dist/core/LeaseHandbackReconciler.d.ts.map +1 -0
  21. package/dist/core/LeaseHandbackReconciler.js +416 -0
  22. package/dist/core/LeaseHandbackReconciler.js.map +1 -0
  23. package/dist/core/MeshRpc.d.ts +6 -1
  24. package/dist/core/MeshRpc.d.ts.map +1 -1
  25. package/dist/core/MeshRpc.js +9 -0
  26. package/dist/core/MeshRpc.js.map +1 -1
  27. package/dist/core/MultiMachineCoordinator.d.ts +20 -0
  28. package/dist/core/MultiMachineCoordinator.d.ts.map +1 -1
  29. package/dist/core/MultiMachineCoordinator.js +30 -0
  30. package/dist/core/MultiMachineCoordinator.js.map +1 -1
  31. package/dist/core/OwnershipReconciler.d.ts +42 -2
  32. package/dist/core/OwnershipReconciler.d.ts.map +1 -1
  33. package/dist/core/OwnershipReconciler.js +100 -2
  34. package/dist/core/OwnershipReconciler.js.map +1 -1
  35. package/dist/core/PostUpdateMigrator.d.ts +1 -0
  36. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  37. package/dist/core/PostUpdateMigrator.js +31 -0
  38. package/dist/core/PostUpdateMigrator.js.map +1 -1
  39. package/dist/core/StaleOwnerReleaseEngine.d.ts +290 -0
  40. package/dist/core/StaleOwnerReleaseEngine.d.ts.map +1 -0
  41. package/dist/core/StaleOwnerReleaseEngine.js +645 -0
  42. package/dist/core/StaleOwnerReleaseEngine.js.map +1 -0
  43. package/dist/core/TopicClaimAnnotationStore.d.ts +121 -0
  44. package/dist/core/TopicClaimAnnotationStore.d.ts.map +1 -0
  45. package/dist/core/TopicClaimAnnotationStore.js +234 -0
  46. package/dist/core/TopicClaimAnnotationStore.js.map +1 -0
  47. package/dist/core/devGatedFeatures.d.ts.map +1 -1
  48. package/dist/core/devGatedFeatures.js +11 -0
  49. package/dist/core/devGatedFeatures.js.map +1 -1
  50. package/dist/core/handbackLatch.d.ts +28 -0
  51. package/dist/core/handbackLatch.d.ts.map +1 -0
  52. package/dist/core/handbackLatch.js +73 -0
  53. package/dist/core/handbackLatch.js.map +1 -0
  54. package/dist/core/ropeHealth.d.ts +26 -0
  55. package/dist/core/ropeHealth.d.ts.map +1 -0
  56. package/dist/core/ropeHealth.js +32 -0
  57. package/dist/core/ropeHealth.js.map +1 -0
  58. package/dist/core/types.d.ts +51 -0
  59. package/dist/core/types.d.ts.map +1 -1
  60. package/dist/core/types.js.map +1 -1
  61. package/dist/monitoring/InputClassifier.d.ts.map +1 -1
  62. package/dist/monitoring/InputClassifier.js +6 -1
  63. package/dist/monitoring/InputClassifier.js.map +1 -1
  64. package/dist/monitoring/StrandedTopicSentinel.d.ts +5 -0
  65. package/dist/monitoring/StrandedTopicSentinel.d.ts.map +1 -1
  66. package/dist/monitoring/StrandedTopicSentinel.js +8 -0
  67. package/dist/monitoring/StrandedTopicSentinel.js.map +1 -1
  68. package/dist/monitoring/guardManifest.d.ts.map +1 -1
  69. package/dist/monitoring/guardManifest.js +45 -0
  70. package/dist/monitoring/guardManifest.js.map +1 -1
  71. package/dist/monitoring/strandedTopicDecision.d.ts +11 -0
  72. package/dist/monitoring/strandedTopicDecision.d.ts.map +1 -1
  73. package/dist/monitoring/strandedTopicDecision.js +12 -1
  74. package/dist/monitoring/strandedTopicDecision.js.map +1 -1
  75. package/dist/scaffold/templates.d.ts.map +1 -1
  76. package/dist/scaffold/templates.js +2 -1
  77. package/dist/scaffold/templates.js.map +1 -1
  78. package/dist/server/AgentServer.d.ts +9 -0
  79. package/dist/server/AgentServer.d.ts.map +1 -1
  80. package/dist/server/AgentServer.js +2 -0
  81. package/dist/server/AgentServer.js.map +1 -1
  82. package/dist/server/routes.d.ts +9 -0
  83. package/dist/server/routes.d.ts.map +1 -1
  84. package/dist/server/routes.js +76 -0
  85. package/dist/server/routes.js.map +1 -1
  86. package/package.json +1 -1
  87. package/src/data/builtin-manifest.json +65 -65
  88. package/src/scaffold/templates.ts +2 -1
  89. package/upgrades/1.3.715.md +92 -0
  90. package/upgrades/1.3.716.md +32 -0
  91. package/upgrades/side-effects/input-classifier-unsure-contract.md +76 -0
  92. package/upgrades/side-effects/u4-stale-owner-release-lease-handback.md +87 -0
@@ -28,6 +28,7 @@ import { SingleInstanceLock, installReleaseHandlers } from '../core/SingleInstan
28
28
  import { resolveDevAgentGate, resolveStateSyncStores } from '../core/devAgentGate.js';
29
29
  import { shouldReleaseOnComplete, planClaimOnSpawn, ownershipNonce } from '../core/ownershipFollowsLiveWork.js';
30
30
  import { PrHandLease } from '../core/PrHandLease.js';
31
+ import { claimSuspensionExcludesPin } from '../core/TopicClaimAnnotationStore.js';
31
32
  import { parseProfileTrigger, platformMessageIdFrom } from '../core/topicProfileIngress.js';
32
33
  import { slugifyChannelName } from '../messaging/slack/sanitize.js';
33
34
  import { TopicProfileOrchestrator, resolvedToApplied, } from '../core/TopicProfileOrchestrator.js';
@@ -705,6 +706,45 @@ let _sessionRefresh = null;
705
706
  // the `/pool/reconciler` readout (status + per-topic explain) can reach it. Null until the
706
707
  // pool block constructs it (single-machine / dark → stays null → the route 503s).
707
708
  let _ownershipReconciler = null;
709
+ // U4.2 stale-owner release — the evidence-upgraded Case C engine, hoisted so the
710
+ // GET /pool/stale-owner-release route + the reconciler dep can reach it. Null
711
+ // until the pool block constructs it (single-machine / dark → the route 503s).
712
+ let _staleOwnerEngine = null;
713
+ // U4.4 lease hand-back — the F4 reconciler, hoisted so the lease pull tick hook
714
+ // (constructed early) and the /pool surfaces (constructed late) can reach it.
715
+ let _leaseHandbackReconciler = null;
716
+ // U4.4 — the /pool/lease-handback route surface (status + the R-r2-5 operator
717
+ // latch levers), assembled where the reconciler is constructed.
718
+ let _leaseHandbackCtx = null;
719
+ // U4.2 evidence-5 mirror freshness — stamped whenever a journal-sync exchange
720
+ // with a peer SUCCEEDS (an empty delta is still a successful sync). Null until
721
+ // the first success; a mirror never synced inside the window classifies
722
+ // AMBIGUITY (fail closed) in the evidence bar.
723
+ let _journalSyncLastOkMs = null;
724
+ // U4.2 evidence seams, assigned in the lease-transport block (which owns the
725
+ // identity manager + the authenticated dial machinery) and read late-bound by
726
+ // the engine. Null = the mesh substrate is absent → the engine fails closed.
727
+ let _staleOwnerAdvertSet = null;
728
+ // U4.2 §2.4/§2.3 — the merged claim-suspension view (engine-gated: empty while the
729
+ // feature is dark). Assigned at reconciler construction; consulted BOTH by the
730
+ // reconciler's effectivePins() and by the SessionReaper closeout's pin-conflict
731
+ // veto (a SUSPENDED pin is not "the reconciler bringing the topic back", so it
732
+ // must not veto the returned owner's post-claim teardown).
733
+ let _sorClaimSuspensionsRead = null;
734
+ let _staleOwnerProbeEndpoint = null;
735
+ let _staleOwnerSelfProof = null;
736
+ let _hasDurableLeaseAuthority = null;
737
+ // Attention seam shared by U4.2 (ambiguity escalation / P19 give-ups) and U4.4
738
+ // (deferral-ceiling / episode-cap notices); assigned where the telegram adapter
739
+ // exists. Items are episode-keyed and deduped by the attention store.
740
+ let _meshAttentionRaise = null;
741
+ // U4.4 offer transport (assigned where the MeshRpcClient exists). Null = no
742
+ // mesh client yet → sendOffer reports 'timeout' and the holder keeps holding.
743
+ // U4.4 — used consent-token keys on the RECEIVING side (single-use; the token
744
+ // TTL is 60s so a bounded in-memory set suffices) + the delivery canary seam.
745
+ const _handbackSeenTokens = new Set();
746
+ let _handbackCanary = null;
747
+ let _handbackSendOffer = null;
708
748
  // Subscription & Auth Standard P1.3 — quota-aware account-swap scheduler. Null
709
749
  // until wired (requires SessionRefresh + the subscription pool).
710
750
  let _quotaAwareScheduler = null;
@@ -3396,6 +3436,9 @@ export async function startServer(options) {
3396
3436
  stateDir: config.stateDir,
3397
3437
  multiMachine: config.multiMachine,
3398
3438
  developmentAgent: config.developmentAgent,
3439
+ // U4.4 — the hand-back reconciler's observation rides the existing lease
3440
+ // pull tick (late-bound: the reconciler is constructed after boot wiring).
3441
+ onLeasePullTick: () => _leaseHandbackReconciler?.observe(),
3399
3442
  });
3400
3443
  const machineRole = coordinator.start();
3401
3444
  if (coordinator.enabled) {
@@ -3613,6 +3656,32 @@ export async function startServer(options) {
3613
3656
  // not range-validated; with none on, the foundation is inert.
3614
3657
  const stateSync = assertStateSyncInvariants(config.multiMachine);
3615
3658
  void stateSync; // foundation knobs are consumed by the store PRs (WS2.1+)
3659
+ // U4.2 (§2.3 TTL-ordering invariant, R-r2-3) — "expired implies self-fenced
3660
+ // by construction" holds ONLY when deathEvidenceMs > selfFenceTtlMs + one
3661
+ // reconciler tick + clock-skew slack. A violating combination is REJECTED at
3662
+ // startup (the reject-nonsensical-combinations pattern), never degraded
3663
+ // silently. U4.4 (§5 enable chokepoint) — dryRun:false with pollFollowsLease
3664
+ // still dry-run is REFUSED loudly (a lease/ingress split).
3665
+ {
3666
+ const { validateStaleOwnerReleaseInvariants, DEFAULT_STALE_OWNER_RELEASE_CONFIG } = await import('../core/StaleOwnerReleaseEngine.js');
3667
+ const sorCfg = config.multiMachine?.sessionPool?.staleOwnerRelease ?? {};
3668
+ const tickMs = Math.max(5000, config.multiMachine?.seamlessness?.ws13TickMs ?? 30000);
3669
+ const skewSlackMs = 2 * (config.multiMachine?.sessionPool?.maxExpectedNtpDriftMs ?? 250);
3670
+ const sorErr = validateStaleOwnerReleaseInvariants({
3671
+ deathEvidenceMs: sorCfg.deathEvidenceMs ?? DEFAULT_STALE_OWNER_RELEASE_CONFIG.deathEvidenceMs,
3672
+ selfFenceTtlMs: sorCfg.selfFenceTtlMs ?? DEFAULT_STALE_OWNER_RELEASE_CONFIG.selfFenceTtlMs,
3673
+ }, tickMs, skewSlackMs);
3674
+ if (sorErr)
3675
+ throw new Error(sorErr);
3676
+ const { validateHandbackEnableChokepoint } = await import('../core/LeaseHandbackReconciler.js');
3677
+ const hbCfg = config.multiMachine?.leaseSelfHeal?.preferredCaptainHandback ?? {};
3678
+ const pollLive = resolveDevAgentGate(config.multiMachine?.pollFollowsLease?.enabled, config) &&
3679
+ config.multiMachine?.pollFollowsLease?.dryRun === false;
3680
+ const hbErr = validateHandbackEnableChokepoint({ enabled: hbCfg.enabled === true, dryRun: hbCfg.dryRun !== false }, pollLive,
3681
+ /* hasPollerSplit — conservative: any instar install runs the lifeline poller split */ true);
3682
+ if (hbErr)
3683
+ throw new Error(hbErr);
3684
+ }
3616
3685
  // The replicated-kind registry (Component 2). Ships EMPTY in this step — the
3617
3686
  // first concrete store (WS2.1) registers its kind onto it. Constructed here so
3618
3687
  // the substrate is wired and the per-store stateSyncReceive advert can be
@@ -3734,6 +3803,15 @@ export async function startServer(options) {
3734
3803
  // the store-enable flag (ws13PinReplicate), so a single-machine / dark agent is a no-op.
3735
3804
  const { TOPIC_PIN_KIND_REGISTRATION } = await import('../core/TopicPinReplicatedStore.js');
3736
3805
  replicatedKindRegistry.register(TOPIC_PIN_KIND_REGISTRATION);
3806
+ // U4.2 (docs/specs/u4-2-stale-owner-release.md §2.4, R-r3-1) — register the
3807
+ // `topic-claim-annotation` kind (claim suspension + per-topic claim budget +
3808
+ // declined-demote pins; epoch-INDEPENDENT, generic-envelope-validated). A new
3809
+ // registered KIND is additive — pre-U4.2 peers never register it and simply
3810
+ // never sync it (no unknown-FIELD surface on any stream they consume).
3811
+ // Registration is INERT; emission stays gated behind the staleOwnerRelease
3812
+ // feature gate below.
3813
+ const { TOPIC_CLAIM_ANNOTATION_KIND_REGISTRATION } = await import('../core/TopicClaimAnnotationStore.js');
3814
+ replicatedKindRegistry.register(TOPIC_CLAIM_ANNOTATION_KIND_REGISTRATION);
3737
3815
  // ── WS2 SEND-SIDE wiring (docs/specs/WS2-SEND-SIDE-EMISSION-SPEC.md). The
3738
3816
  // substrate above ships the registry + receive/serve machinery + advert; THIS
3739
3817
  // wires the SEND half that was deferred ("the journal-backed emitter is attached
@@ -3849,13 +3927,20 @@ export async function startServer(options) {
3849
3927
  // rest of WS1.3). When OFF the entry is absent ⇒ the emitter's enabled-check fails ⇒ no pin
3850
3928
  // record ever crosses (a dark/single-machine agent is a strict no-op).
3851
3929
  const _pinReplicateEnabled = resolveDevAgentGate(config.multiMachine?.seamlessness?.ws13PinReplicate, config);
3930
+ // U4.2 — the `topicClaimAnnotations` store gates on the staleOwnerRelease
3931
+ // feature (dev-live-in-dryRun / fleet-dark; `enabled` omitted from defaults).
3932
+ // Emission additionally requires the annotation writes themselves (claim /
3933
+ // decline transitions), so a dark agent never emits the kind.
3934
+ const _staleOwnerReleaseResolved = resolveDevAgentGate(config.multiMachine?.sessionPool?.staleOwnerRelease?.enabled, config);
3852
3935
  const _stateSyncStoresResolved = (() => {
3853
3936
  let s = _stateSyncStoresBase ?? {};
3854
3937
  if (_accountFollowMeEnabled)
3855
3938
  s = { ...s, subscriptionAccountMeta: { enabled: true } };
3856
3939
  if (_pinReplicateEnabled)
3857
3940
  s = { ...s, topicPins: { enabled: true } };
3858
- return _accountFollowMeEnabled || _pinReplicateEnabled || _stateSyncStoresBase ? s : _stateSyncStoresBase;
3941
+ if (_staleOwnerReleaseResolved)
3942
+ s = { ...s, topicClaimAnnotations: { enabled: true } };
3943
+ return _accountFollowMeEnabled || _pinReplicateEnabled || _staleOwnerReleaseResolved || _stateSyncStoresBase ? s : _stateSyncStoresBase;
3859
3944
  })();
3860
3945
  // WS2 send-side: the generic journal-backed record emitter (the concrete emitter
3861
3946
  // the per-store managers' emit hooks call). Needs the journal sink, the HLC clock,
@@ -4389,6 +4474,215 @@ export async function startServer(options) {
4389
4474
  });
4390
4475
  coordinator.attachLeaseCoordinator(leaseCoordinator);
4391
4476
  leaseCoordinatorRef = leaseCoordinator;
4477
+ // ── U4.2 evidence seams (docs/specs/u4-2-stale-owner-release.md §2.2) ──
4478
+ // Advert-set provenance (R-r2-5b): endpoints recorded ONLY by the
4479
+ // PeerEndpointRecorder out of the signed lease RPC (bound to the
4480
+ // cryptographically-verified sender), read back from the identity
4481
+ // registry's per-peer entry. The git registry's lastKnownUrl is NOT
4482
+ // acceptable disproof input and is deliberately not consulted here.
4483
+ _staleOwnerAdvertSet = (machineId) => {
4484
+ const eps = (idMgr.getMachineEndpoints(machineId) ?? []);
4485
+ // Freshness bound: the peer must still be a registered, non-revoked
4486
+ // machine with a durable heartbeat newer than the staleness ceiling
4487
+ // (an ancient advert set — the owner may have moved networks — is
4488
+ // ambiguity, never disproof).
4489
+ let fresh = false;
4490
+ try {
4491
+ const reg = idMgr.loadRegistry();
4492
+ const entry = (reg.machines ?? {})[machineId];
4493
+ const lastSeenMs = entry?.lastSeen ? Date.parse(entry.lastSeen) : NaN;
4494
+ fresh = !!entry && !entry.revokedAt && Number.isFinite(lastSeenMs) && Date.now() - lastSeenMs < 7 * 24 * 3_600_000;
4495
+ }
4496
+ catch {
4497
+ fresh = false; /* @silent-fallback-ok — an unreadable registry reads as not-fresh ⇒ the evidence bar classifies AMBIGUITY (fail closed), never disproof */
4498
+ }
4499
+ return { endpoints: eps.filter((e) => typeof e?.url === 'string' && typeof e?.kind === 'string'), fresh };
4500
+ };
4501
+ // Authenticated signed-handshake probe of ONE advertised transport: an
4502
+ // identity-verified /api/lease/pull round-trip (the same dial the lease
4503
+ // wire uses; verifyLeaseAckIdentity binds the responder).
4504
+ _staleOwnerProbeEndpoint = async (machineId, endpoint, _timeoutMs) => {
4505
+ if (!leaseTransport?.pullPeer)
4506
+ return false;
4507
+ try {
4508
+ const lease = await leaseTransport.pullPeer({
4509
+ machineId,
4510
+ url: endpoint.url,
4511
+ endpoints: [endpoint],
4512
+ publicKeyPem: idMgr.getSigningPublicKeyPem(machineId),
4513
+ meshAckCapable: !!idMgr.getSigningPublicKeyPem(machineId),
4514
+ });
4515
+ return lease !== null;
4516
+ }
4517
+ catch {
4518
+ return false; /* @silent-fallback-ok — an errored probe is unreachability EVIDENCE for this endpoint, consumed by a fail-closed evidence bar */
4519
+ }
4520
+ };
4521
+ // Claimant self-connectivity proof (evidence 4): an authenticated probe
4522
+ // of a THIRD peer; 2-machine case → verified reach of the durable lease
4523
+ // authority (git). A claimer with a broken NIC must never claim.
4524
+ _hasDurableLeaseAuthority = () => !!gitSyncRef;
4525
+ _staleOwnerSelfProof = async () => {
4526
+ try {
4527
+ const reg = idMgr.loadRegistry();
4528
+ const peers = Object.entries(reg.machines ?? {}).filter(([id, e]) => id !== selfMachineId && !e.revokedAt);
4529
+ if (peers.length >= 2 && leaseTransport?.pullPeer) {
4530
+ // Probe any OTHER peer (the engine excludes the suspect owner by
4531
+ // probing it separately; reaching ANY peer proves our egress).
4532
+ for (const [id, e] of peers) {
4533
+ const entry = e;
4534
+ const url = entry.lastKnownUrl ?? entry.endpoints?.[0]?.url;
4535
+ if (!url)
4536
+ continue;
4537
+ const lease = await leaseTransport.pullPeer({
4538
+ machineId: id,
4539
+ url,
4540
+ endpoints: entry.endpoints,
4541
+ publicKeyPem: idMgr.getSigningPublicKeyPem(id),
4542
+ meshAckCapable: (entry.endpoints?.length ?? 0) > 0 && !!idMgr.getSigningPublicKeyPem(id),
4543
+ });
4544
+ if (lease !== null)
4545
+ return true;
4546
+ }
4547
+ return false;
4548
+ }
4549
+ // 2-machine mesh: verified reach of the durable lease authority.
4550
+ if (gitSyncRef) {
4551
+ const read = leaseStore.read();
4552
+ return read !== null && read !== undefined;
4553
+ }
4554
+ return false; // 2-machine git-less: no self-proof source (claim path disabled upstream)
4555
+ }
4556
+ catch {
4557
+ return false; /* @silent-fallback-ok — a failed self-proof means WE may be the partitioned one; the evidence bar refuses the claim (fail closed) */
4558
+ }
4559
+ };
4560
+ // ── U4.4 lease hand-back reconciler (docs/specs/u4-4-lease-handback.md) ──
4561
+ // Constructed here (the lease block owns every authority input); its
4562
+ // observation rides the coordinator's lease pull tick via the
4563
+ // onLeasePullTick hook wired at coordinator construction. Ships
4564
+ // HARD-DARK (enabled:false + dryRun:true — DARK_GATE_EXCLUSIONS).
4565
+ {
4566
+ const { LeaseHandbackReconciler, DEFAULT_LEASE_HANDBACK_CONFIG, HANDBACK_CONSENT_TTL_MS } = await import('../core/LeaseHandbackReconciler.js');
4567
+ void HANDBACK_CONSENT_TTL_MS;
4568
+ const { readHandbackLatchUntilMs } = await import('../core/handbackLatch.js');
4569
+ const { ropeReachableOnAnyRope } = await import('../core/ropeHealth.js');
4570
+ const { getFeatureMetricsRecorder } = await import('../core/CircuitBreakingIntelligenceProvider.js');
4571
+ const hbCfgRead = () => {
4572
+ const c = config.multiMachine?.leaseSelfHeal?.preferredCaptainHandback ?? {};
4573
+ return {
4574
+ enabled: c.enabled === true,
4575
+ dryRun: c.dryRun !== false,
4576
+ healthWindowMs: c.healthWindowMs ?? DEFAULT_LEASE_HANDBACK_CONFIG.healthWindowMs,
4577
+ deferralCeilingMs: c.deferralCeilingMs ?? DEFAULT_LEASE_HANDBACK_CONFIG.deferralCeilingMs,
4578
+ operatorLatchMs: c.operatorLatchMs ?? DEFAULT_LEASE_HANDBACK_CONFIG.operatorLatchMs,
4579
+ maxPerWindow: c.maxPerWindow ?? DEFAULT_LEASE_HANDBACK_CONFIG.maxPerWindow,
4580
+ windowMs: c.windowMs ?? DEFAULT_LEASE_HANDBACK_CONFIG.windowMs,
4581
+ };
4582
+ };
4583
+ _leaseHandbackReconciler = new LeaseHandbackReconciler({
4584
+ config: hbCfgRead,
4585
+ selfMachineId: () => selfMachineId,
4586
+ // F4's ONE authority — the SAME config field shouldDeferToPreferred
4587
+ // reads (multiMachine.leaseSelfHeal.preferredAwakeMachineId).
4588
+ preferredAwakeMachineId: () => config.multiMachine?.leaseSelfHeal?.preferredAwakeMachineId ?? null,
4589
+ holdsLease: () => coordinator.holdsLease(),
4590
+ currentEpoch: () => leaseCoordinator.currentEpoch(),
4591
+ preferredHealth: (machineId) => {
4592
+ const cap = (() => {
4593
+ try {
4594
+ return machinePoolRegistry?.getCapacities().find((c) => c.machineId === machineId);
4595
+ }
4596
+ catch {
4597
+ return undefined; /* @silent-fallback-ok — no pool view reads as not-healthy → defer (the safe direction) */
4598
+ }
4599
+ })();
4600
+ return {
4601
+ heartbeatFresh: cap?.online === true,
4602
+ // U4.3 rope-health snapshot seam (R-r2-7): absent record/provider
4603
+ // reads NOT-healthy → defer. U4.3 registers the real provider.
4604
+ ropeReachable: ropeReachableOnAnyRope(machineId),
4605
+ leaseEligible: !!cap,
4606
+ quotaOk: cap?.quotaState?.blocked !== true,
4607
+ };
4608
+ },
4609
+ cleanBoundary: () => {
4610
+ const counts = (() => {
4611
+ try {
4612
+ return _inboundQueue?.snapshot().counts;
4613
+ }
4614
+ catch {
4615
+ return undefined; /* @silent-fallback-ok — an unreadable queue reads as quiet=false via queuedInbound 1 below */
4616
+ }
4617
+ })();
4618
+ const queuedInbound = counts ? (counts.queued ?? 0) + (counts.claimed ?? 0) : 0;
4619
+ let msSinceLastIngress = null;
4620
+ try {
4621
+ const st = fs.statSync(path.join(config.stateDir, 'state', 'serve-progress.json'));
4622
+ msSinceLastIngress = Date.now() - st.mtimeMs;
4623
+ }
4624
+ catch {
4625
+ msSinceLastIngress = null; /* @silent-fallback-ok — no serve-progress signal = no recent-ingress evidence; the other two boundary signals still gate */
4626
+ }
4627
+ return {
4628
+ inFlightForwards: (currentInboundByTopic?.size ?? 0) > 0,
4629
+ queuedInbound,
4630
+ msSinceLastIngress,
4631
+ };
4632
+ },
4633
+ kickInboundDrain: () => {
4634
+ // One event-triggered drain pass (the existing durable inbound-queue
4635
+ // semantics). The step-down stays deferred until counts read 0.
4636
+ try {
4637
+ void _inboundQueue?.onMachineOnline();
4638
+ }
4639
+ catch { /* @silent-fallback-ok — drain kick is best-effort; the boundary re-checks next tick */ }
4640
+ },
4641
+ splitBrainActive: () => {
4642
+ try {
4643
+ return coordinator.getSyncStatus().splitBrainState !== 'clear';
4644
+ }
4645
+ catch {
4646
+ return true; /* @silent-fallback-ok — an unreadable sync status reads as split-brain-active → suppress (the safe direction) */
4647
+ }
4648
+ },
4649
+ churnLatched: () => coordinator.churnBreakerLatched(),
4650
+ // Hand-backs COUNT as flips: the step-down role transition feeds the
4651
+ // breaker via the coordinator's reconcileRoleToLease recordFlip path
4652
+ // (a second explicit record here would double-count one hand-back).
4653
+ recordChurnFlip: () => { },
4654
+ operatorLatchUntilMs: () => readHandbackLatchUntilMs(config.stateDir),
4655
+ mintConsentToken: (target, ttlMs) => leaseCoordinator.mintHandbackConsent(target, ttlMs),
4656
+ sendOffer: async (target, offer) => {
4657
+ if (!_handbackSendOffer)
4658
+ return 'timeout';
4659
+ return _handbackSendOffer(target, offer);
4660
+ },
4661
+ metric: (event) => {
4662
+ try {
4663
+ getFeatureMetricsRecorder()?.record({ feature: 'lease-handback', kind: 'event', outcome: event === 'failure' ? 'error' : 'fired', verdictId: event });
4664
+ }
4665
+ catch { /* metrics never gate */ }
4666
+ },
4667
+ notify: (key, title, body) => {
4668
+ try {
4669
+ _meshAttentionRaise?.({ id: key, title, body, priority: 'medium', sourceContext: 'lease-handback' });
4670
+ }
4671
+ catch { /* notice is best-effort */ }
4672
+ },
4673
+ logger: (m) => console.log(pc.dim(` ${m}`)),
4674
+ });
4675
+ // The /pool/lease-handback route surface (status + the R-r2-5 operator
4676
+ // latch levers). latchWrite is the flip-action's POST step — the
4677
+ // configured operatorLatchMs is the TTL (default 24h).
4678
+ const { writeHandbackLatch, clearHandbackLatch, readHandbackLatchRecord } = await import('../core/handbackLatch.js');
4679
+ _leaseHandbackCtx = {
4680
+ status: () => _leaseHandbackReconciler.status(),
4681
+ latchWrite: (reason) => writeHandbackLatch(config.stateDir, hbCfgRead().operatorLatchMs, reason),
4682
+ latchClear: () => clearHandbackLatch(config.stateDir),
4683
+ latchRecord: () => readHandbackLatchRecord(config.stateDir),
4684
+ };
4685
+ }
4392
4686
  // G3 — lease-gated spawn: wire the "do I hold the lease?" accessor so the
4393
4687
  // spawn gate can fire. Setting this non-null also flips the gate out of its
4394
4688
  // single-machine no-op (it now consults the real lease). The flag accessor
@@ -15068,11 +15362,24 @@ export async function startServer(options) {
15068
15362
  // WS1.3: pin-conflict do-not-act — a pin naming THIS machine while the
15069
15363
  // owner is elsewhere means the reconciler is bringing the topic back;
15070
15364
  // the closeout holds instead of attacking the session the pin wants here.
15365
+ // U4.2 §2.3 exception: a pin SUSPENDED by a live stale-owner claim
15366
+ // annotation is NOT being brought back (effectivePins excludes it), so
15367
+ // it must not veto the returned owner's teardown — the same comparison
15368
+ // authority the reconciler uses (claimSuspensionExcludesPin). Feature
15369
+ // dark ⇒ the suspension view is empty ⇒ byte-identical veto behavior.
15071
15370
  topicPinnedHere: (topicId) => {
15072
15371
  try {
15073
15372
  const self = _meshSelfId;
15074
15373
  const pin = _topicPinStore?.get(String(topicId));
15075
- return !!self && !!pin && pin.pinned && pin.preferredMachine === self;
15374
+ if (!self || !pin || !pin.pinned || pin.preferredMachine !== self)
15375
+ return false;
15376
+ const suspension = _sorClaimSuspensionsRead?.().get(Number(topicId));
15377
+ if (suspension) {
15378
+ const pinHlc = pin.hlc ?? { physical: Date.parse(pin.updatedAt) || 0, logical: 0, node: self };
15379
+ if (claimSuspensionExcludesPin(pinHlc, suspension))
15380
+ return false;
15381
+ }
15382
+ return true;
15076
15383
  }
15077
15384
  catch {
15078
15385
  return false; /* @silent-fallback-ok — no pin signal → hold not applied, closeout behaves as before */
@@ -16253,6 +16560,7 @@ export async function startServer(options) {
16253
16560
  const { mergeUnionToPins, compareHlc, TOPIC_PIN_RECORD_KIND } = await import('../core/TopicPinReplicatedStore.js');
16254
16561
  const { CoherenceJournalReader: PinAdvReader } = await import('../core/CoherenceJournalReader.js');
16255
16562
  const pinAdvisoryReader = new PinAdvReader({ stateDir: config.stateDir });
16563
+ const { mergeUnionToClaimAnnotations, TOPIC_CLAIM_ANNOTATION_KIND, TOPIC_CLAIM_ANNOTATION_STORE_KEY, buildClaimAnnotationPut, deriveClaimAnnotationRecordKey } = await import('../core/TopicClaimAnnotationStore.js');
16256
16564
  const reconciler = new OwnershipReconciler({
16257
16565
  // DEV-AGENT DARK GATE (operator directive 2026-06-13, topic 13481):
16258
16566
  // read ws13Reconcile through resolveDevAgentGate so the reconcile loop
@@ -16286,16 +16594,37 @@ export async function startServer(options) {
16286
16594
  ownership: ownReg,
16287
16595
  machines: () => {
16288
16596
  try {
16597
+ // U4.2 R-r2-5a (named prerequisite fix): Case C's staleness feed is the
16598
+ // ROUTER/OBSERVER-stamped arrival time (routerReceivedAt), NEVER the
16599
+ // machine's self-reported wall clock — MachinePoolRegistry's own §L2
16600
+ // header forbids the self-reported input ("a fast-clocked machine must
16601
+ // not appear fresher than it is").
16289
16602
  return (machinePoolRegistry?.getCapacities() ?? []).map((c) => ({
16290
16603
  machineId: c.machineId,
16291
16604
  online: !!c.online,
16292
- lastSeenMs: c.selfReportedLastSeen ? Date.parse(c.selfReportedLastSeen) || 0 : 0,
16605
+ lastSeenMs: c.routerReceivedAt ? Date.parse(c.routerReceivedAt) || 0 : 0,
16293
16606
  }));
16294
16607
  }
16295
16608
  catch {
16296
16609
  return []; /* @silent-fallback-ok — no pool view → module's single-machine strict no-op */
16297
16610
  }
16298
16611
  },
16612
+ staleOwnerEngine: () => _staleOwnerEngine,
16613
+ claimSuspensions: (_sorClaimSuspensionsRead = () => {
16614
+ try {
16615
+ if (!_staleOwnerEngine?.isActive())
16616
+ return new Map();
16617
+ const res = pinAdvisoryReader.query({ kind: TOPIC_CLAIM_ANNOTATION_KIND, limit: 2000 });
16618
+ const merged = mergeUnionToClaimAnnotations(res.entries.map((e) => ({ data: e.data, origin: typeof e.data.origin === 'string' ? e.data.origin : '' })));
16619
+ const out = new Map();
16620
+ for (const [topic, a] of merged)
16621
+ out.set(topic, { suspended: a.suspended, hlc: a.hlc });
16622
+ return out;
16623
+ }
16624
+ catch {
16625
+ return new Map(); /* @silent-fallback-ok — an unreadable annotation view reads as no suspensions this tick; the next tick retries */
16626
+ }
16627
+ }),
16299
16628
  isTopicBusy: (sessionKey) => {
16300
16629
  try {
16301
16630
  // Conservative safe-point signal: an inbound for this topic is
@@ -16334,6 +16663,143 @@ export async function startServer(options) {
16334
16663
  logger: (m) => console.log(pc.dim(` ${m}`)),
16335
16664
  });
16336
16665
  _ownershipReconciler = reconciler; // hoist for the /pool/reconciler readout
16666
+ // ── U4.2 stale-owner release engine (docs/specs/u4-2-stale-owner-release.md) ──
16667
+ // Case C's evidence upgrade, invoked FROM reconciler.tick() (one actor;
16668
+ // claims funnel through reconciler.actStaleOwnerForceClaim). Dev-gate:
16669
+ // dev-live-in-dryRun / dark fleet; subordinate to sessionPool live AND
16670
+ // ≥2 machines (the engine's own gates).
16671
+ {
16672
+ const { StaleOwnerReleaseEngine, DEFAULT_STALE_OWNER_RELEASE_CONFIG } = await import('../core/StaleOwnerReleaseEngine.js');
16673
+ const { MachineHeartbeat: SorHeartbeatReader } = await import('../core/MachineHeartbeat.js');
16674
+ const sorHeartbeatReader = new SorHeartbeatReader({ stateDir: config.stateDir, machineId: 'stale-owner-release-reader' });
16675
+ const sorCfgRead = () => {
16676
+ const c = config.multiMachine?.sessionPool?.staleOwnerRelease ?? {};
16677
+ const poolLive = config.multiMachine?.sessionPool?.enabled === true &&
16678
+ (config.multiMachine?.sessionPool?.stage ?? 'dark') !== 'dark';
16679
+ return {
16680
+ enabled: resolveDevAgentGate(c.enabled, config) && poolLive,
16681
+ dryRun: c.dryRun !== false,
16682
+ deathEvidenceMs: c.deathEvidenceMs ?? DEFAULT_STALE_OWNER_RELEASE_CONFIG.deathEvidenceMs,
16683
+ probeTimeoutMs: c.probeTimeoutMs ?? DEFAULT_STALE_OWNER_RELEASE_CONFIG.probeTimeoutMs,
16684
+ ambiguityCeilingMultiple: c.ambiguityCeilingMultiple ?? DEFAULT_STALE_OWNER_RELEASE_CONFIG.ambiguityCeilingMultiple,
16685
+ maxClaimsPerTick: c.maxClaimsPerTick ?? DEFAULT_STALE_OWNER_RELEASE_CONFIG.maxClaimsPerTick,
16686
+ bootstrapNonObservationMultiple: c.bootstrapNonObservationMultiple ?? DEFAULT_STALE_OWNER_RELEASE_CONFIG.bootstrapNonObservationMultiple,
16687
+ selfFenceTtlMs: c.selfFenceTtlMs ?? DEFAULT_STALE_OWNER_RELEASE_CONFIG.selfFenceTtlMs,
16688
+ };
16689
+ };
16690
+ const sorMachines = () => {
16691
+ try {
16692
+ return (machinePoolRegistry?.getCapacities() ?? []).map((c) => ({
16693
+ machineId: c.machineId,
16694
+ online: !!c.online,
16695
+ // R-r2-5a: OBSERVER-stamped arrival time, never self-reported.
16696
+ observerLastSeenMs: c.routerReceivedAt ? Date.parse(c.routerReceivedAt) || 0 : 0,
16697
+ }));
16698
+ }
16699
+ catch {
16700
+ return []; /* @silent-fallback-ok — no pool view → the engine's single-machine strict no-op */
16701
+ }
16702
+ };
16703
+ const sorReadAnnotations = () => {
16704
+ const res = pinAdvisoryReader.query({ kind: TOPIC_CLAIM_ANNOTATION_KIND, limit: 2000 });
16705
+ return mergeUnionToClaimAnnotations(res.entries.map((e) => ({ data: e.data, origin: typeof e.data.origin === 'string' ? e.data.origin : '' })));
16706
+ };
16707
+ const sorTracePath = path.join(config.stateDir, '..', 'logs', 'stale-owner-release.jsonl');
16708
+ _staleOwnerEngine = new StaleOwnerReleaseEngine({
16709
+ enabled: () => sorCfgRead().enabled,
16710
+ dryRun: () => sorCfgRead().dryRun,
16711
+ config: sorCfgRead,
16712
+ selfMachineId: () => _meshSelfId,
16713
+ machines: sorMachines,
16714
+ holdsLease: () => {
16715
+ try {
16716
+ return coordinator.holdsLease();
16717
+ }
16718
+ catch {
16719
+ return false; /* @silent-fallback-ok — an unreadable lease reads as not-holder → the claim arbiter refuses (fail closed) */
16720
+ }
16721
+ },
16722
+ listOwnershipRecords: () => {
16723
+ try {
16724
+ return ownReg.all();
16725
+ }
16726
+ catch {
16727
+ return []; /* @silent-fallback-ok — no records → nothing to evaluate this tick */
16728
+ }
16729
+ },
16730
+ durableLastKnownHeartbeatMs: (machineId) => {
16731
+ try {
16732
+ const rec = sorHeartbeatReader.read(machineId);
16733
+ const t = rec ? Date.parse(rec.lastHeartbeatAt) : NaN;
16734
+ return Number.isFinite(t) ? t : null;
16735
+ }
16736
+ catch {
16737
+ return null; /* @silent-fallback-ok — no durable heartbeat = null; the bootstrap rule treats it as its tie-breaker's long-dead arm */
16738
+ }
16739
+ },
16740
+ advertSet: (machineId) => (_staleOwnerAdvertSet ? _staleOwnerAdvertSet(machineId) : { endpoints: [], fresh: false }),
16741
+ probeEndpoint: (machineId, endpoint, timeoutMs) => _staleOwnerProbeEndpoint ? _staleOwnerProbeEndpoint(machineId, endpoint, timeoutMs) : Promise.resolve(false),
16742
+ selfConnectivityProof: () => (_staleOwnerSelfProof ? _staleOwnerSelfProof() : Promise.resolve(false)),
16743
+ hasDurableLeaseAuthority: () => (_hasDurableLeaseAuthority ? _hasDurableLeaseAuthority() : false),
16744
+ evidenceMirror: () => ({
16745
+ lastSyncOkMs: _journalSyncLastOkMs,
16746
+ lastOwnerSideEffectMs: (machineId) => {
16747
+ try {
16748
+ let newest = null;
16749
+ for (const kind of ['topic-placement', 'session-lifecycle']) {
16750
+ const res = pinAdvisoryReader.query({ kind, machine: machineId, limit: 1 });
16751
+ const st = res.streams[machineId];
16752
+ const t = st?.lastTs ? Date.parse(st.lastTs) : NaN;
16753
+ if (Number.isFinite(t))
16754
+ newest = newest === null ? t : Math.max(newest, t);
16755
+ }
16756
+ return newest;
16757
+ }
16758
+ catch {
16759
+ return null; /* @silent-fallback-ok — an unreadable mirror yields null; combined with the sync-freshness gate this stays fail-closed */
16760
+ }
16761
+ },
16762
+ }),
16763
+ claimAnnotations: () => {
16764
+ try {
16765
+ return sorReadAnnotations();
16766
+ }
16767
+ catch {
16768
+ return new Map(); /* @silent-fallback-ok — an unreadable annotation view reads as empty budgets; claims still capped per tick */
16769
+ }
16770
+ },
16771
+ actForceClaim: (sessionKey, episodeId) => reconciler.actStaleOwnerForceClaim(sessionKey, episodeId),
16772
+ emitClaimAnnotation: (input) => {
16773
+ const recordKey = deriveClaimAnnotationRecordKey(input.topic, input.episodeId);
16774
+ if (!recordKey || !replicatedRecordEmitter)
16775
+ return;
16776
+ replicatedRecordEmitter.emit(TOPIC_CLAIM_ANNOTATION_STORE_KEY, recordKey, buildClaimAnnotationPut(input));
16777
+ },
16778
+ pullWorkingSet: (topic) => {
16779
+ try {
16780
+ workingSetPullCoordinator?.onTopicAccepted(topic);
16781
+ }
16782
+ catch { /* @silent-fallback-ok — the carrier queues durably; a failed kick is retried by its own sweep */ }
16783
+ },
16784
+ onClaimed: (topic, episodeId, prevOwner) => {
16785
+ console.log(pc.yellow(` [StaleOwnerRelease] claimed topic ${topic} from ${prevOwner} (episode ${episodeId}) — resume rides the paced queue`));
16786
+ },
16787
+ trace: (entry) => {
16788
+ try {
16789
+ fs.mkdirSync(path.dirname(sorTracePath), { recursive: true });
16790
+ fs.appendFileSync(sorTracePath, JSON.stringify(entry) + '\n');
16791
+ }
16792
+ catch { /* trace is observability — never gates a verdict */ }
16793
+ },
16794
+ raiseAttention: (item) => {
16795
+ try {
16796
+ _meshAttentionRaise?.(item);
16797
+ }
16798
+ catch { /* attention raise is best-effort; the trace already recorded the escalation */ }
16799
+ },
16800
+ logger: (m) => console.log(pc.dim(` ${m}`)),
16801
+ });
16802
+ }
16337
16803
  const tickMs = Math.max(5000, ws13Cfg().ws13TickMs ?? 30000);
16338
16804
  const ws13Timer = setInterval(() => {
16339
16805
  try {
@@ -17249,6 +17715,72 @@ export async function startServer(options) {
17249
17715
  ...outcome,
17250
17716
  };
17251
17717
  },
17718
+ // U4.4 — handback-offer (PREFERRED-CAPTAIN side). RBAC already proved
17719
+ // the SENDER is the current lease holder; the LOAD-BEARING authority is
17720
+ // the holder-signed consent token presented at acquire time (the
17721
+ // handbackOpts branch — fail-closed on expiry/replay/reuse). Typed
17722
+ // declines; on 'accept' the claim + delivery-canary run async
17723
+ // (claim-before-release: the holder keeps holding until the higher
17724
+ // epoch lands; a silent claim changes nothing).
17725
+ 'handback-offer': async (cmd, sender) => {
17726
+ const c = cmd;
17727
+ const { decideHandbackOffer } = await import('../core/LeaseHandbackReconciler.js');
17728
+ const { getFeatureMetricsRecorder: hbMetrics } = await import('../core/CircuitBreakingIntelligenceProvider.js');
17729
+ const hb = config.multiMachine?.leaseSelfHeal?.preferredCaptainHandback ?? {};
17730
+ const token = c.consentToken;
17731
+ const tokenKey = `${token?.holder ?? ''}:${token?.nonce ?? ''}`;
17732
+ const decision = decideHandbackOffer({
17733
+ // Receiving-side authority gate: accepting moves REAL serving
17734
+ // authority, so dry-run also declines (the graduated ladder).
17735
+ enabled: hb.enabled === true && hb.dryRun === false,
17736
+ selfMachineId: meshSelfId,
17737
+ preferredAwakeMachineId: config.multiMachine?.leaseSelfHeal?.preferredAwakeMachineId ?? null,
17738
+ churnLatched: coordinator?.churnBreakerLatched() ?? false,
17739
+ quotaBlocked: machinePoolRegistry?.getCapacity(meshSelfId)?.quotaState?.blocked === true,
17740
+ tokenAlreadyUsed: _handbackSeenTokens.has(tokenKey),
17741
+ });
17742
+ if (decision === 'accept') {
17743
+ _handbackSeenTokens.add(tokenKey);
17744
+ if (_handbackSeenTokens.size > 512)
17745
+ _handbackSeenTokens.clear(); // bounded; token TTL is 60s
17746
+ void (async () => {
17747
+ try {
17748
+ const res = await coordinator.acquireLeaseOnHandbackConsent(token);
17749
+ if (!res.ok) {
17750
+ console.log(pc.yellow(` [LeaseHandback] consent claim did not land (${res.reason}) — old holder keeps holding`));
17751
+ return;
17752
+ }
17753
+ // Post-hand-back verification (Runtime End-to-End Proof): one
17754
+ // delivery-canary round-trip through the REAL signed mesh RPC
17755
+ // back to the OLD holder + a live lease check on this side.
17756
+ let canaryOk = false;
17757
+ try {
17758
+ canaryOk = (await _handbackCanary?.(sender)) === true;
17759
+ }
17760
+ catch {
17761
+ canaryOk = false; /* @silent-fallback-ok — a canary fault IS a canary failure; escalated loudly below, never silent */
17762
+ }
17763
+ try {
17764
+ hbMetrics()?.record({ feature: 'lease-handback', kind: 'event', outcome: canaryOk ? 'fired' : 'error', verdictId: canaryOk ? 'canary-verify-ok' : 'canary-verify-fail' });
17765
+ }
17766
+ catch { /* metrics never gate */ }
17767
+ if (!canaryOk) {
17768
+ _meshAttentionRaise?.({
17769
+ id: `lease-handback-canary:${Date.now()}`,
17770
+ title: 'Lease hand-back completed but the delivery canary FAILED',
17771
+ body: `This machine claimed the serving lease back from ${sender} but could not verify an end-to-end mesh round-trip afterward. Ingress may be degraded — check /pool and the mesh ropes.`,
17772
+ priority: 'high',
17773
+ sourceContext: 'lease-handback',
17774
+ });
17775
+ }
17776
+ }
17777
+ catch (err) {
17778
+ console.log(pc.yellow(` [LeaseHandback] consent claim error: ${err instanceof Error ? err.message : String(err)}`));
17779
+ }
17780
+ })();
17781
+ }
17782
+ return { response: decision };
17783
+ },
17252
17784
  'secret-share': (cmd, sender) => _secretShareHandler
17253
17785
  ? _secretShareHandler.handle(cmd, sender)
17254
17786
  : { ok: false, reason: 'secret-sync disabled' },
@@ -17352,6 +17884,45 @@ export async function startServer(options) {
17352
17884
  });
17353
17885
  const peerUrl = (machineId) => meshIdMgr.getActiveMachines().find((m) => m.machineId === machineId)?.entry.lastKnownUrl ?? null;
17354
17886
  _meshSelfId = meshSelfId;
17887
+ // U4.4 — the offer transport (HOLDER side) + the post-hand-back delivery
17888
+ // canary (PREFERRED side), both over the signed mesh RPC.
17889
+ _handbackSendOffer = async (target, offer) => {
17890
+ const url = peerUrl(target);
17891
+ if (!url)
17892
+ return 'timeout';
17893
+ try {
17894
+ const res = await meshClient.send({ machineId: target, url }, { type: 'handback-offer', proposedEpoch: offer.proposedEpoch, consentToken: offer.consentToken, expiresAt: offer.expiresAt }, 0, { timeoutMs: 15_000 });
17895
+ if (res.ok) {
17896
+ const r = (res.result ?? {});
17897
+ if (r.response === 'accept')
17898
+ return 'accept';
17899
+ if (r.response === 'declined:churn-latched' || r.response === 'declined:quota' || r.response === 'declined:legacy-peer')
17900
+ return r.response;
17901
+ return 'declined:other';
17902
+ }
17903
+ // Version skew (R-r2-2): 403 handback-offer-unauthorized / 501
17904
+ // no-handler ⇒ peer-cannot-hand-back → the sender stops re-offering
17905
+ // for the episode (degrade to today's sticky behavior).
17906
+ if (res.reason === 'no-handler' || res.reason === 'handback-offer-unauthorized')
17907
+ return 'declined:legacy-peer';
17908
+ return 'declined:other';
17909
+ }
17910
+ catch {
17911
+ return 'timeout'; /* @silent-fallback-ok — a transport fault reads as timeout: the holder KEEPS HOLDING (claim-before-release, the safe direction) */
17912
+ }
17913
+ };
17914
+ _handbackCanary = async (oldHolder) => {
17915
+ // One identity-authenticated round-trip through the REAL mesh RPC to
17916
+ // the old holder, plus a live lease check on THIS side — proves the
17917
+ // new holder's mesh path is genuinely serving after the hand-back.
17918
+ if (!(coordinator?.holdsLease() ?? false))
17919
+ return false;
17920
+ const url = peerUrl(oldHolder);
17921
+ if (!url)
17922
+ return false;
17923
+ const res = await meshClient.send({ machineId: oldHolder, url }, { type: 'session-status' }, 0, { timeoutMs: 10_000 });
17924
+ return res.ok === true;
17925
+ };
17355
17926
  // WS5.2 R4a — operator/issuer leg: deliver an R4a-signed account-follow-me mandate to a
17356
17927
  // REMOTE target over the signed mesh `account-follow-me-mandate-deliver` verb. Every
17357
17928
  // failure shape maps to an explicit outcome the issue-for-machine route surfaces honestly
@@ -18410,6 +18981,8 @@ export async function startServer(options) {
18410
18981
  requestJournalDelta: async (machineId, url, kind, fromSeq) => {
18411
18982
  try {
18412
18983
  const res = await meshClient.send({ machineId, url }, { type: 'journal-sync', request: { machineId, kind, fromSeq } }, 0);
18984
+ if (res.ok)
18985
+ _journalSyncLastOkMs = Date.now(); // U4.2 evidence-5 mirror freshness (an empty delta is still a successful sync)
18413
18986
  if (res.ok && res.result && typeof res.result === 'object' && 'batch' in res.result) {
18414
18987
  const b = res.result.batch;
18415
18988
  if (Array.isArray(b) && b.length > 0) {
@@ -18895,6 +19468,23 @@ export async function startServer(options) {
18895
19468
  // Lease-holder is the sole actor; single-machine = strict no-op. GET /guards.
18896
19469
  // Wired here (late) because it depends on machinePoolRegistry +
18897
19470
  // sessionOwnershipRegistry + _meshSelfId, all assigned during pool boot.
19471
+ // U4.2 / U4.4 shared attention seam — episode-keyed, deduped by the
19472
+ // attention store; created topics ride the existing flood budgets.
19473
+ _meshAttentionRaise = (item) => {
19474
+ if (!telegram)
19475
+ return;
19476
+ void telegram.createAttentionItem({
19477
+ id: item.id,
19478
+ title: item.title,
19479
+ summary: item.title,
19480
+ description: item.body,
19481
+ category: 'agent-health',
19482
+ priority: item.priority === 'high' ? 'HIGH' : 'NORMAL',
19483
+ sourceContext: item.sourceContext,
19484
+ lane: 'agent-health',
19485
+ healthKey: item.id,
19486
+ });
19487
+ };
18898
19488
  const _strandedEnabled = resolveDevAgentGate(config.monitoring?.strandedTopicSentinel?.enabled, config);
18899
19489
  if (_strandedEnabled && machinePoolRegistry && sessionOwnershipRegistry) {
18900
19490
  try {
@@ -18923,6 +19513,11 @@ export async function startServer(options) {
18923
19513
  });
18924
19514
  },
18925
19515
  nicknameOf: _nickById,
19516
+ // U4.2 R-r2-1: the DETECTION + ESCALATION path is hosted on ANY
19517
+ // quorum member when stale-owner release is active — a
19518
+ // no-lease-holder mesh still reaches the operator. The CLAIM stays
19519
+ // lease-holder-only (OwnershipReconciler).
19520
+ escalationQuorumHosted: () => _staleOwnerEngine?.isActive() === true,
18926
19521
  now: () => Date.now(),
18927
19522
  }, { ...config.monitoring?.strandedTopicSentinel, enabled: _strandedEnabled });
18928
19523
  strandedSentinel.start();
@@ -19036,7 +19631,7 @@ export async function startServer(options) {
19036
19631
  const server = new AgentServer({ config, sessionManager, state, scheduler, telegram, relationships, feedback, feedbackAnomalyDetector, dispatches, updateChecker, autoUpdater, autoDispatcher, quotaTracker, quotaManager, publisher, viewer, tunnel, evolution, watchdog, topicMemory, triageNurse, projectMapper, cartographer: cartographer ?? undefined, coherenceGate: scopeVerifier, contextHierarchy, canonicalState, operationGate, sentinel, adaptiveTrust, memoryMonitor, orphanReaper, coherenceMonitor, commitmentTracker, subscriptionPool, accountFollowMePeerViews: async () => { const nickById = new Map((_listPoolMachines?.() ?? []).map((m) => [m.machineId, m.nickname ?? m.machineId])); let peers = (_resolvePeerUrls?.() ?? []).map((p) => ({ machineId: p.machineId, nickname: nickById.get(p.machineId) ?? p.machineId, url: p.url })); if (peers.length === 0) {
19037
19632
  peers = (_listPoolMachines?.() ?? []).filter((m) => m.machineId !== _meshSelfId && !!m.lastKnownUrl).map((m) => ({ machineId: m.machineId, nickname: m.nickname ?? m.machineId, url: m.lastKnownUrl }));
19038
19633
  } if (peers.length === 0)
19039
- return []; const { fetchPeerSubscriptionViews } = await import('../core/fetchPeerSubscriptionViews.js'); return fetchPeerSubscriptionViews({ peers: () => peers, fetchImpl: fetch, authToken: config.authToken ?? '' }); }, quotaPoller, quotaAwareScheduler: _quotaAwareScheduler ?? undefined, proactiveSwapMonitor: _proactiveSwapMonitor ?? undefined, inUseAccountResolver, enrollmentWizard, accountFollowMeRevocation, credentialRepointing, semanticMemory, activitySentinel, rateLimitSentinel, releaseReadinessSentinel: releaseReadinessSentinel ?? undefined, greenPrAutoMerger: greenPrAutoMerger ?? undefined, guardLatchStore: guardLatchStore ?? undefined, messageRouter, summarySentinel, spawnManager, systemReviewer, capabilityMapper, selfKnowledgeTree, coverageAuditor, topicResumeMap: _topicResumeMap ?? undefined, topicProfile: _topicProfileCtx ?? undefined, sessionRefresh: _sessionRefresh ?? undefined, autonomyManager, trustElevationTracker, autonomousEvolution, coordinator: coordinator.enabled ? coordinator : undefined, meshBindActive: coordinator.managers.identityManager.hasIdentity() && config.multiMachine?.meshTransport?.enabled !== false, localSigningKeyPem, leaseTransport, peerEndpointRecorder, getSelfMeshEndpoints, onLeasePullRequest: () => leaseCoordinatorRef?.currentLease() ?? null, liveTailReceiver, handoffWireTransport, onHandoffBegin, onHandoffInitiate: handoffInitiate, handoffInProgress: handoffSentinelInProgress, messageLedger, currentInboundByTopic, replyMarkerTransport, onReplyMarker: messageLedger ? (marker) => { const m = marker; messageLedger.applyRemoteReplyMarker(m.dedupeKey, { platform: m.platform, replyIdempotencyKey: m.replyIdempotencyKey, epoch: m.epoch, topic: m.topic ?? null }); } : undefined, whatsapp: whatsappAdapter, slack: slackAdapter, imessage: imessageAdapter, whatsappBusinessBackend, messageBridge, hookEventReceiver, worktreeMonitor, subagentTracker, instructionsVerifier, handshakeManager: threadlineHandshake, threadlineRouter, conversationStore, threadLog, threadMessageRecorder, warrantsReplyGate, collaborationSurfacer, threadResumeMap, topicLinkageHandler: topicLinkageHandler ?? undefined, threadlineRelayClient, threadlineReplyWaiters, listenerManager: listenerManager ?? undefined, a2aDeliveryTracker: a2aDeliveryTracker ?? undefined, responseReviewGate, messagingToneGate, outboundDedupGate, telemetryHeartbeat, pasteManager, featureRegistry, discoveryEvaluator, completionEvaluator, unifiedTrust, liveConfig, sharedStateLedger, ledgerSessionRegistry, worktreeManager, oidcEnrolledRepos: parallelDevConfig?.oidcEnrolledRepos, initiativeTracker, projectRoundRunner, projectDriftChecker, machineHeartbeat, machinePoolRegistry, ropeHealthMonitor, getInboundQueue: () => _inboundQueue, meshRpcDispatcher, workingSetPullCoordinator, commitmentReplicaStore, preferenceReplicaStore, replicatedRecordEmitter, conflictStore, rollbackUnmerge, droppedOriginRegistry, preferencesUnionReader, forwardCommitmentMutate, sessionOwnershipRegistry, topicPinStore: _topicPinStore ?? undefined, ownershipReconciler: _ownershipReconciler ?? undefined, streamTicketStore: _streamTicketStore ?? undefined, poolStreamAllowRemoteInput: config.dashboard?.poolStream?.allowRemoteInput ?? false, poolStreamConnector: _poolStreamConnector ?? undefined, secretSync: _secretSyncHandle ?? undefined, meshSelfId: _meshSelfId ?? undefined, resolveRouterUrl: _resolveRouterUrl ?? undefined, resolvePeerUrls: _resolvePeerUrls ?? undefined, guardRegistry, listPoolMachines: _listPoolMachines ?? undefined, deliverMandateToMachine: _deliverMandateToMachine ?? undefined, poolLink: _poolLink ?? undefined, poolPollCache: _poolPollCache ?? undefined, sessionPoolE2EResultStore, proxyCoordinator, topicIntentStore, topicIntentArcCheck, usherSignalStore, intelligence: sharedIntelligence ?? undefined, telegramBridgeConfig, telegramBridge: telegramBridge ?? undefined, threadlineObservability, briefDeps, workingMemory, taskFlowRegistry, threadlineFlowBridge, sessionReaper, agentWorktreeReaper, orphanedWorkSentinel, mcpProcessReaper, geminiLoopRunner, sleepController, agentActivityState, reapLog, resumeQueue, resumeDrainer, autonomousLivenessReconciler, enforcedTerminationStatus: () => enforcedTerminationWatchdog?.guardStatus() ?? null, prHandLease: prHandLease ?? undefined, operatorStopRecorder: recordOperatorStop, sleepWakeDetector, unjustifiedStopGate, stopGateDb, stopNotifier, liveTestGate, liveTestGateMode, liveTestRunnerCtx }); // Resolve the late-bound topic-operator getter (increment 2e): routing was
19634
+ return []; const { fetchPeerSubscriptionViews } = await import('../core/fetchPeerSubscriptionViews.js'); return fetchPeerSubscriptionViews({ peers: () => peers, fetchImpl: fetch, authToken: config.authToken ?? '' }); }, quotaPoller, quotaAwareScheduler: _quotaAwareScheduler ?? undefined, proactiveSwapMonitor: _proactiveSwapMonitor ?? undefined, inUseAccountResolver, enrollmentWizard, accountFollowMeRevocation, credentialRepointing, semanticMemory, activitySentinel, rateLimitSentinel, releaseReadinessSentinel: releaseReadinessSentinel ?? undefined, greenPrAutoMerger: greenPrAutoMerger ?? undefined, guardLatchStore: guardLatchStore ?? undefined, messageRouter, summarySentinel, spawnManager, systemReviewer, capabilityMapper, selfKnowledgeTree, coverageAuditor, topicResumeMap: _topicResumeMap ?? undefined, topicProfile: _topicProfileCtx ?? undefined, sessionRefresh: _sessionRefresh ?? undefined, autonomyManager, trustElevationTracker, autonomousEvolution, coordinator: coordinator.enabled ? coordinator : undefined, meshBindActive: coordinator.managers.identityManager.hasIdentity() && config.multiMachine?.meshTransport?.enabled !== false, localSigningKeyPem, leaseTransport, peerEndpointRecorder, getSelfMeshEndpoints, onLeasePullRequest: () => leaseCoordinatorRef?.currentLease() ?? null, liveTailReceiver, handoffWireTransport, onHandoffBegin, onHandoffInitiate: handoffInitiate, handoffInProgress: handoffSentinelInProgress, messageLedger, currentInboundByTopic, replyMarkerTransport, onReplyMarker: messageLedger ? (marker) => { const m = marker; messageLedger.applyRemoteReplyMarker(m.dedupeKey, { platform: m.platform, replyIdempotencyKey: m.replyIdempotencyKey, epoch: m.epoch, topic: m.topic ?? null }); } : undefined, whatsapp: whatsappAdapter, slack: slackAdapter, imessage: imessageAdapter, whatsappBusinessBackend, messageBridge, hookEventReceiver, worktreeMonitor, subagentTracker, instructionsVerifier, handshakeManager: threadlineHandshake, threadlineRouter, conversationStore, threadLog, threadMessageRecorder, warrantsReplyGate, collaborationSurfacer, threadResumeMap, topicLinkageHandler: topicLinkageHandler ?? undefined, threadlineRelayClient, threadlineReplyWaiters, listenerManager: listenerManager ?? undefined, a2aDeliveryTracker: a2aDeliveryTracker ?? undefined, responseReviewGate, messagingToneGate, outboundDedupGate, telemetryHeartbeat, pasteManager, featureRegistry, discoveryEvaluator, completionEvaluator, unifiedTrust, liveConfig, sharedStateLedger, ledgerSessionRegistry, worktreeManager, oidcEnrolledRepos: parallelDevConfig?.oidcEnrolledRepos, initiativeTracker, projectRoundRunner, projectDriftChecker, machineHeartbeat, machinePoolRegistry, ropeHealthMonitor, getInboundQueue: () => _inboundQueue, meshRpcDispatcher, workingSetPullCoordinator, commitmentReplicaStore, preferenceReplicaStore, replicatedRecordEmitter, conflictStore, rollbackUnmerge, droppedOriginRegistry, preferencesUnionReader, forwardCommitmentMutate, sessionOwnershipRegistry, topicPinStore: _topicPinStore ?? undefined, ownershipReconciler: _ownershipReconciler ?? undefined, staleOwnerEngine: _staleOwnerEngine ?? undefined, leaseHandback: _leaseHandbackCtx ?? undefined, streamTicketStore: _streamTicketStore ?? undefined, poolStreamAllowRemoteInput: config.dashboard?.poolStream?.allowRemoteInput ?? false, poolStreamConnector: _poolStreamConnector ?? undefined, secretSync: _secretSyncHandle ?? undefined, meshSelfId: _meshSelfId ?? undefined, resolveRouterUrl: _resolveRouterUrl ?? undefined, resolvePeerUrls: _resolvePeerUrls ?? undefined, guardRegistry, listPoolMachines: _listPoolMachines ?? undefined, deliverMandateToMachine: _deliverMandateToMachine ?? undefined, poolLink: _poolLink ?? undefined, poolPollCache: _poolPollCache ?? undefined, sessionPoolE2EResultStore, proxyCoordinator, topicIntentStore, topicIntentArcCheck, usherSignalStore, intelligence: sharedIntelligence ?? undefined, telegramBridgeConfig, telegramBridge: telegramBridge ?? undefined, threadlineObservability, briefDeps, workingMemory, taskFlowRegistry, threadlineFlowBridge, sessionReaper, agentWorktreeReaper, orphanedWorkSentinel, mcpProcessReaper, geminiLoopRunner, sleepController, agentActivityState, reapLog, resumeQueue, resumeDrainer, autonomousLivenessReconciler, enforcedTerminationStatus: () => enforcedTerminationWatchdog?.guardStatus() ?? null, prHandLease: prHandLease ?? undefined, operatorStopRecorder: recordOperatorStop, sleepWakeDetector, unjustifiedStopGate, stopGateDb, stopNotifier, liveTestGate, liveTestGateMode, liveTestRunnerCtx }); // Resolve the late-bound topic-operator getter (increment 2e): routing was
19040
19635
  // wired before the server existed; from here on inbound binds use the
19041
19636
  // server's own store instance.
19042
19637
  _agentServerRef = server;