instar 1.3.703 → 1.3.705

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 (69) hide show
  1. package/dist/commands/server.d.ts.map +1 -1
  2. package/dist/commands/server.js +216 -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 +108 -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 +12 -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/scaffold/templates.d.ts.map +1 -1
  47. package/dist/scaffold/templates.js +2 -1
  48. package/dist/scaffold/templates.js.map +1 -1
  49. package/dist/server/CapabilityIndex.d.ts.map +1 -1
  50. package/dist/server/CapabilityIndex.js +1 -0
  51. package/dist/server/CapabilityIndex.js.map +1 -1
  52. package/dist/server/routes.d.ts.map +1 -1
  53. package/dist/server/routes.js +36 -0
  54. package/dist/server/routes.js.map +1 -1
  55. package/dist/users/UserManager.d.ts +22 -1
  56. package/dist/users/UserManager.d.ts.map +1 -1
  57. package/dist/users/UserManager.js +87 -2
  58. package/dist/users/UserManager.js.map +1 -1
  59. package/dist/users/testIdentityMarkers.d.ts +94 -0
  60. package/dist/users/testIdentityMarkers.d.ts.map +1 -0
  61. package/dist/users/testIdentityMarkers.js +170 -0
  62. package/dist/users/testIdentityMarkers.js.map +1 -0
  63. package/package.json +1 -1
  64. package/src/data/builtin-manifest.json +63 -63
  65. package/src/scaffold/templates.ts +2 -1
  66. package/upgrades/1.3.704.md +48 -0
  67. package/upgrades/1.3.705.md +79 -0
  68. package/upgrades/side-effects/postmortem-standards-s1-s6.md +46 -0
  69. 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;AA4H7D,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"}
@@ -127,7 +127,10 @@ import { HandoffWireTransport } from '../core/HandoffWireTransport.js';
127
127
  import { createHandoffReceiverWiring } from '../core/handoffReceiverWiring.js';
128
128
  import { createHandoffSentinelBootWiring } from '../core/handoffSentinelBootWiring.js';
129
129
  import { MessageProcessingLedger } from '../messaging/MessageProcessingLedger.js';
130
+ import { dedupeKeyFor } from '../messaging/ingressDedup.js';
130
131
  import { recoverStuckMessages } from '../messaging/stuckMessageRecovery.js';
132
+ import { SenderRejectionNoticer, SENDER_DEAUTHORIZED_CAUSE } from '../core/senderRejectionNotice.js';
133
+ import { appendMeshRejection } from '../core/meshRejectionLog.js';
131
134
  import { ReplyMarkerTransport } from '../core/ReplyMarkerTransport.js';
132
135
  import { decryptFromSync, encryptForSync } from '../core/SecretStore.js';
133
136
  import { createPrivateKey, createPublicKey, createHash } from 'node:crypto';
@@ -192,6 +195,7 @@ import { DEFAULT_RELAY_HOST } from '../threadline/constants.js';
192
195
  import { createUnifiedTrustSystem } from '../threadline/UnifiedTrustWiring.js';
193
196
  import { toInjection } from '../types/pipeline.js';
194
197
  import { UserManager } from '../users/UserManager.js';
198
+ import { loadTestIdentityKey } from '../users/testIdentityMarkers.js';
195
199
  import { formatUserContextForSession, hasUserContext } from '../users/UserContextBuilder.js';
196
200
  import { SafeFsExecutor } from '../core/SafeFsExecutor.js';
197
201
  // setup.ts uses @inquirer/prompts which requires Node 20.12+
@@ -483,6 +487,11 @@ let _meshSelfId = null;
483
487
  * strict no-op (always spawn locally, byte-for-byte legacy). Set in startServer's
484
488
  * mesh block from the lease coordinator. */
485
489
  let _holdsLeaseForSpawn = null;
490
+ /** silent-loss-refusal-conservation §2.C — the unified sender-rejection loss
491
+ * noticer. Constructed in startServer (needs messageLedger + notify + userManager);
492
+ * referenced module-wide (incl. wireTelegramRouting's live consumer) via this ref.
493
+ * null = not yet wired → the consumer falls back to the deterministic adapter send. */
494
+ let _senderRejectionNoticer = null;
486
495
  /** G3: the ownershipCheckedSpawn flag accessor (multiMachine.sessionPool.
487
496
  * ownershipCheckedSpawn). Defaults dark (no-op). Set in startServer. */
488
497
  let _ownershipCheckedSpawn = () => ({ enabled: false, dryRun: true });
@@ -2004,6 +2013,33 @@ getAttentionTopicId) {
2004
2013
  // box (the recognizer logs its pin, but route()'s actual placement/forward
2005
2014
  // decision was invisible; that hid the bug below from the first live test).
2006
2015
  console.log(`[session-pool] route topic ${topicId} → action=${outcome.action} owner=${outcome.owner ?? '?'} self=${_meshSelfId ?? '?'} acked=${outcome.acked}`);
2016
+ // silent-loss-refusal-conservation §2.A/§2.C — a first-class terminal
2017
+ // `rejected` (the owner re-validated the sender and refused) is NOT a
2018
+ // successful forward: TELL the user via the ORIGINATING adapter and
2019
+ // terminal-return WITHOUT local dispatch, BEFORE the isRemotelyHandled
2020
+ // test (which returns false for 'rejected' → would otherwise fall through
2021
+ // to a local inject the owner already refused). User-originated only (a
2022
+ // real platform uid + a bound topic — job/sentinel/canary keys excluded).
2023
+ if (outcome.action === 'rejected') {
2024
+ console.log(`[session-pool] topic ${topicId} REJECTED (${outcome.detail ?? 'sender-deauthorized'}) — not dispatching locally; notifying user`);
2025
+ if (telegramUserId) {
2026
+ const noticer = _senderRejectionNoticer;
2027
+ if (noticer) {
2028
+ noticer.onRejected({
2029
+ adapter: 'telegram',
2030
+ topicId,
2031
+ messageId: dedupeKeyFor('telegram', topicId, msg.id),
2032
+ senderUid: telegramUserId,
2033
+ peer: outcome.owner ?? undefined,
2034
+ });
2035
+ }
2036
+ else {
2037
+ // Noticer not yet wired — the deterministic adapter send is the floor.
2038
+ 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(() => { });
2039
+ }
2040
+ }
2041
+ return;
2042
+ }
2007
2043
  // Short-circuit local dispatch whenever the session ended up on ANOTHER machine
2008
2044
  // (forward/duplicate, OR a fresh remote 'spawned'/'owner-dead-replaced'). Before
2009
2045
  // this, only 'forwarded'/'duplicate' were caught, so a just-moved topic was
@@ -2214,7 +2250,11 @@ getAttentionTopicId) {
2214
2250
  ?? _topicPinStore?.asTopicMetadata(dmsg.sessionKey),
2215
2251
  senderEnvelope: dmsg.senderEnvelope,
2216
2252
  });
2217
- if (outcome.action === 'forwarded' && outcome.detail === 'sender-rejected') {
2253
+ // silent-loss-refusal-conservation §2.A forwardToOwner no longer masquerades
2254
+ // a refusal as `forwarded`; it returns the first-class terminal `rejected`.
2255
+ // Map it to the drain's `sender-rejected` disposition (→ terminal
2256
+ // `sender-deauthorized` + the unified §2.C loss notice via reportLoss).
2257
+ if (outcome.action === 'rejected') {
2218
2258
  return { kind: 'sender-rejected' };
2219
2259
  }
2220
2260
  if (isRemotelyHandled(outcome, _meshSelfId)) {
@@ -5744,7 +5784,12 @@ export async function startServer(options) {
5744
5784
  // collector runs even when the lifeline owns Telegram polling. See the
5745
5785
  // "Account switcher + quota collector pipeline" section above.
5746
5786
  // Initialize persistent UserManager for user identity resolution (Gap 8)
5747
- const userManager = new UserManager(config.stateDir, config.users);
5787
+ // silent-loss-refusal-conservation §2.D the authoritative UserManager
5788
+ // carries the server-held allow-marker key so a legitimate fixture-collision
5789
+ // profile's signed `allowTestIdentity` VERIFIES on load (a bogus marker is
5790
+ // quarantined). Read-only probes elsewhere construct WITHOUT the key (fixtures
5791
+ // skipped — the safe direction).
5792
+ const userManager = new UserManager(config.stateDir, config.users, { testIdentityKey: loadTestIdentityKey(config.stateDir) });
5748
5793
  attachUserReplication(userManager); // WS2.6 send-side (dark by default)
5749
5794
  // Fix command dependencies — populated later when subsystems initialize.
5750
5795
  // Uses a mutable ref so wireTelegramRouting can capture it in a closure now.
@@ -6559,6 +6604,30 @@ export async function startServer(options) {
6559
6604
  channel: { platform: 'slack', workspaceId: _slackAdapter?.getWorkspaceId(), channelId },
6560
6605
  });
6561
6606
  console.log(`[session-pool] slack route key=${routingKey} → action=${outcome.action} owner=${outcome.owner ?? '?'} self=${_meshSelfId ?? '?'} acked=${outcome.acked}`);
6607
+ // silent-loss-refusal-conservation §2.A/§2.C — a first-class terminal
6608
+ // `rejected` is NOT a successful forward: reply in-thread on the
6609
+ // originating Slack channel and terminal-return WITHOUT local dispatch,
6610
+ // BEFORE the isRemotelyHandled test. Slack NOTICE parity ships now;
6611
+ // Slack SENDER re-validation is tracked-followup 4.
6612
+ if (outcome.action === 'rejected') {
6613
+ console.log(`[session-pool] slack key ${routingKey} REJECTED (${outcome.detail ?? 'sender-deauthorized'}) — not dispatching locally; notifying user`);
6614
+ const slackUid = message.metadata?.slackUserId || undefined;
6615
+ if (slackUid) {
6616
+ const noticer = _senderRejectionNoticer;
6617
+ if (noticer) {
6618
+ noticer.onRejected({
6619
+ adapter: 'slack',
6620
+ slackKey: routingKey,
6621
+ messageId: dedupeKeyFor('slack', routingKey, String(message.id)),
6622
+ peer: outcome.owner ?? undefined,
6623
+ });
6624
+ }
6625
+ else {
6626
+ 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(() => { });
6627
+ }
6628
+ }
6629
+ return;
6630
+ }
6562
6631
  if (isRemotelyHandled(outcome, _meshSelfId)) {
6563
6632
  console.log(`[session-pool] slack key ${routingKey} handled by owner ${outcome.owner ?? '?'} (${outcome.action}) — not dispatching locally`);
6564
6633
  return;
@@ -15284,6 +15353,46 @@ export async function startServer(options) {
15284
15353
  console.error(`[exactly-once] ledger open failed, gate stays dark: ${err instanceof Error ? err.message : err}`);
15285
15354
  }
15286
15355
  }
15356
+ // ── Sender-rejection loss noticer (silent-loss-refusal-conservation §2.C) ──
15357
+ // The single funnel the live consumer (Telegram/Slack) AND the queue-drain loss
15358
+ // path route through so a first-class `rejected` outcome ALWAYS tells the user,
15359
+ // deduped + ceilinged. Durable per-messageId dedupe uses the ledger's
15360
+ // `markRejected` marker when the ledger is present; otherwise the in-memory
15361
+ // 30-min window bounds it. Divergence probe reads a fresh read-only UserManager.
15362
+ {
15363
+ const ledgerForNotice = messageLedger;
15364
+ _senderRejectionNoticer = new SenderRejectionNoticer({
15365
+ // @silent-fallback-ok: fire-and-forget notice send; the deterministic
15366
+ // delivery path + DeliveryFailureSentinel own retry, not this callsite.
15367
+ sendTelegram: (topicId, text) => { telegram?.sendToTopic(topicId, text).catch(() => { }); },
15368
+ sendSlack: (slackKey, text) => {
15369
+ const channelId = slackKey.split(':')[0];
15370
+ // @silent-fallback-ok: fire-and-forget notice send (see sendTelegram).
15371
+ _slackAdapter?.sendToChannel(channelId, text).catch(() => { });
15372
+ },
15373
+ alertHub: (title, body) => notify('IMMEDIATE', 'sender-rejection', `${title}: ${body}`),
15374
+ markRejectedDurable: ledgerForNotice
15375
+ // @silent-fallback-ok: a dedupe-ledger fault returns true (treat as
15376
+ // first-transition → SEND the notice) — fails toward TELLING the user.
15377
+ ? (messageId) => { try {
15378
+ return ledgerForNotice.markRejected(messageId, 0, { platform: 'mesh' });
15379
+ }
15380
+ catch {
15381
+ return true;
15382
+ } }
15383
+ : undefined,
15384
+ resolvesLocally: (uid) => {
15385
+ // @silent-fallback-ok: divergence-probe read helper; a resolve fault →
15386
+ // false (no divergence signal this tick), advisory only.
15387
+ try {
15388
+ return new UserManager(config.stateDir).resolveFromTelegramUserId(uid) !== null;
15389
+ }
15390
+ catch {
15391
+ return false;
15392
+ }
15393
+ },
15394
+ });
15395
+ }
15287
15396
  // ── ReleaseReadinessSentinel (Layer B of release-readiness-visibility) ──
15288
15397
  // Repo-gated dev-environment watchdog: only constructed when the install has
15289
15398
  // an analyzable instar git repo AND the feature is enabled in config. On a
@@ -16366,6 +16475,76 @@ export async function startServer(options) {
16366
16475
  // activation; the durable receipt + ACK is the complete dark-phase contract.)
16367
16476
  const deliverMod = await import('../core/DeliverMessageHandler.js');
16368
16477
  const deliverSeenFallback = new Set(); // used only if the SQLite ledger is unavailable
16478
+ // silent-loss-refusal-conservation §2.D — the wiring-time registry gate.
16479
+ // A stat-gated, READ-ONLY (no initialUsers merge — the old closure passed
16480
+ // config.users, which can writeFileSync per message) resolver + the
16481
+ // degenerate/unknown-unsafe/operator-resolution arm decision. Constructed
16482
+ // ONCE; the operator-uid probe reads the LOCAL topic-operator binding of the
16483
+ // deciding machine (KYP) via the late-bound AgentServer ref.
16484
+ const { SenderValidationGate } = await import('../core/senderValidationGate.js');
16485
+ const _svUsersFile = path.join(config.stateDir, 'users.json');
16486
+ const _svStatUsers = () => {
16487
+ // @silent-fallback-ok: stat fault → null (ENOENT/unreadable) is a first-class
16488
+ // input to classifyRegistry, which fails TOWARD delivery on a missing store.
16489
+ try {
16490
+ const s = fs.statSync(_svUsersFile);
16491
+ return { mtimeMs: s.mtimeMs, size: s.size };
16492
+ }
16493
+ catch {
16494
+ return null;
16495
+ }
16496
+ };
16497
+ let _svManager = null;
16498
+ let _svManagerStat = undefined;
16499
+ const _svResolveUid = (uid) => {
16500
+ const s = _svStatUsers();
16501
+ const changed = _svManagerStat === undefined ||
16502
+ (s === null) !== (_svManagerStat === null) ||
16503
+ (s !== null && _svManagerStat != null && (s.mtimeMs !== _svManagerStat.mtimeMs || s.size !== _svManagerStat.size));
16504
+ if (changed || !_svManager) {
16505
+ // READ-ONLY load: NO initialUsers → loadUsers runs no merge/persist.
16506
+ // @silent-fallback-ok: a load fault → null manager → resolveUid below
16507
+ // returns false; the gate's own degenerate-state check then fails toward
16508
+ // delivery rather than silently rejecting.
16509
+ try {
16510
+ _svManager = new UserManager(config.stateDir);
16511
+ }
16512
+ catch {
16513
+ _svManager = null;
16514
+ }
16515
+ _svManagerStat = s;
16516
+ }
16517
+ // @silent-fallback-ok: a resolve fault → false (unresolved) is safe — the
16518
+ // gate never rejects on an unresolved sender against a degenerate registry.
16519
+ try {
16520
+ return _svManager?.resolveFromTelegramUserId(uid) != null;
16521
+ }
16522
+ catch {
16523
+ return false;
16524
+ }
16525
+ };
16526
+ const senderValidationGate = new SenderValidationGate({
16527
+ usersFilePath: _svUsersFile,
16528
+ stateDir: config.stateDir,
16529
+ statUsers: _svStatUsers,
16530
+ resolveUid: _svResolveUid,
16531
+ operatorUidForTopic: (session) => {
16532
+ try {
16533
+ const op = _agentServerRef?.getTopicOperatorStore()?.getOperator(session);
16534
+ if (!op)
16535
+ return null;
16536
+ const n = Number(op.uid);
16537
+ return Number.isFinite(n) && n !== 0 ? n : null;
16538
+ }
16539
+ catch {
16540
+ // @silent-fallback-ok: an operator-store fault → null → the gate
16541
+ // declines to arm (fails toward delivery + alerts), never a silent reject.
16542
+ return null;
16543
+ }
16544
+ },
16545
+ alert: (level, cause, message) => notify(level === 'HIGH' ? 'IMMEDIATE' : 'SUMMARY', 'sender-validation', `[${cause}] ${message}`),
16546
+ log: (line) => console.warn(pc.yellow(line)),
16547
+ });
16369
16548
  const deliverMessageHandler = deliverMod.createDeliverMessageHandler({
16370
16549
  ownerEpochOf: (s) => ownReg.read(s)?.ownershipEpoch ?? null,
16371
16550
  recordReceipt: (messageId, session) => {
@@ -16385,22 +16564,26 @@ export async function startServer(options) {
16385
16564
  deliverSeenFallback.add(messageId);
16386
16565
  return true;
16387
16566
  },
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) => {
16567
+ // §3.4 sender re-validation, hardened by the §2.D wiring gate: a carried
16568
+ // envelope whose userId no longer resolves on THIS machine NACKs
16569
+ // `sender-rejected` BUT the gate never arms against a degenerate /
16570
+ // never-populated / corrupt / operator-unresolvable registry (it fails
16571
+ // toward delivery + shouts instead of rejecting everyone, the incident
16572
+ // fix). Envelope absent (old peer / live local frame) → not consulted.
16573
+ validateSender: (envelope, session) => {
16393
16574
  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
- }
16575
+ return senderValidationGate.decide(uid, session).verdict === 'deliver';
16576
+ },
16577
+ // §2.B the receiver-side rejection trace (metadata-only). The DECIDING
16578
+ // machine records its own refusal to logs/mesh-rejections.jsonl so a
16579
+ // future incident is never forensically blank on the machine that decided.
16580
+ onRejected: (meta) => {
16581
+ appendMeshRejection(config.stateDir, {
16582
+ reason: meta.reason,
16583
+ session: meta.session,
16584
+ messageId: meta.messageId,
16585
+ senderUid: meta.senderUid,
16586
+ });
16404
16587
  },
16405
16588
  // Owner-side bridge (§L4 handoff): a forwarded message landed → spawn/resume
16406
16589
  // the local session for the topic so the conversation continues on THIS machine.
@@ -17950,6 +18133,22 @@ export async function startServer(options) {
17950
18133
  }
17951
18134
  },
17952
18135
  reportLoss: (items, reason) => {
18136
+ // silent-loss-refusal-conservation §2.C — a `sender-deauthorized`
18137
+ // loss is a first-class REFUSAL, not a generic "didn't get to it":
18138
+ // route each item through the UNIFIED noticer (same neutral
18139
+ // wording + dedupe as the live path), NOT the generic message.
18140
+ if (reason === SENDER_DEAUTHORIZED_CAUSE && _senderRejectionNoticer) {
18141
+ for (const it of items) {
18142
+ const tid = Number(it.sessionKey);
18143
+ if (Number.isFinite(tid) && tid > 0) {
18144
+ _senderRejectionNoticer.onRejected({ adapter: 'telegram', topicId: tid, messageId: it.messageId });
18145
+ }
18146
+ else {
18147
+ _senderRejectionNoticer.onRejected({ adapter: 'slack', slackKey: it.sessionKey, messageId: it.messageId });
18148
+ }
18149
+ }
18150
+ return;
18151
+ }
17953
18152
  notifyInboundLoss(items, 'SUMMARY', (count) => `I didn't get to ${count} of your message(s) (${reason}) — resend anything still needed.`);
17954
18153
  },
17955
18154
  reportPossiblyNotInjected: (items) => {