instar 1.3.704 → 1.3.706

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 (86) hide show
  1. package/dist/commands/server.d.ts.map +1 -1
  2. package/dist/commands/server.js +232 -17
  3. package/dist/commands/server.js.map +1 -1
  4. package/dist/core/DeliverMessageHandler.d.ts +16 -0
  5. package/dist/core/DeliverMessageHandler.d.ts.map +1 -1
  6. package/dist/core/DeliverMessageHandler.js +0 -0
  7. package/dist/core/DeliverMessageHandler.js.map +1 -1
  8. package/dist/core/PostUpdateMigrator.d.ts +15 -0
  9. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  10. package/dist/core/PostUpdateMigrator.js +128 -0
  11. package/dist/core/PostUpdateMigrator.js.map +1 -1
  12. package/dist/core/QueueDrainLoop.d.ts +6 -2
  13. package/dist/core/QueueDrainLoop.d.ts.map +1 -1
  14. package/dist/core/QueueDrainLoop.js +0 -0
  15. package/dist/core/QueueDrainLoop.js.map +1 -1
  16. package/dist/core/SessionRouter.d.ts +24 -4
  17. package/dist/core/SessionRouter.d.ts.map +1 -1
  18. package/dist/core/SessionRouter.js +25 -2
  19. package/dist/core/SessionRouter.js.map +1 -1
  20. package/dist/core/meshRejectionLog.d.ts +37 -0
  21. package/dist/core/meshRejectionLog.d.ts.map +1 -0
  22. package/dist/core/meshRejectionLog.js +73 -0
  23. package/dist/core/meshRejectionLog.js.map +1 -0
  24. package/dist/core/registryHighWater.d.ts +59 -0
  25. package/dist/core/registryHighWater.d.ts.map +1 -0
  26. package/dist/core/registryHighWater.js +120 -0
  27. package/dist/core/registryHighWater.js.map +1 -0
  28. package/dist/core/senderRejectionNotice.d.ts +89 -0
  29. package/dist/core/senderRejectionNotice.d.ts.map +1 -0
  30. package/dist/core/senderRejectionNotice.js +149 -0
  31. package/dist/core/senderRejectionNotice.js.map +1 -0
  32. package/dist/core/senderValidationGate.d.ts +80 -0
  33. package/dist/core/senderValidationGate.d.ts.map +1 -0
  34. package/dist/core/senderValidationGate.js +133 -0
  35. package/dist/core/senderValidationGate.js.map +1 -0
  36. package/dist/core/types.d.ts +18 -0
  37. package/dist/core/types.d.ts.map +1 -1
  38. package/dist/core/types.js.map +1 -1
  39. package/dist/messaging/MessageProcessingLedger.d.ts +33 -5
  40. package/dist/messaging/MessageProcessingLedger.d.ts.map +1 -1
  41. package/dist/messaging/MessageProcessingLedger.js +50 -7
  42. package/dist/messaging/MessageProcessingLedger.js.map +1 -1
  43. package/dist/messaging/ingressDedup.d.ts.map +1 -1
  44. package/dist/messaging/ingressDedup.js +9 -0
  45. package/dist/messaging/ingressDedup.js.map +1 -1
  46. package/dist/monitoring/guardAcceptedFallbacks.d.ts +48 -0
  47. package/dist/monitoring/guardAcceptedFallbacks.d.ts.map +1 -0
  48. package/dist/monitoring/guardAcceptedFallbacks.js +106 -0
  49. package/dist/monitoring/guardAcceptedFallbacks.js.map +1 -0
  50. package/dist/monitoring/guardManifest.d.ts +25 -0
  51. package/dist/monitoring/guardManifest.d.ts.map +1 -1
  52. package/dist/monitoring/guardManifest.js +48 -0
  53. package/dist/monitoring/guardManifest.js.map +1 -1
  54. package/dist/monitoring/guardPostureView.d.ts +23 -0
  55. package/dist/monitoring/guardPostureView.d.ts.map +1 -1
  56. package/dist/monitoring/guardPostureView.js +66 -0
  57. package/dist/monitoring/guardPostureView.js.map +1 -1
  58. package/dist/monitoring/probes/GuardPostureProbe.d.ts +13 -2
  59. package/dist/monitoring/probes/GuardPostureProbe.d.ts.map +1 -1
  60. package/dist/monitoring/probes/GuardPostureProbe.js +143 -40
  61. package/dist/monitoring/probes/GuardPostureProbe.js.map +1 -1
  62. package/dist/scaffold/templates.d.ts.map +1 -1
  63. package/dist/scaffold/templates.js +3 -1
  64. package/dist/scaffold/templates.js.map +1 -1
  65. package/dist/server/CapabilityIndex.d.ts.map +1 -1
  66. package/dist/server/CapabilityIndex.js +1 -0
  67. package/dist/server/CapabilityIndex.js.map +1 -1
  68. package/dist/server/routes.d.ts.map +1 -1
  69. package/dist/server/routes.js +82 -1
  70. package/dist/server/routes.js.map +1 -1
  71. package/dist/users/UserManager.d.ts +22 -1
  72. package/dist/users/UserManager.d.ts.map +1 -1
  73. package/dist/users/UserManager.js +87 -2
  74. package/dist/users/UserManager.js.map +1 -1
  75. package/dist/users/testIdentityMarkers.d.ts +94 -0
  76. package/dist/users/testIdentityMarkers.d.ts.map +1 -0
  77. package/dist/users/testIdentityMarkers.js +170 -0
  78. package/dist/users/testIdentityMarkers.js.map +1 -0
  79. package/package.json +1 -1
  80. package/src/data/builtin-manifest.json +63 -63
  81. package/src/data/state-coherence-registry.json +18 -0
  82. package/src/scaffold/templates.ts +3 -1
  83. package/upgrades/1.3.705.md +79 -0
  84. package/upgrades/1.3.706.md +80 -0
  85. package/upgrades/side-effects/g3-dark-but-load-bearing-guards.md +143 -0
  86. package/upgrades/side-effects/silent-loss-refusal-conservation.md +124 -0
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AA8CH,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAU3D,OAAO,EAAE,eAAe,EAAiC,MAAM,iCAAiC,CAAC;AAyBjG,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAGvD,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAyH7D,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AA+EtD;;;;;;;;;;;;;;;;;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;AAq7CD,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,CAihBN;AA2lBD,wBAAsB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA01hBtE;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;AA8CH,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAU3D,OAAO,EAAE,eAAe,EAAiC,MAAM,iCAAiC,CAAC;AAyBjG,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAGvD,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AA6H7D,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAgFtD;;;;;;;;;;;;;;;;;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;AA07CD,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,CAijBN;AA2lBD,wBAAsB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAq/hBtE;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"}
@@ -86,6 +86,7 @@ import { SessionWatchdog } from '../monitoring/SessionWatchdog.js';
86
86
  import { GuardRegistry } from '../monitoring/GuardRegistry.js';
87
87
  import { resolveGuardConfigSnapshot, readGuardPostureBootSnapshot } from '../monitoring/guardPosture.js';
88
88
  import { buildGuardInventory, buildHeartbeatPostureBlock } from '../monitoring/guardPostureView.js';
89
+ import { readAcceptedFallbacks, scopeAcceptedFallbacks } from '../monitoring/guardAcceptedFallbacks.js';
89
90
  import { createGuardPostureProbes } from '../monitoring/probes/GuardPostureProbe.js';
90
91
  import { GuardPostureStore } from '../core/GuardPostureStore.js';
91
92
  import { isPeerUrlAllowedForCredentials } from '../server/peerUrlGuard.js';
@@ -127,7 +128,10 @@ import { HandoffWireTransport } from '../core/HandoffWireTransport.js';
127
128
  import { createHandoffReceiverWiring } from '../core/handoffReceiverWiring.js';
128
129
  import { createHandoffSentinelBootWiring } from '../core/handoffSentinelBootWiring.js';
129
130
  import { MessageProcessingLedger } from '../messaging/MessageProcessingLedger.js';
131
+ import { dedupeKeyFor } from '../messaging/ingressDedup.js';
130
132
  import { recoverStuckMessages } from '../messaging/stuckMessageRecovery.js';
133
+ import { SenderRejectionNoticer, SENDER_DEAUTHORIZED_CAUSE } from '../core/senderRejectionNotice.js';
134
+ import { appendMeshRejection } from '../core/meshRejectionLog.js';
131
135
  import { ReplyMarkerTransport } from '../core/ReplyMarkerTransport.js';
132
136
  import { decryptFromSync, encryptForSync } from '../core/SecretStore.js';
133
137
  import { createPrivateKey, createPublicKey, createHash } from 'node:crypto';
@@ -192,6 +196,7 @@ import { DEFAULT_RELAY_HOST } from '../threadline/constants.js';
192
196
  import { createUnifiedTrustSystem } from '../threadline/UnifiedTrustWiring.js';
193
197
  import { toInjection } from '../types/pipeline.js';
194
198
  import { UserManager } from '../users/UserManager.js';
199
+ import { loadTestIdentityKey } from '../users/testIdentityMarkers.js';
195
200
  import { formatUserContextForSession, hasUserContext } from '../users/UserContextBuilder.js';
196
201
  import { SafeFsExecutor } from '../core/SafeFsExecutor.js';
197
202
  // setup.ts uses @inquirer/prompts which requires Node 20.12+
@@ -483,6 +488,11 @@ let _meshSelfId = null;
483
488
  * strict no-op (always spawn locally, byte-for-byte legacy). Set in startServer's
484
489
  * mesh block from the lease coordinator. */
485
490
  let _holdsLeaseForSpawn = null;
491
+ /** silent-loss-refusal-conservation §2.C — the unified sender-rejection loss
492
+ * noticer. Constructed in startServer (needs messageLedger + notify + userManager);
493
+ * referenced module-wide (incl. wireTelegramRouting's live consumer) via this ref.
494
+ * null = not yet wired → the consumer falls back to the deterministic adapter send. */
495
+ let _senderRejectionNoticer = null;
486
496
  /** G3: the ownershipCheckedSpawn flag accessor (multiMachine.sessionPool.
487
497
  * ownershipCheckedSpawn). Defaults dark (no-op). Set in startServer. */
488
498
  let _ownershipCheckedSpawn = () => ({ enabled: false, dryRun: true });
@@ -2004,6 +2014,33 @@ getAttentionTopicId) {
2004
2014
  // box (the recognizer logs its pin, but route()'s actual placement/forward
2005
2015
  // decision was invisible; that hid the bug below from the first live test).
2006
2016
  console.log(`[session-pool] route topic ${topicId} → action=${outcome.action} owner=${outcome.owner ?? '?'} self=${_meshSelfId ?? '?'} acked=${outcome.acked}`);
2017
+ // silent-loss-refusal-conservation §2.A/§2.C — a first-class terminal
2018
+ // `rejected` (the owner re-validated the sender and refused) is NOT a
2019
+ // successful forward: TELL the user via the ORIGINATING adapter and
2020
+ // terminal-return WITHOUT local dispatch, BEFORE the isRemotelyHandled
2021
+ // test (which returns false for 'rejected' → would otherwise fall through
2022
+ // to a local inject the owner already refused). User-originated only (a
2023
+ // real platform uid + a bound topic — job/sentinel/canary keys excluded).
2024
+ if (outcome.action === 'rejected') {
2025
+ console.log(`[session-pool] topic ${topicId} REJECTED (${outcome.detail ?? 'sender-deauthorized'}) — not dispatching locally; notifying user`);
2026
+ if (telegramUserId) {
2027
+ const noticer = _senderRejectionNoticer;
2028
+ if (noticer) {
2029
+ noticer.onRejected({
2030
+ adapter: 'telegram',
2031
+ topicId,
2032
+ messageId: dedupeKeyFor('telegram', topicId, msg.id),
2033
+ senderUid: telegramUserId,
2034
+ peer: outcome.owner ?? undefined,
2035
+ });
2036
+ }
2037
+ else {
2038
+ // Noticer not yet wired — the deterministic adapter send is the floor.
2039
+ telegram.sendToTopic(topicId, "I got your message but couldn't confirm you as an approved sender, so it wasn't delivered. I've logged the details so this can be diagnosed.").catch(() => { });
2040
+ }
2041
+ }
2042
+ return;
2043
+ }
2007
2044
  // Short-circuit local dispatch whenever the session ended up on ANOTHER machine
2008
2045
  // (forward/duplicate, OR a fresh remote 'spawned'/'owner-dead-replaced'). Before
2009
2046
  // this, only 'forwarded'/'duplicate' were caught, so a just-moved topic was
@@ -2214,7 +2251,11 @@ getAttentionTopicId) {
2214
2251
  ?? _topicPinStore?.asTopicMetadata(dmsg.sessionKey),
2215
2252
  senderEnvelope: dmsg.senderEnvelope,
2216
2253
  });
2217
- if (outcome.action === 'forwarded' && outcome.detail === 'sender-rejected') {
2254
+ // silent-loss-refusal-conservation §2.A forwardToOwner no longer masquerades
2255
+ // a refusal as `forwarded`; it returns the first-class terminal `rejected`.
2256
+ // Map it to the drain's `sender-rejected` disposition (→ terminal
2257
+ // `sender-deauthorized` + the unified §2.C loss notice via reportLoss).
2258
+ if (outcome.action === 'rejected') {
2218
2259
  return { kind: 'sender-rejected' };
2219
2260
  }
2220
2261
  if (isRemotelyHandled(outcome, _meshSelfId)) {
@@ -5744,7 +5785,12 @@ export async function startServer(options) {
5744
5785
  // collector runs even when the lifeline owns Telegram polling. See the
5745
5786
  // "Account switcher + quota collector pipeline" section above.
5746
5787
  // Initialize persistent UserManager for user identity resolution (Gap 8)
5747
- const userManager = new UserManager(config.stateDir, config.users);
5788
+ // silent-loss-refusal-conservation §2.D the authoritative UserManager
5789
+ // carries the server-held allow-marker key so a legitimate fixture-collision
5790
+ // profile's signed `allowTestIdentity` VERIFIES on load (a bogus marker is
5791
+ // quarantined). Read-only probes elsewhere construct WITHOUT the key (fixtures
5792
+ // skipped — the safe direction).
5793
+ const userManager = new UserManager(config.stateDir, config.users, { testIdentityKey: loadTestIdentityKey(config.stateDir) });
5748
5794
  attachUserReplication(userManager); // WS2.6 send-side (dark by default)
5749
5795
  // Fix command dependencies — populated later when subsystems initialize.
5750
5796
  // Uses a mutable ref so wireTelegramRouting can capture it in a closure now.
@@ -6559,6 +6605,30 @@ export async function startServer(options) {
6559
6605
  channel: { platform: 'slack', workspaceId: _slackAdapter?.getWorkspaceId(), channelId },
6560
6606
  });
6561
6607
  console.log(`[session-pool] slack route key=${routingKey} → action=${outcome.action} owner=${outcome.owner ?? '?'} self=${_meshSelfId ?? '?'} acked=${outcome.acked}`);
6608
+ // silent-loss-refusal-conservation §2.A/§2.C — a first-class terminal
6609
+ // `rejected` is NOT a successful forward: reply in-thread on the
6610
+ // originating Slack channel and terminal-return WITHOUT local dispatch,
6611
+ // BEFORE the isRemotelyHandled test. Slack NOTICE parity ships now;
6612
+ // Slack SENDER re-validation is tracked-followup 4.
6613
+ if (outcome.action === 'rejected') {
6614
+ console.log(`[session-pool] slack key ${routingKey} REJECTED (${outcome.detail ?? 'sender-deauthorized'}) — not dispatching locally; notifying user`);
6615
+ const slackUid = message.metadata?.slackUserId || undefined;
6616
+ if (slackUid) {
6617
+ const noticer = _senderRejectionNoticer;
6618
+ if (noticer) {
6619
+ noticer.onRejected({
6620
+ adapter: 'slack',
6621
+ slackKey: routingKey,
6622
+ messageId: dedupeKeyFor('slack', routingKey, String(message.id)),
6623
+ peer: outcome.owner ?? undefined,
6624
+ });
6625
+ }
6626
+ else {
6627
+ slackAdapter.sendToChannel(channelId, "I got your message but couldn't confirm you as an approved sender, so it wasn't delivered. I've logged the details so this can be diagnosed.").catch(() => { });
6628
+ }
6629
+ }
6630
+ return;
6631
+ }
6562
6632
  if (isRemotelyHandled(outcome, _meshSelfId)) {
6563
6633
  console.log(`[session-pool] slack key ${routingKey} handled by owner ${outcome.owner ?? '?'} (${outcome.action}) — not dispatching locally`);
6564
6634
  return;
@@ -13228,6 +13298,10 @@ export async function startServer(options) {
13228
13298
  snapshot: snap,
13229
13299
  bootSnapshot: readGuardPostureBootSnapshot(config.stateDir),
13230
13300
  registry: guardRegistry,
13301
+ now: Date.now(),
13302
+ // G3 (§2.6): thread THIS machine's operator-accept map (one read),
13303
+ // so an accepted load-bearing guard never surfaces as a gap here.
13304
+ acceptedFallbacks: scopeAcceptedFallbacks(readAcceptedFallbacks(config.stateDir), _meshSelfId ?? null),
13231
13305
  });
13232
13306
  }
13233
13307
  catch {
@@ -13278,6 +13352,11 @@ export async function startServer(options) {
13278
13352
  await telegram.createAttentionItem(item);
13279
13353
  },
13280
13354
  stateDir: config.stateDir,
13355
+ // G3 (§6): the load-bearing-gap ATTENTION alert defaults on (soak-gated is
13356
+ // automatic — soak windows are manifest constants). Rollback = set this
13357
+ // false; /guards keeps the classification either way.
13358
+ alertLoadBearingGaps: config.monitoring
13359
+ ?.guardPostureProbe?.alertLoadBearingGaps ?? true,
13281
13360
  }),
13282
13361
  ...createPlatformProbes({
13283
13362
  tmuxPath,
@@ -15284,6 +15363,46 @@ export async function startServer(options) {
15284
15363
  console.error(`[exactly-once] ledger open failed, gate stays dark: ${err instanceof Error ? err.message : err}`);
15285
15364
  }
15286
15365
  }
15366
+ // ── Sender-rejection loss noticer (silent-loss-refusal-conservation §2.C) ──
15367
+ // The single funnel the live consumer (Telegram/Slack) AND the queue-drain loss
15368
+ // path route through so a first-class `rejected` outcome ALWAYS tells the user,
15369
+ // deduped + ceilinged. Durable per-messageId dedupe uses the ledger's
15370
+ // `markRejected` marker when the ledger is present; otherwise the in-memory
15371
+ // 30-min window bounds it. Divergence probe reads a fresh read-only UserManager.
15372
+ {
15373
+ const ledgerForNotice = messageLedger;
15374
+ _senderRejectionNoticer = new SenderRejectionNoticer({
15375
+ // @silent-fallback-ok: fire-and-forget notice send; the deterministic
15376
+ // delivery path + DeliveryFailureSentinel own retry, not this callsite.
15377
+ sendTelegram: (topicId, text) => { telegram?.sendToTopic(topicId, text).catch(() => { }); },
15378
+ sendSlack: (slackKey, text) => {
15379
+ const channelId = slackKey.split(':')[0];
15380
+ // @silent-fallback-ok: fire-and-forget notice send (see sendTelegram).
15381
+ _slackAdapter?.sendToChannel(channelId, text).catch(() => { });
15382
+ },
15383
+ alertHub: (title, body) => notify('IMMEDIATE', 'sender-rejection', `${title}: ${body}`),
15384
+ markRejectedDurable: ledgerForNotice
15385
+ // @silent-fallback-ok: a dedupe-ledger fault returns true (treat as
15386
+ // first-transition → SEND the notice) — fails toward TELLING the user.
15387
+ ? (messageId) => { try {
15388
+ return ledgerForNotice.markRejected(messageId, 0, { platform: 'mesh' });
15389
+ }
15390
+ catch {
15391
+ return true;
15392
+ } }
15393
+ : undefined,
15394
+ resolvesLocally: (uid) => {
15395
+ // @silent-fallback-ok: divergence-probe read helper; a resolve fault →
15396
+ // false (no divergence signal this tick), advisory only.
15397
+ try {
15398
+ return new UserManager(config.stateDir).resolveFromTelegramUserId(uid) !== null;
15399
+ }
15400
+ catch {
15401
+ return false;
15402
+ }
15403
+ },
15404
+ });
15405
+ }
15287
15406
  // ── ReleaseReadinessSentinel (Layer B of release-readiness-visibility) ──
15288
15407
  // Repo-gated dev-environment watchdog: only constructed when the install has
15289
15408
  // an analyzable instar git repo AND the feature is enabled in config. On a
@@ -15570,6 +15689,12 @@ export async function startServer(options) {
15570
15689
  snapshot: snap,
15571
15690
  bootSnapshot: readGuardPostureBootSnapshot(config.stateDir),
15572
15691
  registry: guardRegistry,
15692
+ now: Date.now(),
15693
+ // G3 (§2.6, round-4 3rd-site fix): the heartbeat compute MUST thread
15694
+ // the local accept map + now, else an operator-accepted load-bearing
15695
+ // guard is re-derived as a Gap here and shipped to peers as a FALSE
15696
+ // load-bearing-gap (and the soak lapse would misfire with an unthreaded now).
15697
+ acceptedFallbacks: scopeAcceptedFallbacks(readAcceptedFallbacks(config.stateDir), _meshSelfId ?? null),
15573
15698
  });
15574
15699
  return buildHeartbeatPostureBlock(inv, new Date().toISOString());
15575
15700
  }
@@ -16366,6 +16491,76 @@ export async function startServer(options) {
16366
16491
  // activation; the durable receipt + ACK is the complete dark-phase contract.)
16367
16492
  const deliverMod = await import('../core/DeliverMessageHandler.js');
16368
16493
  const deliverSeenFallback = new Set(); // used only if the SQLite ledger is unavailable
16494
+ // silent-loss-refusal-conservation §2.D — the wiring-time registry gate.
16495
+ // A stat-gated, READ-ONLY (no initialUsers merge — the old closure passed
16496
+ // config.users, which can writeFileSync per message) resolver + the
16497
+ // degenerate/unknown-unsafe/operator-resolution arm decision. Constructed
16498
+ // ONCE; the operator-uid probe reads the LOCAL topic-operator binding of the
16499
+ // deciding machine (KYP) via the late-bound AgentServer ref.
16500
+ const { SenderValidationGate } = await import('../core/senderValidationGate.js');
16501
+ const _svUsersFile = path.join(config.stateDir, 'users.json');
16502
+ const _svStatUsers = () => {
16503
+ // @silent-fallback-ok: stat fault → null (ENOENT/unreadable) is a first-class
16504
+ // input to classifyRegistry, which fails TOWARD delivery on a missing store.
16505
+ try {
16506
+ const s = fs.statSync(_svUsersFile);
16507
+ return { mtimeMs: s.mtimeMs, size: s.size };
16508
+ }
16509
+ catch {
16510
+ return null;
16511
+ }
16512
+ };
16513
+ let _svManager = null;
16514
+ let _svManagerStat = undefined;
16515
+ const _svResolveUid = (uid) => {
16516
+ const s = _svStatUsers();
16517
+ const changed = _svManagerStat === undefined ||
16518
+ (s === null) !== (_svManagerStat === null) ||
16519
+ (s !== null && _svManagerStat != null && (s.mtimeMs !== _svManagerStat.mtimeMs || s.size !== _svManagerStat.size));
16520
+ if (changed || !_svManager) {
16521
+ // READ-ONLY load: NO initialUsers → loadUsers runs no merge/persist.
16522
+ // @silent-fallback-ok: a load fault → null manager → resolveUid below
16523
+ // returns false; the gate's own degenerate-state check then fails toward
16524
+ // delivery rather than silently rejecting.
16525
+ try {
16526
+ _svManager = new UserManager(config.stateDir);
16527
+ }
16528
+ catch {
16529
+ _svManager = null;
16530
+ }
16531
+ _svManagerStat = s;
16532
+ }
16533
+ // @silent-fallback-ok: a resolve fault → false (unresolved) is safe — the
16534
+ // gate never rejects on an unresolved sender against a degenerate registry.
16535
+ try {
16536
+ return _svManager?.resolveFromTelegramUserId(uid) != null;
16537
+ }
16538
+ catch {
16539
+ return false;
16540
+ }
16541
+ };
16542
+ const senderValidationGate = new SenderValidationGate({
16543
+ usersFilePath: _svUsersFile,
16544
+ stateDir: config.stateDir,
16545
+ statUsers: _svStatUsers,
16546
+ resolveUid: _svResolveUid,
16547
+ operatorUidForTopic: (session) => {
16548
+ try {
16549
+ const op = _agentServerRef?.getTopicOperatorStore()?.getOperator(session);
16550
+ if (!op)
16551
+ return null;
16552
+ const n = Number(op.uid);
16553
+ return Number.isFinite(n) && n !== 0 ? n : null;
16554
+ }
16555
+ catch {
16556
+ // @silent-fallback-ok: an operator-store fault → null → the gate
16557
+ // declines to arm (fails toward delivery + alerts), never a silent reject.
16558
+ return null;
16559
+ }
16560
+ },
16561
+ alert: (level, cause, message) => notify(level === 'HIGH' ? 'IMMEDIATE' : 'SUMMARY', 'sender-validation', `[${cause}] ${message}`),
16562
+ log: (line) => console.warn(pc.yellow(line)),
16563
+ });
16369
16564
  const deliverMessageHandler = deliverMod.createDeliverMessageHandler({
16370
16565
  ownerEpochOf: (s) => ownReg.read(s)?.ownershipEpoch ?? null,
16371
16566
  recordReceipt: (messageId, session) => {
@@ -16385,22 +16580,26 @@ export async function startServer(options) {
16385
16580
  deliverSeenFallback.add(messageId);
16386
16581
  return true;
16387
16582
  },
16388
- // §3.4 sender re-validation (per-machine registries can diverge during
16389
- // a deauthorization): a carried envelope whose userId no longer
16390
- // resolves on THIS machine NACKs `sender-rejected`. Envelope absent
16391
- // (old peer / live local frame) not consulted.
16392
- validateSender: (envelope) => {
16583
+ // §3.4 sender re-validation, hardened by the §2.D wiring gate: a carried
16584
+ // envelope whose userId no longer resolves on THIS machine NACKs
16585
+ // `sender-rejected` BUT the gate never arms against a degenerate /
16586
+ // never-populated / corrupt / operator-unresolvable registry (it fails
16587
+ // toward delivery + shouts instead of rejecting everyone, the incident
16588
+ // fix). Envelope absent (old peer / live local frame) → not consulted.
16589
+ validateSender: (envelope, session) => {
16393
16590
  const uid = Number(envelope.userId);
16394
- if (!Number.isFinite(uid) || uid === 0)
16395
- return true;
16396
- // THIS machine's registry (file-backed; the telegram block's
16397
- // instance is scoped there same files, same truth).
16398
- try {
16399
- return new UserManager(config.stateDir, config.users).resolveFromTelegramUserId(uid) !== null;
16400
- }
16401
- catch {
16402
- return true;
16403
- }
16591
+ return senderValidationGate.decide(uid, session).verdict === 'deliver';
16592
+ },
16593
+ // §2.B the receiver-side rejection trace (metadata-only). The DECIDING
16594
+ // machine records its own refusal to logs/mesh-rejections.jsonl so a
16595
+ // future incident is never forensically blank on the machine that decided.
16596
+ onRejected: (meta) => {
16597
+ appendMeshRejection(config.stateDir, {
16598
+ reason: meta.reason,
16599
+ session: meta.session,
16600
+ messageId: meta.messageId,
16601
+ senderUid: meta.senderUid,
16602
+ });
16404
16603
  },
16405
16604
  // Owner-side bridge (§L4 handoff): a forwarded message landed → spawn/resume
16406
16605
  // the local session for the topic so the conversation continues on THIS machine.
@@ -17950,6 +18149,22 @@ export async function startServer(options) {
17950
18149
  }
17951
18150
  },
17952
18151
  reportLoss: (items, reason) => {
18152
+ // silent-loss-refusal-conservation §2.C — a `sender-deauthorized`
18153
+ // loss is a first-class REFUSAL, not a generic "didn't get to it":
18154
+ // route each item through the UNIFIED noticer (same neutral
18155
+ // wording + dedupe as the live path), NOT the generic message.
18156
+ if (reason === SENDER_DEAUTHORIZED_CAUSE && _senderRejectionNoticer) {
18157
+ for (const it of items) {
18158
+ const tid = Number(it.sessionKey);
18159
+ if (Number.isFinite(tid) && tid > 0) {
18160
+ _senderRejectionNoticer.onRejected({ adapter: 'telegram', topicId: tid, messageId: it.messageId });
18161
+ }
18162
+ else {
18163
+ _senderRejectionNoticer.onRejected({ adapter: 'slack', slackKey: it.sessionKey, messageId: it.messageId });
18164
+ }
18165
+ }
18166
+ return;
18167
+ }
17953
18168
  notifyInboundLoss(items, 'SUMMARY', (count) => `I didn't get to ${count} of your message(s) (${reason}) — resend anything still needed.`);
17954
18169
  },
17955
18170
  reportPossiblyNotInjected: (items) => {