instar 1.3.732 → 1.3.733

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 (50) hide show
  1. package/dist/commands/server.d.ts.map +1 -1
  2. package/dist/commands/server.js +127 -2
  3. package/dist/commands/server.js.map +1 -1
  4. package/dist/core/ConversationRegistry.d.ts +95 -0
  5. package/dist/core/ConversationRegistry.d.ts.map +1 -1
  6. package/dist/core/ConversationRegistry.js +292 -5
  7. package/dist/core/ConversationRegistry.js.map +1 -1
  8. package/dist/core/SessionManager.d.ts +15 -0
  9. package/dist/core/SessionManager.d.ts.map +1 -1
  10. package/dist/core/SessionManager.js +28 -0
  11. package/dist/core/SessionManager.js.map +1 -1
  12. package/dist/core/conversationBindToken.d.ts +36 -0
  13. package/dist/core/conversationBindToken.d.ts.map +1 -0
  14. package/dist/core/conversationBindToken.js +144 -0
  15. package/dist/core/conversationBindToken.js.map +1 -0
  16. package/dist/core/conversationIdentity.d.ts +14 -0
  17. package/dist/core/conversationIdentity.d.ts.map +1 -1
  18. package/dist/core/conversationIdentity.js +19 -0
  19. package/dist/core/conversationIdentity.js.map +1 -1
  20. package/dist/core/deliverToConversation.d.ts +68 -25
  21. package/dist/core/deliverToConversation.d.ts.map +1 -1
  22. package/dist/core/deliverToConversation.js +300 -11
  23. package/dist/core/deliverToConversation.js.map +1 -1
  24. package/dist/core/slackRefreshBinding.d.ts +8 -10
  25. package/dist/core/slackRefreshBinding.d.ts.map +1 -1
  26. package/dist/core/slackRefreshBinding.js +8 -16
  27. package/dist/core/slackRefreshBinding.js.map +1 -1
  28. package/dist/core/types.d.ts +9 -0
  29. package/dist/core/types.d.ts.map +1 -1
  30. package/dist/core/types.js.map +1 -1
  31. package/dist/monitoring/CommitmentTracker.d.ts +64 -0
  32. package/dist/monitoring/CommitmentTracker.d.ts.map +1 -1
  33. package/dist/monitoring/CommitmentTracker.js +58 -0
  34. package/dist/monitoring/CommitmentTracker.js.map +1 -1
  35. package/dist/monitoring/PromiseBeacon.d.ts +70 -0
  36. package/dist/monitoring/PromiseBeacon.d.ts.map +1 -1
  37. package/dist/monitoring/PromiseBeacon.js +221 -24
  38. package/dist/monitoring/PromiseBeacon.js.map +1 -1
  39. package/dist/server/AgentServer.d.ts +9 -0
  40. package/dist/server/AgentServer.d.ts.map +1 -1
  41. package/dist/server/AgentServer.js +8 -0
  42. package/dist/server/AgentServer.js.map +1 -1
  43. package/dist/server/routes.d.ts +12 -0
  44. package/dist/server/routes.d.ts.map +1 -1
  45. package/dist/server/routes.js +128 -10
  46. package/dist/server/routes.js.map +1 -1
  47. package/package.json +1 -1
  48. package/src/data/builtin-manifest.json +48 -48
  49. package/upgrades/1.3.733.md +108 -0
  50. package/upgrades/side-effects/durable-conversation-identity-increment2.md +88 -0
@@ -1,3 +1,4 @@
1
+ import { ConversationTuple } from './conversationIdentity.js';
1
2
  /** Entry origins (§3.4). `deliverToConversation` resolves ONLY the three local
2
3
  * origins; a pure `replicated` entry is read-context (KYP, §3.5). */
3
4
  export type ConversationOrigin = 'adopted-legacy-hash' | 'minted-probed' | 'adopted-replicated' | 'replicated';
@@ -95,6 +96,16 @@ export interface ConversationRegistryDeps {
95
96
  snapshotAdaptiveStep?: number;
96
97
  snapshotMaxIntervalMs?: number;
97
98
  }
99
+ /** §5.0(a) pinned E1 constants (R3-M1/R7-M2). Exported for the §10 tests. */
100
+ export declare const AMBIGUOUS_DEDUP_TTL_MS = 604800000;
101
+ export declare const CONTENT_HASH_DEDUP_WINDOW_MS = 900000;
102
+ /** §5.0(a) pathological TRIPWIRE, not a correctness bound (R4-M2/R5-minor-1):
103
+ * reached-with-all-live raises ONE aggregated attention item while the new
104
+ * entry is STILL journaled + retained — nothing is ever dropped. */
105
+ export declare const AMBIGUOUS_SENDS_HARD_CAP = 1000;
106
+ /** §5.1 reachability flap dampening (R3-minor). */
107
+ export declare const REACHABILITY_FLAP_THRESHOLD = 3;
108
+ export declare const REACHABILITY_FLAP_WINDOW_MS = 86400000;
98
109
  export declare class ConversationRegistry {
99
110
  private readonly d;
100
111
  private readonly snapshotPath;
@@ -109,6 +120,11 @@ export declare class ConversationRegistry {
109
120
  private bindPins;
110
121
  private ambiguousSends;
111
122
  private sendIntents;
123
+ /** R9-minor-1: parseable send-intent lines with the lane field STRIPPED —
124
+ * resolved toward RETRY at replay, surfaced once (never a suppressor). */
125
+ private missingLaneIntents;
126
+ /** §5.1 reachability flap tracking (in-memory, advisory — R3-minor). */
127
+ private reachabilityFlips;
112
128
  private workspacePin;
113
129
  private seqCounter;
114
130
  private snapshotHighWaterSeq;
@@ -224,6 +240,12 @@ export declare class ConversationRegistry {
224
240
  resolve(id: number): ConversationDescriptor | null;
225
241
  /** Forward lookup by canonical key OR transport sessionKey — mints NOTHING (§8). */
226
242
  resolveByKey(keyOrSessionKey: string): ConversationDescriptor | null;
243
+ /**
244
+ * §4 READ-ONLY id for a routing key (integration-nit): the existing entry's
245
+ * id, else the B6 collision-checked candidate — NEVER a registration. A pure
246
+ * comparison callsite must not acquire a get-or-create WRITE side-effect.
247
+ */
248
+ readIdForRoutingKey(routingKey: string): number | null;
227
249
  /**
228
250
  * `idForSessionKey` — GET-OR-CREATE (§6.0 #12, a named mint chokepoint).
229
251
  * Positive numeric session keys pass through; Slack routing keys mint.
@@ -239,6 +261,79 @@ export declare class ConversationRegistry {
239
261
  }>;
240
262
  aliasTable(): Record<string, number>;
241
263
  entryCount(): number;
264
+ /**
265
+ * Record a durable bind on a minted id (§3.5.2 property 2/4): the pin binds
266
+ * the id to the TUPLE it resolves to AT BIND TIME, refcounted per live
267
+ * binding, journaled (`op:"bind-pin"`, fsynced — the bind's own WAL line).
268
+ * Returns the bind-time tuple (the caller denormalizes it as `boundTuple`
269
+ * onto the binding record — property 5), or null when the id is unresolvable.
270
+ */
271
+ bindPin(id: number): ConversationTuple | null;
272
+ /** Release one refcount on a bind-pin (`op:"bind-release"`, §3.5.2 property 4).
273
+ * The pin is released only at ZERO — a commitment closing never strands a
274
+ * still-live sibling binding on the same id. No-op on a missing pin. */
275
+ bindRelease(id: number): void;
276
+ /** The live bind-pin tuple for an id (delivery-time overlay read — §3.5.2 property 3). */
277
+ getBindPinTuple(id: number): ConversationTuple | null;
278
+ private sendGuardKey;
279
+ /**
280
+ * Durable SEND-INTENT (R6-M1): appended+fsynced BEFORE the transport send,
281
+ * stamped with the caller's lane (R8-M1 — the boot-conversion discriminator,
282
+ * never inferred from the id's shape at boot).
283
+ */
284
+ recordSendIntent(conversationId: number, logicalSendId: string, lane: 'logical' | 'content-hash'): void;
285
+ /**
286
+ * Record the E1 suppressor entry — ONLY on a likely-posted outcome (success
287
+ * or ambiguous/ack-lost; NEVER a clean transient failure — R2-security-NEW-3).
288
+ * Durable (`op:"ambiguous-send"`, fsynced — R4-M2); resolves the pair's
289
+ * send-intent. The in-memory map is the loaded image of this durable state.
290
+ */
291
+ recordLikelyPosted(conversationId: number, logicalSendId: string, lane: 'logical' | 'content-hash'): void;
292
+ /** Retire a logical send (`op:"send-retire"` — delivered outcome advanced the
293
+ * seq, or the commitment closed). Idempotent; also resolves the intent. */
294
+ retireSend(conversationId: number, logicalSendId: string): void;
295
+ /** Resolve a send-intent WITHOUT a suppressor (`op:"send-intent-resolved"`) —
296
+ * the clean-transient-failure path (positive evidence the message never
297
+ * posted), so the retry is NOT suppressed (R2-security-NEW-3/R6-M1). */
298
+ resolveSendIntent(conversationId: number, logicalSendId: string): void;
299
+ /**
300
+ * The E1 suppression read (§5.0(a) R3-M1/R7-M2): the LOGICAL lane is
301
+ * retirement-based (an unretired entry suppresses whenever the re-fire
302
+ * arrives, bounded only by the 7-day TTL backstop); the CONTENT-HASH lane is
303
+ * WINDOW-based (a windowless caller has nothing to retire — outside the
304
+ * 15-min window the same text delivers again).
305
+ */
306
+ isSendSuppressed(conversationId: number, logicalSendId: string, lane: 'logical' | 'content-hash'): boolean;
307
+ /** Prune RETIRED/EXPIRED stragglers only (never a live unretired logical
308
+ * entry below its TTL — R4-M2 second arm). An expired content-hash entry
309
+ * prunes exactly like a retired one (R7-M2). */
310
+ pruneSendGuardState(): void;
311
+ /**
312
+ * R8-M1 lane-scoped boot conversion of crash-orphaned send-intents: an intent
313
+ * that is the LAST word for its (conversationId, logicalSendId) PAIR (R7-M3
314
+ * composite keying — replay already resolved every superseded pair) resolves
315
+ * BY ITS RECORDED LANE: `logical` (beacon) converts into an ambiguous-send
316
+ * suppressor (the outcome is genuinely unknown and the beacon has a next
317
+ * tick); `content-hash` resolves toward RETRY (the suppressed send IS the
318
+ * message — replay appends the missing send-intent-resolved durably, so the
319
+ * verdict is decided once, not re-decided every boot). Staged appends land
320
+ * POST-compose, fsynced before the registry begins serving (R9-low-3 /
321
+ * R10-minor-1).
322
+ */
323
+ private convertOrphanedSendIntents;
324
+ /**
325
+ * Flip an entry's LOCAL-authoritative reachability (§5.1). IDEMPOTENT
326
+ * (already-in-state → no write) and journaled WITHOUT fsync — it rides the
327
+ * same batched flush, so a mass event coalesces into one write, never an
328
+ * O(N) write storm on the failure path (scalability-F2). Returns whether the
329
+ * state changed and whether the conversation is in the R3-minor FLAP-DAMPENED
330
+ * state (>3 flips in 24h — the caller coalesces its dead-letter attention
331
+ * into one per-window flap item instead of one per episode).
332
+ */
333
+ setReachability(id: number, state: 'ok' | 'unreachable'): {
334
+ changed: boolean;
335
+ dampened: boolean;
336
+ };
242
337
  /**
243
338
  * Idempotent boot-time ensure: pre-register channel-level conversations from
244
339
  * the channel registry with their legacy-hash ids, inside ONE batched-save
@@ -1 +1 @@
1
- {"version":3,"file":"ConversationRegistry.d.ts","sourceRoot":"","sources":["../../src/core/ConversationRegistry.ts"],"names":[],"mappings":"AAkDA;sEACsE;AACtE,MAAM,MAAM,kBAAkB,GAAG,qBAAqB,GAAG,eAAe,GAAG,oBAAoB,GAAG,YAAY,CAAC;AAE/G,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,OAAO,CAAC;IAClB,2EAA2E;IAC3E,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,kBAAkB,CAAC;IAC3B,wFAAwF;IACxF,YAAY,EAAE,IAAI,GAAG,aAAa,CAAC;IACnC,GAAG,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IACzD,gFAAgF;IAChF,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,sBAAsB,GAC9B;IAAE,QAAQ,EAAE,UAAU,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,IAAI,CAAA;CAAE,GAC5D;IACE,QAAQ,EAAE,OAAO,CAAC;IAClB,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,kBAAkB,CAAC;IAC3B,YAAY,EAAE,IAAI,GAAG,aAAa,CAAC;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gFAAgF;IAChF,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEN;;qDAEqD;AACrD,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EACL,iBAAiB,GACjB,6BAA6B,GAC7B,oBAAoB,GACpB,iBAAiB,GACjB,gBAAgB,CAAC;CACtB;AAED;0CAC0C;AAC1C,MAAM,MAAM,iBAAiB,GACzB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GAC1C;IACE,EAAE,EAAE,KAAK,CAAC;IACV,KAAK,EACD,iBAAiB,GACjB,6BAA6B,GAC7B,iCAAiC,GACjC,oCAAoC,GACpC,cAAc,CAAC;CACpB,CAAC;AAEN,qFAAqF;AACrF,eAAO,MAAM,WAAW,gJAUd,CAAC;AACX,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC;AAmBrD,uEAAuE;AACvE,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,uBAAuB,CAAC,EAAE,MAAM,CAAC;CAClC;AAED,MAAM,WAAW,wBAAwB;IACvC;;iFAE6E;IAC7E,QAAQ,EAAE,MAAM,CAAC;IACjB,6DAA6D;IAC7D,SAAS,EAAE,MAAM,MAAM,CAAC;IACxB,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;IACjB,0EAA0E;IAC1E,kBAAkB,CAAC,EAAE,MAAM,OAAO,CAAC;IACnC,+EAA+E;IAC/E,sBAAsB,CAAC,EAAE,MAAM,OAAO,CAAC;IACvC,2GAA2G;IAC3G,mBAAmB,CAAC,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;IAC/C,8EAA8E;IAC9E,qBAAqB,CAAC,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;IACjD,OAAO,CAAC,EAAE,iBAAiB,CAAC;IAC5B;qEACiE;IACjE,WAAW,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACvE,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7B,wEAAwE;IACxE,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,8FAA8F;IAC9F,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;AAwBD,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,QAAQ,CAAC,CAAC,CAA2B;IAC7C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IAGrC,OAAO,CAAC,aAAa,CAAwC;IAC7D,OAAO,CAAC,IAAI,CAAwC;IACpD,OAAO,CAAC,OAAO,CAAwC;IACvD,OAAO,CAAC,OAAO,CAA6B;IAG5C,OAAO,CAAC,kBAAkB,CAA6B;IACvD,OAAO,CAAC,oBAAoB,CAA6B;IACzD,OAAO,CAAC,aAAa,CAAkC;IAGvD,OAAO,CAAC,QAAQ,CAAmF;IACnG,OAAO,CAAC,cAAc,CAA6C;IACnE,OAAO,CAAC,WAAW,CAAwE;IAE3F,OAAO,CAAC,YAAY,CAAkG;IAGtH,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,oBAAoB,CAAK;IACjC,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,YAAY,CAAK;IACzB,gGAAgG;IAChG,OAAO,CAAC,mBAAmB,CAA0C;IAGrE,OAAO,CAAC,aAAa,CAA+B;IACpD,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,iBAAiB,CAAK;IAC9B,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,aAAa,CAAS;IAG9B,OAAO,CAAC,cAAc,CAAgH;IACtI,OAAO,CAAC,eAAe,CAAK;IAC5B,OAAO,CAAC,YAAY,CAA0C;IAC9D,OAAO,CAAC,gBAAgB,CAAK;IAG7B,OAAO,CAAC,UAAU,CAAuB;IACzC,OAAO,CAAC,mBAAmB,CAAK;IAChC,OAAO,CAAC,qBAAqB,CAAuB;IACpD,OAAO,CAAC,oBAAoB,CAAuB;IACnD,OAAO,CAAC,aAAa,CAAgF;IACrG,OAAO,CAAC,MAAM,CAAS;gBAEX,IAAI,EAAE,wBAAwB;IAM1C,OAAO,CAAC,GAAG;IAIX,OAAO,CAAC,GAAG;IAQX,OAAO,CAAC,SAAS;IAQjB,OAAO,CAAC,gBAAgB;IAYxB;sEACkE;IAClE,IAAI,IAAI,IAAI;IA4BZ,OAAO,CAAC,aAAa;IASrB,OAAO,CAAC,YAAY;IAmDpB,mFAAmF;IACnF,OAAO,CAAC,YAAY;IAgBpB,OAAO,CAAC,aAAa;IAoFrB,uFAAuF;IACvF,OAAO,CAAC,kBAAkB;IAiG1B,OAAO,CAAC,UAAU;IAMlB,qFAAqF;IACrF,OAAO,CAAC,qBAAqB;IAmB7B;;uDAEmD;IACnD,OAAO,CAAC,0BAA0B;IAYlC;;;;;OAKG;IACH,OAAO,CAAC,aAAa;IAsBrB,OAAO,CAAC,QAAQ;IAahB,OAAO,CAAC,qBAAqB;IA0B7B;;;;;;OAMG;IACH,OAAO,CAAC,oBAAoB;IA+B5B,OAAO,CAAC,sBAAsB;IAe9B,OAAO,CAAC,gBAAgB;IAgBxB,4EAA4E;IACtE,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;IAoCpC,OAAO,CAAC,wBAAwB;IAUhC,iFAAiF;IACjF,UAAU,IAAI,IAAI;IAIlB,QAAQ,IAAI,IAAI;IAShB;;mDAE+C;IAC/C,OAAO,CAAC,iBAAiB;IASzB,+EAA+E;IAC/E,OAAO,CAAC,oBAAoB;IAM5B;;;OAGG;IACH,OAAO,CAAC,cAAc;IA8BtB,oGAAoG;IACpG,OAAO,CAAC,aAAa;IAuBrB;;;;OAIG;IACH,cAAc,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,iBAAiB;IAOhF;;;;;OAKG;IACH,qBAAqB,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,iBAAiB;IAuCvF,0EAA0E;IAC1E,OAAO,CAAC,qBAAqB;IA2B7B,OAAO,CAAC,SAAS;IAsHjB,OAAO,CAAC,aAAa;IAQrB;;;;OAIG;IACH,OAAO,CAAC,oBAAoB;IAS5B;oEACgE;IAChE,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,sBAAsB,GAAG,IAAI;IAqBlD,oFAAoF;IACpF,YAAY,CAAC,eAAe,EAAE,MAAM,GAAG,sBAAsB,GAAG,IAAI;IAYpE;;;OAGG;IACH,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAMlD,uDAAuD;IACvD,IAAI,CAAC,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,iBAAiB,CAAA;KAAE,CAAC;IAYpG,UAAU,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAKpC,UAAU,IAAI,MAAM;IASpB;;;;;;OAMG;IACH,eAAe,CACb,QAAQ,EAAE,KAAK,CAAC;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,EACrD,oBAAoB,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,GACnD;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,mBAAmB,EAAE,MAAM,CAAA;KAAE;IAgCnD,MAAM,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAuDjC,mDAAmD;IAC7C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAgB7B"}
1
+ {"version":3,"file":"ConversationRegistry.d.ts","sourceRoot":"","sources":["../../src/core/ConversationRegistry.ts"],"names":[],"mappings":"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"}
@@ -47,6 +47,16 @@ export const JOURNAL_OPS = [
47
47
  'send-intent',
48
48
  'send-intent-resolved',
49
49
  ];
50
+ /** §5.0(a) pinned E1 constants (R3-M1/R7-M2). Exported for the §10 tests. */
51
+ export const AMBIGUOUS_DEDUP_TTL_MS = 604800000; // 7 days — safety bound ONLY, never the suppression mechanism (logical lane)
52
+ export const CONTENT_HASH_DEDUP_WINDOW_MS = 900000; // 15 min — the fallback lane's WINDOW (mirrors the Telegram exact-duplicate window)
53
+ /** §5.0(a) pathological TRIPWIRE, not a correctness bound (R4-M2/R5-minor-1):
54
+ * reached-with-all-live raises ONE aggregated attention item while the new
55
+ * entry is STILL journaled + retained — nothing is ever dropped. */
56
+ export const AMBIGUOUS_SENDS_HARD_CAP = 1000;
57
+ /** §5.1 reachability flap dampening (R3-minor). */
58
+ export const REACHABILITY_FLAP_THRESHOLD = 3;
59
+ export const REACHABILITY_FLAP_WINDOW_MS = 86400000; // 24h
50
60
  const ENTRY_CEILING = 50000; // §3.4 JSON-store design ceiling
51
61
  const ENTRY_THRESHOLD = 40000; // 80% of ceiling — the pinned tripwire (R3-minor)
52
62
  const FILE_BYTES_THRESHOLD = 8 * 1024 * 1024; // 80% of ~10MB
@@ -67,10 +77,15 @@ export class ConversationRegistry {
67
77
  reservedCanonicals = new Map(); // cand → owning tupleKey
68
78
  displacedAssignments = new Map(); // offset → owning tupleKey (GLOBAL — R4-C1)
69
79
  candClaimants = new Map(); // cand → claimant tupleKeys
70
- // ── §5.0(a)/§3.5.2 journal-applied state (writers land in later increments) ──
80
+ // ── §5.0(a)/§3.5.2 journal-applied state (writers: §6.1 increment 2) ──
71
81
  bindPins = new Map();
72
82
  ambiguousSends = new Map();
73
83
  sendIntents = new Map();
84
+ /** R9-minor-1: parseable send-intent lines with the lane field STRIPPED —
85
+ * resolved toward RETRY at replay, surfaced once (never a suppressor). */
86
+ missingLaneIntents = 0;
87
+ /** §5.1 reachability flap tracking (in-memory, advisory — R3-minor). */
88
+ reachabilityFlips = new Map();
74
89
  workspacePin = null;
75
90
  // ── Journal state ──
76
91
  seqCounter = 0;
@@ -149,6 +164,11 @@ export class ConversationRegistry {
149
164
  this.replayJournal();
150
165
  this.rebuildDerivedIndexes();
151
166
  this.applyAliasAssignmentFilter(); // R6-M3 pin 2: the disjointness invariant holds at every BOOT fixpoint
167
+ this.convertOrphanedSendIntents(); // R8-M1 lane-scoped boot conversion (staged post-compose, fsynced before serving — R9-low-3/R10-minor-1)
168
+ this.pruneSendGuardState();
169
+ if (this.missingLaneIntents > 0) {
170
+ this.attention('conversation-registry:missing-lane-intent', 'Conversation registry: send-intent record(s) with a stripped lane field', `${this.missingLaneIntents} parseable send-intent journal record(s) carried no lane discriminator (R9-minor-1). Each was resolved toward RETRY (content-hash treatment — loss is never silent, never a suppressor). This indicates a serialization bug worth a look.`);
171
+ }
152
172
  if (this.unappliedUnknownOps.length > 0) {
153
173
  const first = this.unappliedUnknownOps[0];
154
174
  const kinds = [...new Set(this.unappliedUnknownOps.map((u) => u.op))].join(', ');
@@ -197,8 +217,12 @@ export class ConversationRegistry {
197
217
  }
198
218
  }
199
219
  for (const [k, v] of Object.entries(raw.ambiguousSends ?? {})) {
200
- if (v && typeof v.recordedAt === 'string')
201
- this.ambiguousSends.set(k, v);
220
+ if (v && typeof v.recordedAt === 'string') {
221
+ this.ambiguousSends.set(k, {
222
+ recordedAt: v.recordedAt,
223
+ ...(v.lane === 'logical' || v.lane === 'content-hash' ? { lane: v.lane } : {}),
224
+ });
225
+ }
202
226
  }
203
227
  for (const [k, v] of Object.entries(raw.sendIntents ?? {})) {
204
228
  if (v && (v.lane === 'logical' || v.lane === 'content-hash') && typeof v.seq === 'number')
@@ -411,7 +435,10 @@ export class ConversationRegistry {
411
435
  case 'ambiguous-send': {
412
436
  if (typeof rec.conversationId !== 'number' || !rec.logicalSendId)
413
437
  return;
414
- this.ambiguousSends.set(`${rec.conversationId}|${rec.logicalSendId}`, { recordedAt: rec.ts });
438
+ this.ambiguousSends.set(`${rec.conversationId}|${rec.logicalSendId}`, {
439
+ recordedAt: rec.ts,
440
+ ...(rec.lane === 'logical' || rec.lane === 'content-hash' ? { lane: rec.lane } : {}),
441
+ });
415
442
  this.sendIntents.delete(`${rec.conversationId}|${rec.logicalSendId}`);
416
443
  return;
417
444
  }
@@ -426,7 +453,10 @@ export class ConversationRegistry {
426
453
  if (typeof rec.conversationId !== 'number' || !rec.logicalSendId)
427
454
  return;
428
455
  // Malformed/unknown lane resolves toward RETRY (content-hash treatment)
429
- // — R9-minor-1/R10-low-2 (loss-is-never-silent picks retry).
456
+ // — R9-minor-1/R10-low-2 (loss-is-never-silent picks retry). Counted so
457
+ // the post-replay boot pass can raise ONE deduped attention item.
458
+ if (rec.lane !== 'logical' && rec.lane !== 'content-hash')
459
+ this.missingLaneIntents++;
430
460
  const lane = rec.lane === 'logical' ? 'logical' : 'content-hash';
431
461
  this.sendIntents.set(`${rec.conversationId}|${rec.logicalSendId}`, { lane, seq: rec.seq });
432
462
  return;
@@ -793,6 +823,19 @@ export class ConversationRegistry {
793
823
  const existing = this.byTuple.get(tupleKeyFor(tuple));
794
824
  if (existing) {
795
825
  this.maybeUpgradeWorkspace(existing);
826
+ // WAL rule at durable-bind time (§3.3): a pre-existing SPECULATIVE entry
827
+ // rode the batched snapshot only — its journal line may not exist yet. A
828
+ // durable binding is not re-derivable after a crash, so ensure the entry
829
+ // is durably journaled NOW (idempotent op:"mint" re-apply; fsynced)
830
+ // BEFORE the id is handed to the binding.
831
+ this.appendJournal({
832
+ op: 'mint',
833
+ key: canonicalKeyFor(tuple, existing.workspaceId),
834
+ tuple: [tuple.platform, tuple.channelId, tuple.threadTs],
835
+ id: existing.id,
836
+ origin: existing.origin,
837
+ hlc: existing.hlc,
838
+ }, { fsync: true });
796
839
  return { ok: true, id: existing.id, created: false };
797
840
  }
798
841
  const w = this.breakerWindow(tuple.channelId);
@@ -845,6 +888,11 @@ export class ConversationRegistry {
845
888
  existing.label = opts.label; // write-on-change (§3.4 G4)
846
889
  this.scheduleSnapshot();
847
890
  }
891
+ // §5.1 reachability auto-clear: an authenticated inbound on the tuple is
892
+ // positive evidence the conversation is reachable again (re-invited /
893
+ // un-archived channel is never stuck).
894
+ if (existing.reachability === 'unreachable')
895
+ this.setReachability(existing.id, 'ok');
848
896
  return { id: existing.id, created: false, registered: true };
849
897
  }
850
898
  const routingKey = routingKeyForTuple(tuple);
@@ -997,6 +1045,22 @@ export class ConversationRegistry {
997
1045
  const entry = this.byTuple.get(tupleKeyFor(tuple));
998
1046
  return entry ? this.resolve(entry.id) : null;
999
1047
  }
1048
+ /**
1049
+ * §4 READ-ONLY id for a routing key (integration-nit): the existing entry's
1050
+ * id, else the B6 collision-checked candidate — NEVER a registration. A pure
1051
+ * comparison callsite must not acquire a get-or-create WRITE side-effect.
1052
+ */
1053
+ readIdForRoutingKey(routingKey) {
1054
+ this.load();
1055
+ const tuple = tupleForRoutingKey(routingKey);
1056
+ if (!tuple)
1057
+ return null;
1058
+ const tKey = tupleKeyFor(tuple);
1059
+ const existing = this.byTuple.get(tKey);
1060
+ if (existing)
1061
+ return existing.id;
1062
+ return this.collisionCheckedRead(candidateIdForRoutingKey(routingKey), tKey);
1063
+ }
1000
1064
  /**
1001
1065
  * `idForSessionKey` — GET-OR-CREATE (§6.0 #12, a named mint chokepoint).
1002
1066
  * Positive numeric session keys pass through; Slack routing keys mint.
@@ -1030,6 +1094,223 @@ export class ConversationRegistry {
1030
1094
  return this.conversations.size;
1031
1095
  }
1032
1096
  // ─────────────────────────────────────────────────────────────────────────
1097
+ // Bind-pin overlay (§3.5.2 — increment 2 writers)
1098
+ // ─────────────────────────────────────────────────────────────────────────
1099
+ /**
1100
+ * Record a durable bind on a minted id (§3.5.2 property 2/4): the pin binds
1101
+ * the id to the TUPLE it resolves to AT BIND TIME, refcounted per live
1102
+ * binding, journaled (`op:"bind-pin"`, fsynced — the bind's own WAL line).
1103
+ * Returns the bind-time tuple (the caller denormalizes it as `boundTuple`
1104
+ * onto the binding record — property 5), or null when the id is unresolvable.
1105
+ */
1106
+ bindPin(id) {
1107
+ this.load();
1108
+ const entry = this.byId.get(this.aliases.get(id) ?? id);
1109
+ if (!entry)
1110
+ return null;
1111
+ const tuple = ['slack', entry.channelId, entry.threadTs];
1112
+ const pin = this.bindPins.get(id);
1113
+ const refcount = pin ? pin.refcount + 1 : 1;
1114
+ if (pin)
1115
+ pin.refcount = refcount;
1116
+ else
1117
+ this.bindPins.set(id, { tuple, refcount });
1118
+ this.appendJournal({ op: 'bind-pin', id, tuple, refcount }, { fsync: true });
1119
+ this.scheduleSnapshot();
1120
+ return { platform: 'slack', channelId: entry.channelId, threadTs: entry.threadTs };
1121
+ }
1122
+ /** Release one refcount on a bind-pin (`op:"bind-release"`, §3.5.2 property 4).
1123
+ * The pin is released only at ZERO — a commitment closing never strands a
1124
+ * still-live sibling binding on the same id. No-op on a missing pin. */
1125
+ bindRelease(id) {
1126
+ this.load();
1127
+ const pin = this.bindPins.get(id);
1128
+ if (!pin)
1129
+ return;
1130
+ const refcount = pin.refcount - 1;
1131
+ this.appendJournal({ op: 'bind-release', id, refcount }, { fsync: true });
1132
+ if (refcount <= 0)
1133
+ this.bindPins.delete(id);
1134
+ else
1135
+ pin.refcount = refcount;
1136
+ this.scheduleSnapshot();
1137
+ }
1138
+ /** The live bind-pin tuple for an id (delivery-time overlay read — §3.5.2 property 3). */
1139
+ getBindPinTuple(id) {
1140
+ this.load();
1141
+ const pin = this.bindPins.get(id);
1142
+ if (!pin)
1143
+ return null;
1144
+ return { platform: 'slack', channelId: pin.tuple[1], threadTs: pin.tuple[2] ?? null };
1145
+ }
1146
+ // ─────────────────────────────────────────────────────────────────────────
1147
+ // E1 ambiguous-outcome idempotency guard state (§5.0(a) — increment 2 writers)
1148
+ // ─────────────────────────────────────────────────────────────────────────
1149
+ sendGuardKey(conversationId, logicalSendId) {
1150
+ return `${conversationId}|${logicalSendId}`;
1151
+ }
1152
+ /**
1153
+ * Durable SEND-INTENT (R6-M1): appended+fsynced BEFORE the transport send,
1154
+ * stamped with the caller's lane (R8-M1 — the boot-conversion discriminator,
1155
+ * never inferred from the id's shape at boot).
1156
+ */
1157
+ recordSendIntent(conversationId, logicalSendId, lane) {
1158
+ this.load();
1159
+ const seq = this.appendJournal({ op: 'send-intent', conversationId, logicalSendId, lane }, { fsync: true });
1160
+ this.sendIntents.set(this.sendGuardKey(conversationId, logicalSendId), { lane, seq });
1161
+ this.scheduleSnapshot();
1162
+ }
1163
+ /**
1164
+ * Record the E1 suppressor entry — ONLY on a likely-posted outcome (success
1165
+ * or ambiguous/ack-lost; NEVER a clean transient failure — R2-security-NEW-3).
1166
+ * Durable (`op:"ambiguous-send"`, fsynced — R4-M2); resolves the pair's
1167
+ * send-intent. The in-memory map is the loaded image of this durable state.
1168
+ */
1169
+ recordLikelyPosted(conversationId, logicalSendId, lane) {
1170
+ this.load();
1171
+ this.pruneSendGuardState();
1172
+ const key = this.sendGuardKey(conversationId, logicalSendId);
1173
+ this.appendJournal({ op: 'ambiguous-send', conversationId, logicalSendId, lane }, { fsync: true });
1174
+ this.ambiguousSends.set(key, { recordedAt: this.now().toISOString(), lane });
1175
+ this.sendIntents.delete(key);
1176
+ if (this.ambiguousSends.size > AMBIGUOUS_SENDS_HARD_CAP) {
1177
+ // Pathological tripwire (R4-M2 scoped per R5-minor-1): loud, aggregated,
1178
+ // and the new entry is STILL journaled + retained — nothing dropped, so a
1179
+ // silent eviction can never re-open the double-post.
1180
+ this.attention('conversation-registry:send-guard-cap', 'E1 send-guard entry population exceeded its pathological tripwire', `${this.ambiguousSends.size} unretired/unexpired ambiguous-send entries exceed the ${AMBIGUOUS_SENDS_HARD_CAP} cap (§5.0(a)). Live entries are bounded by open beacon commitments (~maxActiveBeacons) by construction — this population means an upstream cap was raised or a retirement path is broken. No entry was evicted or dropped.`);
1181
+ }
1182
+ this.scheduleSnapshot();
1183
+ }
1184
+ /** Retire a logical send (`op:"send-retire"` — delivered outcome advanced the
1185
+ * seq, or the commitment closed). Idempotent; also resolves the intent. */
1186
+ retireSend(conversationId, logicalSendId) {
1187
+ this.load();
1188
+ const key = this.sendGuardKey(conversationId, logicalSendId);
1189
+ if (!this.ambiguousSends.has(key) && !this.sendIntents.has(key))
1190
+ return;
1191
+ this.appendJournal({ op: 'send-retire', conversationId, logicalSendId }, { fsync: true });
1192
+ this.ambiguousSends.delete(key);
1193
+ this.sendIntents.delete(key);
1194
+ this.scheduleSnapshot();
1195
+ }
1196
+ /** Resolve a send-intent WITHOUT a suppressor (`op:"send-intent-resolved"`) —
1197
+ * the clean-transient-failure path (positive evidence the message never
1198
+ * posted), so the retry is NOT suppressed (R2-security-NEW-3/R6-M1). */
1199
+ resolveSendIntent(conversationId, logicalSendId) {
1200
+ this.load();
1201
+ const key = this.sendGuardKey(conversationId, logicalSendId);
1202
+ if (!this.sendIntents.has(key))
1203
+ return;
1204
+ this.appendJournal({ op: 'send-intent-resolved', conversationId, logicalSendId }, { fsync: true });
1205
+ this.sendIntents.delete(key);
1206
+ this.scheduleSnapshot();
1207
+ }
1208
+ /**
1209
+ * The E1 suppression read (§5.0(a) R3-M1/R7-M2): the LOGICAL lane is
1210
+ * retirement-based (an unretired entry suppresses whenever the re-fire
1211
+ * arrives, bounded only by the 7-day TTL backstop); the CONTENT-HASH lane is
1212
+ * WINDOW-based (a windowless caller has nothing to retire — outside the
1213
+ * 15-min window the same text delivers again).
1214
+ */
1215
+ isSendSuppressed(conversationId, logicalSendId, lane) {
1216
+ this.load();
1217
+ const entry = this.ambiguousSends.get(this.sendGuardKey(conversationId, logicalSendId));
1218
+ if (!entry)
1219
+ return false;
1220
+ const age = this.now().getTime() - Date.parse(entry.recordedAt);
1221
+ if (!Number.isFinite(age))
1222
+ return false;
1223
+ return lane === 'content-hash' ? age < CONTENT_HASH_DEDUP_WINDOW_MS : age < AMBIGUOUS_DEDUP_TTL_MS;
1224
+ }
1225
+ /** Prune RETIRED/EXPIRED stragglers only (never a live unretired logical
1226
+ * entry below its TTL — R4-M2 second arm). An expired content-hash entry
1227
+ * prunes exactly like a retired one (R7-M2). */
1228
+ pruneSendGuardState() {
1229
+ const nowMs = this.now().getTime();
1230
+ let pruned = false;
1231
+ for (const [key, entry] of this.ambiguousSends) {
1232
+ const age = nowMs - Date.parse(entry.recordedAt);
1233
+ if (!Number.isFinite(age))
1234
+ continue;
1235
+ const horizon = entry.lane === 'content-hash' ? CONTENT_HASH_DEDUP_WINDOW_MS : AMBIGUOUS_DEDUP_TTL_MS;
1236
+ if (age > horizon) {
1237
+ this.ambiguousSends.delete(key);
1238
+ pruned = true;
1239
+ }
1240
+ }
1241
+ if (pruned)
1242
+ this.scheduleSnapshot();
1243
+ }
1244
+ /**
1245
+ * R8-M1 lane-scoped boot conversion of crash-orphaned send-intents: an intent
1246
+ * that is the LAST word for its (conversationId, logicalSendId) PAIR (R7-M3
1247
+ * composite keying — replay already resolved every superseded pair) resolves
1248
+ * BY ITS RECORDED LANE: `logical` (beacon) converts into an ambiguous-send
1249
+ * suppressor (the outcome is genuinely unknown and the beacon has a next
1250
+ * tick); `content-hash` resolves toward RETRY (the suppressed send IS the
1251
+ * message — replay appends the missing send-intent-resolved durably, so the
1252
+ * verdict is decided once, not re-decided every boot). Staged appends land
1253
+ * POST-compose, fsynced before the registry begins serving (R9-low-3 /
1254
+ * R10-minor-1).
1255
+ */
1256
+ convertOrphanedSendIntents() {
1257
+ if (this.sendIntents.size === 0)
1258
+ return;
1259
+ for (const [key, intent] of [...this.sendIntents]) {
1260
+ const sep = key.indexOf('|'); // conversationId prefix is numeric — the FIRST '|' always delimits (§3.4)
1261
+ if (sep <= 0) {
1262
+ this.sendIntents.delete(key);
1263
+ continue;
1264
+ }
1265
+ const conversationId = Number(key.slice(0, sep));
1266
+ const logicalSendId = key.slice(sep + 1);
1267
+ if (!Number.isSafeInteger(conversationId) || !logicalSendId) {
1268
+ this.sendIntents.delete(key);
1269
+ continue;
1270
+ }
1271
+ if (intent.lane === 'logical') {
1272
+ this.appendJournal({ op: 'ambiguous-send', conversationId, logicalSendId, lane: 'logical' }, { fsync: true });
1273
+ this.ambiguousSends.set(key, { recordedAt: this.now().toISOString(), lane: 'logical' });
1274
+ }
1275
+ else {
1276
+ this.appendJournal({ op: 'send-intent-resolved', conversationId, logicalSendId }, { fsync: true });
1277
+ }
1278
+ this.sendIntents.delete(key);
1279
+ }
1280
+ this.scheduleSnapshot();
1281
+ }
1282
+ // ─────────────────────────────────────────────────────────────────────────
1283
+ // Reachability (§5.1 — increment 2 writer)
1284
+ // ─────────────────────────────────────────────────────────────────────────
1285
+ /**
1286
+ * Flip an entry's LOCAL-authoritative reachability (§5.1). IDEMPOTENT
1287
+ * (already-in-state → no write) and journaled WITHOUT fsync — it rides the
1288
+ * same batched flush, so a mass event coalesces into one write, never an
1289
+ * O(N) write storm on the failure path (scalability-F2). Returns whether the
1290
+ * state changed and whether the conversation is in the R3-minor FLAP-DAMPENED
1291
+ * state (>3 flips in 24h — the caller coalesces its dead-letter attention
1292
+ * into one per-window flap item instead of one per episode).
1293
+ */
1294
+ setReachability(id, state) {
1295
+ this.load();
1296
+ const entry = this.byId.get(this.aliases.get(id) ?? id);
1297
+ if (!entry)
1298
+ return { changed: false, dampened: false };
1299
+ const nowMs = this.now().getTime();
1300
+ const cutoff = nowMs - REACHABILITY_FLAP_WINDOW_MS;
1301
+ const flips = (this.reachabilityFlips.get(entry.id) ?? []).filter((t) => t > cutoff);
1302
+ if (entry.reachability === state) {
1303
+ this.reachabilityFlips.set(entry.id, flips);
1304
+ return { changed: false, dampened: flips.length > REACHABILITY_FLAP_THRESHOLD };
1305
+ }
1306
+ flips.push(nowMs);
1307
+ this.reachabilityFlips.set(entry.id, flips);
1308
+ entry.reachability = state;
1309
+ this.appendJournal({ op: 'reachability', id: entry.id, reachability: state }, { fsync: false });
1310
+ this.scheduleSnapshot();
1311
+ return { changed: true, dampened: flips.length > REACHABILITY_FLAP_THRESHOLD };
1312
+ }
1313
+ // ─────────────────────────────────────────────────────────────────────────
1033
1314
  // Adoption pass (§6.2)
1034
1315
  // ─────────────────────────────────────────────────────────────────────────
1035
1316
  /**
@@ -1117,6 +1398,12 @@ export class ConversationRegistry {
1117
1398
  journalQuarantinedAt: this.journalQuarantinedAt,
1118
1399
  durabilityIncidents: this.durabilityIncidents,
1119
1400
  },
1401
+ // §5.0(a)/§3.5.2 increment-2 observability.
1402
+ sendGuard: {
1403
+ unretiredEntries: this.ambiguousSends.size,
1404
+ unresolvedIntents: this.sendIntents.size,
1405
+ bindPins: this.bindPins.size,
1406
+ },
1120
1407
  // Snapshot-suspension observability (R11-low-1).
1121
1408
  snapshotSuspended: this.unappliedUnknownOps.length > 0,
1122
1409
  firstUnappliedUnknownSeq: this.unappliedUnknownOps[0]?.seq ?? null,