instar 1.3.18 → 1.3.19

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 (95) hide show
  1. package/dist/commands/server.d.ts.map +1 -1
  2. package/dist/commands/server.js +104 -6
  3. package/dist/commands/server.js.map +1 -1
  4. package/dist/core/FencedLease.d.ts +126 -0
  5. package/dist/core/FencedLease.d.ts.map +1 -0
  6. package/dist/core/FencedLease.js +189 -0
  7. package/dist/core/FencedLease.js.map +1 -0
  8. package/dist/core/GitLeaseStore.d.ts +46 -0
  9. package/dist/core/GitLeaseStore.d.ts.map +1 -0
  10. package/dist/core/GitLeaseStore.js +79 -0
  11. package/dist/core/GitLeaseStore.js.map +1 -0
  12. package/dist/core/GitSync.d.ts +19 -0
  13. package/dist/core/GitSync.d.ts.map +1 -1
  14. package/dist/core/GitSync.js +77 -0
  15. package/dist/core/GitSync.js.map +1 -1
  16. package/dist/core/HandoffSentinel.d.ts +80 -0
  17. package/dist/core/HandoffSentinel.d.ts.map +1 -0
  18. package/dist/core/HandoffSentinel.js +141 -0
  19. package/dist/core/HandoffSentinel.js.map +1 -0
  20. package/dist/core/LeaseCoordinator.d.ts +141 -0
  21. package/dist/core/LeaseCoordinator.d.ts.map +1 -0
  22. package/dist/core/LeaseCoordinator.js +226 -0
  23. package/dist/core/LeaseCoordinator.js.map +1 -0
  24. package/dist/core/LiveTailBuffer.d.ts +64 -0
  25. package/dist/core/LiveTailBuffer.d.ts.map +1 -0
  26. package/dist/core/LiveTailBuffer.js +122 -0
  27. package/dist/core/LiveTailBuffer.js.map +1 -0
  28. package/dist/core/MultiMachineCoordinator.d.ts +49 -0
  29. package/dist/core/MultiMachineCoordinator.d.ts.map +1 -1
  30. package/dist/core/MultiMachineCoordinator.js +136 -0
  31. package/dist/core/MultiMachineCoordinator.js.map +1 -1
  32. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  33. package/dist/core/PostUpdateMigrator.js +24 -0
  34. package/dist/core/PostUpdateMigrator.js.map +1 -1
  35. package/dist/core/RegistrySyncDebouncer.d.ts +77 -0
  36. package/dist/core/RegistrySyncDebouncer.d.ts.map +1 -0
  37. package/dist/core/RegistrySyncDebouncer.js +131 -0
  38. package/dist/core/RegistrySyncDebouncer.js.map +1 -0
  39. package/dist/core/liveTailRedaction.d.ts +36 -0
  40. package/dist/core/liveTailRedaction.d.ts.map +1 -0
  41. package/dist/core/liveTailRedaction.js +57 -0
  42. package/dist/core/liveTailRedaction.js.map +1 -0
  43. package/dist/core/registryReplayGuard.d.ts +59 -0
  44. package/dist/core/registryReplayGuard.d.ts.map +1 -0
  45. package/dist/core/registryReplayGuard.js +86 -0
  46. package/dist/core/registryReplayGuard.js.map +1 -0
  47. package/dist/core/seamlessnessConfig.d.ts +79 -0
  48. package/dist/core/seamlessnessConfig.d.ts.map +1 -0
  49. package/dist/core/seamlessnessConfig.js +131 -0
  50. package/dist/core/seamlessnessConfig.js.map +1 -0
  51. package/dist/core/types.d.ts +117 -0
  52. package/dist/core/types.d.ts.map +1 -1
  53. package/dist/core/types.js.map +1 -1
  54. package/dist/core/wireRegistrySync.d.ts +32 -0
  55. package/dist/core/wireRegistrySync.d.ts.map +1 -0
  56. package/dist/core/wireRegistrySync.js +31 -0
  57. package/dist/core/wireRegistrySync.js.map +1 -0
  58. package/dist/feedback-factory/dispatch/dispatch.d.ts +47 -0
  59. package/dist/feedback-factory/dispatch/dispatch.d.ts.map +1 -0
  60. package/dist/feedback-factory/dispatch/dispatch.js +72 -0
  61. package/dist/feedback-factory/dispatch/dispatch.js.map +1 -0
  62. package/dist/messaging/FencedOutbox.d.ts +42 -0
  63. package/dist/messaging/FencedOutbox.d.ts.map +1 -0
  64. package/dist/messaging/FencedOutbox.js +60 -0
  65. package/dist/messaging/FencedOutbox.js.map +1 -0
  66. package/dist/messaging/MessageProcessingLedger.d.ts +105 -0
  67. package/dist/messaging/MessageProcessingLedger.d.ts.map +1 -0
  68. package/dist/messaging/MessageProcessingLedger.js +206 -0
  69. package/dist/messaging/MessageProcessingLedger.js.map +1 -0
  70. package/dist/messaging/TelegramAdapter.d.ts +18 -1
  71. package/dist/messaging/TelegramAdapter.d.ts.map +1 -1
  72. package/dist/messaging/TelegramAdapter.js +50 -0
  73. package/dist/messaging/TelegramAdapter.js.map +1 -1
  74. package/dist/scaffold/templates.d.ts.map +1 -1
  75. package/dist/scaffold/templates.js +7 -0
  76. package/dist/scaffold/templates.js.map +1 -1
  77. package/dist/server/AgentServer.d.ts.map +1 -1
  78. package/dist/server/AgentServer.js +1 -0
  79. package/dist/server/AgentServer.js.map +1 -1
  80. package/dist/server/routes.d.ts +2 -0
  81. package/dist/server/routes.d.ts.map +1 -1
  82. package/dist/server/routes.js +6 -0
  83. package/dist/server/routes.js.map +1 -1
  84. package/package.json +1 -1
  85. package/src/data/builtin-manifest.json +64 -64
  86. package/src/scaffold/templates.ts +7 -0
  87. package/upgrades/1.3.19.md +23 -0
  88. package/upgrades/side-effects/PR-BODY-cross-machine-seamlessness.md +63 -0
  89. package/upgrades/side-effects/cross-machine-seamlessness-g1-lease.md +71 -0
  90. package/upgrades/side-effects/cross-machine-seamlessness-g2-foundation.md +75 -0
  91. package/upgrades/side-effects/cross-machine-seamlessness-g3-telegram-contract.md +36 -0
  92. package/upgrades/side-effects/cross-machine-seamlessness-g3a-ledger-handoff.md +56 -0
  93. package/upgrades/side-effects/cross-machine-seamlessness-g3bc-livetail-outbox.md +51 -0
  94. package/upgrades/side-effects/cross-machine-seamlessness-p4-awareness-observability.md +54 -0
  95. package/upgrades/side-effects/feedback-factory-dispatch.md +31 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wireRegistrySync.js","sourceRoot":"","sources":["../../src/core/wireRegistrySync.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAkBH;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAC9B,WAAmC,EACnC,IAAuB;IAEvB,MAAM,MAAM,GAAG,CAAC,IAAa,EAAE,EAAW,EAAE,EAAE,CAC5C,IAAI,CAAC,iBAAiB,CAAC,cAAc,MAAM,CAAC,IAAI,CAAC,KAAK,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;IACtE,MAAM,OAAO,GAAG,CAAC,KAAc,EAAE,EAAE,CAAC,IAAI,CAAC,iBAAiB,CAAC,cAAc,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IAE1F,WAAW,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;IACrC,WAAW,CAAC,EAAE,CAAC,kBAAkB,EAAE,OAAO,CAAC,CAAC;IAE5C,OAAO;QACL,WAAW,EAAE,CAAC,YAAY,EAAE,kBAAkB,CAAC;QAC/C,MAAM;YACJ,WAAW,CAAC,GAAG,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;YACtC,WAAW,CAAC,GAAG,CAAC,kBAAkB,EAAE,OAAO,CAAC,CAAC;QAC/C,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * dispatch.ts — framework-agnostic port of the dispatch (guidance-out) logic.
3
+ *
4
+ * Ports the pure logic of `the-portal/pages/api/instar/dispatches/index.ts` out of
5
+ * the Next.js handler: the dispatch type/priority vocab, the semver comparison used
6
+ * for version-compat filtering (an agent only receives a dispatch whose
7
+ * [minVersion, maxVersion] window includes the agent's version), the list query
8
+ * filter (since/type/version), and the create-dedup title normalization. The HTTP
9
+ * wiring + storage are excluded (the app-placement decision); this is the reusable
10
+ * core. Reference is TypeScript, so equivalence is by faithful transcription +
11
+ * exhaustive both-sides-of-boundary tests (not a cross-runtime parity harness).
12
+ */
13
+ export declare const DISPATCH_TYPES: readonly ["strategy", "behavioral", "lesson", "configuration", "security", "action"];
14
+ export type DispatchType = typeof DISPATCH_TYPES[number];
15
+ export declare const DISPATCH_PRIORITIES: readonly ["low", "normal", "high", "critical"];
16
+ export type DispatchPriority = typeof DISPATCH_PRIORITIES[number];
17
+ export declare const SEMVER_RE: RegExp;
18
+ export declare function isValidDispatchType(type: unknown): type is DispatchType;
19
+ export declare function isValidDispatchPriority(p: unknown): p is DispatchPriority;
20
+ /** Port of parseVersion: leading major.minor.patch → [n,n,n]; [0,0,0] if unparseable. */
21
+ export declare function parseVersion(v: string): [number, number, number];
22
+ /** Port of isVersionGte: version >= min (major.minor.patch, equal counts as gte). */
23
+ export declare function isVersionGte(version: string, min: string): boolean;
24
+ /** Port of isVersionLte: version <= max (equal counts as lte). */
25
+ export declare function isVersionLte(version: string, max: string): boolean;
26
+ export interface DispatchRecord {
27
+ dispatchId: string;
28
+ type: string;
29
+ title: string;
30
+ content?: string;
31
+ priority?: string;
32
+ minVersion?: string | null;
33
+ maxVersion?: string | null;
34
+ active?: boolean;
35
+ createdAt?: string;
36
+ [k: string]: unknown;
37
+ }
38
+ /**
39
+ * Port of handleList's version-compat filter: a dispatch is included only if the
40
+ * agent's version is within its [minVersion, maxVersion] window. Mirrors the
41
+ * reference — applied only when the agent sent a valid-semver version; otherwise
42
+ * all candidates pass (the caller gates on SEMVER_RE before calling).
43
+ */
44
+ export declare function filterDispatchesForVersion(dispatches: DispatchRecord[], version: string): DispatchRecord[];
45
+ /** Port of the create-path title normalization used for dedup: trim + cap at 500 chars. */
46
+ export declare function normalizeDispatchTitle(title: string): string;
47
+ //# sourceMappingURL=dispatch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../../../src/feedback-factory/dispatch/dispatch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,eAAO,MAAM,cAAc,sFAAuF,CAAC;AACnH,MAAM,MAAM,YAAY,GAAG,OAAO,cAAc,CAAC,MAAM,CAAC,CAAC;AACzD,eAAO,MAAM,mBAAmB,gDAAiD,CAAC;AAClF,MAAM,MAAM,gBAAgB,GAAG,OAAO,mBAAmB,CAAC,MAAM,CAAC,CAAC;AAElE,eAAO,MAAM,SAAS,QAA8B,CAAC;AAErD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,IAAI,YAAY,CAEvE;AACD,wBAAgB,uBAAuB,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,IAAI,gBAAgB,CAEzE;AAED,yFAAyF;AACzF,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAIhE;AAED,qFAAqF;AACrF,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAQlE;AAED,kEAAkE;AAClE,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAQlE;AAED,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CACtB;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,UAAU,EAAE,cAAc,EAAE,EAAE,OAAO,EAAE,MAAM,GAAG,cAAc,EAAE,CAM1G;AAED,2FAA2F;AAC3F,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE5D"}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * dispatch.ts — framework-agnostic port of the dispatch (guidance-out) logic.
3
+ *
4
+ * Ports the pure logic of `the-portal/pages/api/instar/dispatches/index.ts` out of
5
+ * the Next.js handler: the dispatch type/priority vocab, the semver comparison used
6
+ * for version-compat filtering (an agent only receives a dispatch whose
7
+ * [minVersion, maxVersion] window includes the agent's version), the list query
8
+ * filter (since/type/version), and the create-dedup title normalization. The HTTP
9
+ * wiring + storage are excluded (the app-placement decision); this is the reusable
10
+ * core. Reference is TypeScript, so equivalence is by faithful transcription +
11
+ * exhaustive both-sides-of-boundary tests (not a cross-runtime parity harness).
12
+ */
13
+ export const DISPATCH_TYPES = ['strategy', 'behavioral', 'lesson', 'configuration', 'security', 'action'];
14
+ export const DISPATCH_PRIORITIES = ['low', 'normal', 'high', 'critical'];
15
+ export const SEMVER_RE = /^\d+\.\d+\.\d+(-[\w.]+)?$/;
16
+ export function isValidDispatchType(type) {
17
+ return typeof type === 'string' && DISPATCH_TYPES.includes(type);
18
+ }
19
+ export function isValidDispatchPriority(p) {
20
+ return typeof p === 'string' && DISPATCH_PRIORITIES.includes(p);
21
+ }
22
+ /** Port of parseVersion: leading major.minor.patch → [n,n,n]; [0,0,0] if unparseable. */
23
+ export function parseVersion(v) {
24
+ const match = /^(\d+)\.(\d+)\.(\d+)/.exec(v);
25
+ if (!match)
26
+ return [0, 0, 0];
27
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
28
+ }
29
+ /** Port of isVersionGte: version >= min (major.minor.patch, equal counts as gte). */
30
+ export function isVersionGte(version, min) {
31
+ const v = parseVersion(version);
32
+ const m = parseVersion(min);
33
+ for (let i = 0; i < 3; i++) {
34
+ if (v[i] > m[i])
35
+ return true;
36
+ if (v[i] < m[i])
37
+ return false;
38
+ }
39
+ return true; // equal
40
+ }
41
+ /** Port of isVersionLte: version <= max (equal counts as lte). */
42
+ export function isVersionLte(version, max) {
43
+ const v = parseVersion(version);
44
+ const m = parseVersion(max);
45
+ for (let i = 0; i < 3; i++) {
46
+ if (v[i] < m[i])
47
+ return true;
48
+ if (v[i] > m[i])
49
+ return false;
50
+ }
51
+ return true; // equal
52
+ }
53
+ /**
54
+ * Port of handleList's version-compat filter: a dispatch is included only if the
55
+ * agent's version is within its [minVersion, maxVersion] window. Mirrors the
56
+ * reference — applied only when the agent sent a valid-semver version; otherwise
57
+ * all candidates pass (the caller gates on SEMVER_RE before calling).
58
+ */
59
+ export function filterDispatchesForVersion(dispatches, version) {
60
+ return dispatches.filter((d) => {
61
+ if (d.minVersion && !isVersionGte(version, d.minVersion))
62
+ return false;
63
+ if (d.maxVersion && !isVersionLte(version, d.maxVersion))
64
+ return false;
65
+ return true;
66
+ });
67
+ }
68
+ /** Port of the create-path title normalization used for dedup: trim + cap at 500 chars. */
69
+ export function normalizeDispatchTitle(title) {
70
+ return title.trim().slice(0, 500);
71
+ }
72
+ //# sourceMappingURL=dispatch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dispatch.js","sourceRoot":"","sources":["../../../src/feedback-factory/dispatch/dispatch.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * FencedOutbox — fencing-token-gated outbound reply path (spec §8 G3a).
3
+ *
4
+ * "No duplicate replies" is enforced here STRUCTURALLY, not by assuming a single
5
+ * consumer. A reply is sent only while this machine holds the lease at the epoch
6
+ * the reply was stamped under; a fenced (stale-epoch) machine's in-flight reply
7
+ * is SUPPRESSED at the send path. The reply is committed to the message ledger
8
+ * with a deterministic idempotency key (hash(dedupeKey + replyIndex)) so any
9
+ * machine re-running the same event reproduces it identically — and the ledger
10
+ * recognizes an already-committed reply and refuses to re-send.
11
+ *
12
+ * On a channel with native outbound dedup the idempotency key makes a re-send a
13
+ * no-op; on a channel without it (Telegram), the dual-medium reply_committed
14
+ * marker (propagated by the sync layer) is what prevents a failover re-send.
15
+ */
16
+ import { MessageProcessingLedger } from './MessageProcessingLedger.js';
17
+ export interface FencedOutboxDeps {
18
+ ledger: MessageProcessingLedger;
19
+ /** The current effective fencing epoch. */
20
+ currentEpoch: () => number;
21
+ /** Whether this machine structurally holds the lease right now. */
22
+ holdsLease: () => boolean;
23
+ logger?: (msg: string) => void;
24
+ }
25
+ export interface SendOutcome {
26
+ sent: boolean;
27
+ suppressed: boolean;
28
+ reason: 'sent' | 'already-replied' | 'fenced-no-lease' | 'fenced-stale-epoch' | 'send-failed';
29
+ idempotencyKey: string;
30
+ }
31
+ export declare class FencedOutbox {
32
+ private readonly d;
33
+ constructor(deps: FencedOutboxDeps);
34
+ private log;
35
+ /**
36
+ * Send a reply for `dedupeKey`, stamped under `stampedEpoch` (the epoch the
37
+ * turn began processing under). Returns the outcome. The actual platform send
38
+ * is `sendFn`; it is invoked ONLY after the fencing checks pass.
39
+ */
40
+ send(dedupeKey: string, replyIndex: number, stampedEpoch: number, sendFn: () => Promise<void>): Promise<SendOutcome>;
41
+ }
42
+ //# sourceMappingURL=FencedOutbox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FencedOutbox.d.ts","sourceRoot":"","sources":["../../src/messaging/FencedOutbox.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,uBAAuB,EAA8B,MAAM,8BAA8B,CAAC;AAEnG,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,uBAAuB,CAAC;IAChC,2CAA2C;IAC3C,YAAY,EAAE,MAAM,MAAM,CAAC;IAC3B,mEAAmE;IACnE,UAAU,EAAE,MAAM,OAAO,CAAC;IAC1B,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAChC;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,OAAO,CAAC;IACd,UAAU,EAAE,OAAO,CAAC;IACpB,MAAM,EAAE,MAAM,GAAG,iBAAiB,GAAG,iBAAiB,GAAG,oBAAoB,GAAG,aAAa,CAAC;IAC9F,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAmB;gBAEzB,IAAI,EAAE,gBAAgB;IAIlC,OAAO,CAAC,GAAG;IAIX;;;;OAIG;IACG,IAAI,CACR,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAC1B,OAAO,CAAC,WAAW,CAAC;CAgCxB"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * FencedOutbox — fencing-token-gated outbound reply path (spec §8 G3a).
3
+ *
4
+ * "No duplicate replies" is enforced here STRUCTURALLY, not by assuming a single
5
+ * consumer. A reply is sent only while this machine holds the lease at the epoch
6
+ * the reply was stamped under; a fenced (stale-epoch) machine's in-flight reply
7
+ * is SUPPRESSED at the send path. The reply is committed to the message ledger
8
+ * with a deterministic idempotency key (hash(dedupeKey + replyIndex)) so any
9
+ * machine re-running the same event reproduces it identically — and the ledger
10
+ * recognizes an already-committed reply and refuses to re-send.
11
+ *
12
+ * On a channel with native outbound dedup the idempotency key makes a re-send a
13
+ * no-op; on a channel without it (Telegram), the dual-medium reply_committed
14
+ * marker (propagated by the sync layer) is what prevents a failover re-send.
15
+ */
16
+ import { computeReplyIdempotencyKey } from './MessageProcessingLedger.js';
17
+ export class FencedOutbox {
18
+ d;
19
+ constructor(deps) {
20
+ this.d = deps;
21
+ }
22
+ log(m) {
23
+ this.d.logger?.(`[outbox] ${m}`);
24
+ }
25
+ /**
26
+ * Send a reply for `dedupeKey`, stamped under `stampedEpoch` (the epoch the
27
+ * turn began processing under). Returns the outcome. The actual platform send
28
+ * is `sendFn`; it is invoked ONLY after the fencing checks pass.
29
+ */
30
+ async send(dedupeKey, replyIndex, stampedEpoch, sendFn) {
31
+ const idempotencyKey = computeReplyIdempotencyKey(dedupeKey, replyIndex);
32
+ // Idempotency: if a reply was already committed (locally or via a remote
33
+ // dual-medium marker), do not send again.
34
+ if (this.d.ledger.isActedOn(dedupeKey)) {
35
+ this.log(`already replied to ${dedupeKey} — suppressing`);
36
+ return { sent: false, suppressed: true, reason: 'already-replied', idempotencyKey };
37
+ }
38
+ // Fencing: only the current lease holder, at the stamped epoch, may send.
39
+ if (!this.d.holdsLease()) {
40
+ this.log(`fenced (no lease) — suppressing reply to ${dedupeKey}`);
41
+ return { sent: false, suppressed: true, reason: 'fenced-no-lease', idempotencyKey };
42
+ }
43
+ const epoch = this.d.currentEpoch();
44
+ if (epoch !== stampedEpoch) {
45
+ this.log(`fenced (stale epoch ${stampedEpoch} != current ${epoch}) — suppressing reply to ${dedupeKey}`);
46
+ return { sent: false, suppressed: true, reason: 'fenced-stale-epoch', idempotencyKey };
47
+ }
48
+ try {
49
+ await sendFn();
50
+ }
51
+ catch (err) {
52
+ this.log(`send failed for ${dedupeKey}: ${err instanceof Error ? err.message : String(err)}`);
53
+ return { sent: false, suppressed: false, reason: 'send-failed', idempotencyKey };
54
+ }
55
+ // Commit the reply marker (durable, synchronous) BEFORE advancing the cursor.
56
+ this.d.ledger.commitReply(dedupeKey, idempotencyKey, stampedEpoch);
57
+ return { sent: true, suppressed: false, reason: 'sent', idempotencyKey };
58
+ }
59
+ }
60
+ //# sourceMappingURL=FencedOutbox.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FencedOutbox.js","sourceRoot":"","sources":["../../src/messaging/FencedOutbox.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAA2B,0BAA0B,EAAE,MAAM,8BAA8B,CAAC;AAkBnG,MAAM,OAAO,YAAY;IACN,CAAC,CAAmB;IAErC,YAAY,IAAsB;QAChC,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC;IAChB,CAAC;IAEO,GAAG,CAAC,CAAS;QACnB,IAAI,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;IACnC,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,IAAI,CACR,SAAiB,EACjB,UAAkB,EAClB,YAAoB,EACpB,MAA2B;QAE3B,MAAM,cAAc,GAAG,0BAA0B,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;QAEzE,yEAAyE;QACzE,0CAA0C;QAC1C,IAAI,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,SAAS,CAAC,EAAE,CAAC;YACvC,IAAI,CAAC,GAAG,CAAC,sBAAsB,SAAS,gBAAgB,CAAC,CAAC;YAC1D,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,iBAAiB,EAAE,cAAc,EAAE,CAAC;QACtF,CAAC;QAED,0EAA0E;QAC1E,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC;YACzB,IAAI,CAAC,GAAG,CAAC,4CAA4C,SAAS,EAAE,CAAC,CAAC;YAClE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,iBAAiB,EAAE,cAAc,EAAE,CAAC;QACtF,CAAC;QACD,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,YAAY,EAAE,CAAC;QACpC,IAAI,KAAK,KAAK,YAAY,EAAE,CAAC;YAC3B,IAAI,CAAC,GAAG,CAAC,uBAAuB,YAAY,eAAe,KAAK,4BAA4B,SAAS,EAAE,CAAC,CAAC;YACzG,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,oBAAoB,EAAE,cAAc,EAAE,CAAC;QACzF,CAAC;QAED,IAAI,CAAC;YACH,MAAM,MAAM,EAAE,CAAC;QACjB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,GAAG,CAAC,mBAAmB,SAAS,KAAK,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YAC9F,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,cAAc,EAAE,CAAC;QACnF,CAAC;QAED,8EAA8E;QAC9E,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,SAAS,EAAE,cAAc,EAAE,YAAY,CAAC,CAAC;QACnE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC;IAC3E,CAAC;CACF"}
@@ -0,0 +1,105 @@
1
+ /**
2
+ * MessageProcessingLedger — the no-loss / no-duplicate-reply guarantee (spec §8 G3a).
3
+ *
4
+ * A durable, SQLite-backed record of each inbound message's lifecycle:
5
+ * received → processing → reply_committed → cursor_advanced
6
+ * keyed by the adapter's `dedupeKey(rawEvent)` (Telegram update_id, Slack
7
+ * event_id/client_msg_id). Rules:
8
+ *
9
+ * - An event whose dedupeKey is already reply_committed/cursor_advanced is
10
+ * NEVER acted on again — a redelivery (Telegram retry, Slack reconnect, or a
11
+ * transfer-window overlap during a handoff) is recognized and dropped. This
12
+ * is what makes "no duplicate replies" structural, not merely "only one
13
+ * consumer".
14
+ * - The ingress cursor advances ONLY on durable completion (cursor_advanced),
15
+ * so a crash before completion replays the event (at-least-once) and the
16
+ * ledger makes the replay a no-op-or-resume (exactly-once effect).
17
+ * - Each transition is flushed to local SQLite synchronously on commit (WAL +
18
+ * NORMAL), so a same-machine crash-restart never double-acts.
19
+ * - A stuck `processing` entry past maxProcessingMs (old holder fenced
20
+ * mid-turn) is re-runnable by the current holder from its stored input.
21
+ *
22
+ * Substrate is SQLite (the proven PendingRelayStore/CommitmentTracker path) —
23
+ * NOT a new ad-hoc JSON file and NOT a git-synced blob. Schema self-initializes
24
+ * on first access (no PostUpdateMigrator step needed). Per-agent-id isolation.
25
+ */
26
+ export type LedgerState = 'received' | 'processing' | 'reply_committed' | 'cursor_advanced';
27
+ export interface LedgerEntry {
28
+ dedupeKey: string;
29
+ platform: string;
30
+ topic: string | null;
31
+ state: LedgerState;
32
+ receivedAt: string;
33
+ processingStartedAt: string | null;
34
+ replyCommittedAt: string | null;
35
+ cursorAdvancedAt: string | null;
36
+ replyIdempotencyKey: string | null;
37
+ replyEpoch: number | null;
38
+ inputSnapshot: string | null;
39
+ attempts: number;
40
+ }
41
+ export declare function resolveMessageLedgerPath(stateDir: string, agentId: string): string;
42
+ /**
43
+ * Deterministic outbound idempotency key — any machine re-running the same
44
+ * inbound event reproduces it identically (spec §8 G3a). Used so a failover
45
+ * re-send is recognized as the same reply.
46
+ */
47
+ export declare function computeReplyIdempotencyKey(dedupeKey: string, replyIndex: number): string;
48
+ export declare class MessageProcessingLedger {
49
+ private readonly db;
50
+ readonly path: string;
51
+ private constructor();
52
+ static open(agentId: string, stateDir: string): MessageProcessingLedger;
53
+ /** Open an in-memory ledger (tests). */
54
+ static openMemory(): MessageProcessingLedger;
55
+ /**
56
+ * Record an inbound event. Idempotent on dedupeKey (INSERT OR IGNORE).
57
+ * Returns whether this is the first time we've seen it + the current state.
58
+ * A caller seeing firstSeen:false with an acted-on state must DROP the event.
59
+ */
60
+ record(dedupeKey: string, opts: {
61
+ platform: string;
62
+ topic?: string | null;
63
+ input?: string;
64
+ }): {
65
+ firstSeen: boolean;
66
+ state: LedgerState;
67
+ };
68
+ /** Has this event already been acted on (reply committed or cursor advanced)? */
69
+ isActedOn(dedupeKey: string): boolean;
70
+ /**
71
+ * Claim an event for processing under the given fencing epoch. Returns false
72
+ * if it has already been acted on (drop). A stuck 'processing' entry can be
73
+ * re-claimed (the old holder was fenced); attempts is incremented.
74
+ */
75
+ beginProcessing(dedupeKey: string, epoch: number): boolean;
76
+ /**
77
+ * Commit that a reply was sent. Records the deterministic idempotency key and
78
+ * the fencing epoch it was committed under. Idempotent — committing twice is
79
+ * a no-op (the marker already exists).
80
+ */
81
+ commitReply(dedupeKey: string, replyIdempotencyKey: string, epoch: number): void;
82
+ /** Advance the ingress cursor — only call after durable reply commit. */
83
+ advanceCursor(dedupeKey: string): void;
84
+ /**
85
+ * Apply a reply_committed marker propagated from another machine (dual-medium
86
+ * marker, spec §8 G3a): if we have the entry and it's not yet acted on, mark
87
+ * it committed so a failover does not re-send. Creates the entry if unknown
88
+ * (the other machine saw it first).
89
+ */
90
+ applyRemoteReplyMarker(dedupeKey: string, opts: {
91
+ platform: string;
92
+ replyIdempotencyKey: string;
93
+ epoch: number;
94
+ topic?: string | null;
95
+ }): void;
96
+ /**
97
+ * Entries stuck in 'processing' past maxProcessingMs — eligible for re-run by
98
+ * the current lease holder from their stored input (spec §8 G3a). The old
99
+ * holder's abandoned output is discarded (the new holder re-executes).
100
+ */
101
+ reclaimStuck(maxProcessingMs: number, nowMs?: number): LedgerEntry[];
102
+ get(dedupeKey: string): LedgerEntry | null;
103
+ close(): void;
104
+ }
105
+ //# sourceMappingURL=MessageProcessingLedger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MessageProcessingLedger.d.ts","sourceRoot":"","sources":["../../src/messaging/MessageProcessingLedger.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAQH,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG,YAAY,GAAG,iBAAiB,GAAG,iBAAiB,CAAC;AAE5F,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,KAAK,EAAE,WAAW,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;CAClB;AAoBD,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAGlF;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAExF;AAED,qBAAa,uBAAuB;IAClC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAuB;IAC1C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB,OAAO;IAKP,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,uBAAuB;IAYvE,wCAAwC;IACxC,MAAM,CAAC,UAAU,IAAI,uBAAuB;IAO5C;;;;OAIG;IACH,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG;QAC5F,SAAS,EAAE,OAAO,CAAC;QACnB,KAAK,EAAE,WAAW,CAAC;KACpB;IAYD,iFAAiF;IACjF,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO;IAKrC;;;;OAIG;IACH,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO;IAc1D;;;;OAIG;IACH,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,mBAAmB,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAUhF,yEAAyE;IACzE,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAStC;;;;;OAKG;IACH,sBAAsB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,mBAAmB,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI;IAe9I;;;;OAIG;IACH,YAAY,CAAC,eAAe,EAAE,MAAM,EAAE,KAAK,GAAE,MAAmB,GAAG,WAAW,EAAE;IAahF,GAAG,CAAC,SAAS,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI;IAK1C,KAAK,IAAI,IAAI;CAGd"}
@@ -0,0 +1,206 @@
1
+ /**
2
+ * MessageProcessingLedger — the no-loss / no-duplicate-reply guarantee (spec §8 G3a).
3
+ *
4
+ * A durable, SQLite-backed record of each inbound message's lifecycle:
5
+ * received → processing → reply_committed → cursor_advanced
6
+ * keyed by the adapter's `dedupeKey(rawEvent)` (Telegram update_id, Slack
7
+ * event_id/client_msg_id). Rules:
8
+ *
9
+ * - An event whose dedupeKey is already reply_committed/cursor_advanced is
10
+ * NEVER acted on again — a redelivery (Telegram retry, Slack reconnect, or a
11
+ * transfer-window overlap during a handoff) is recognized and dropped. This
12
+ * is what makes "no duplicate replies" structural, not merely "only one
13
+ * consumer".
14
+ * - The ingress cursor advances ONLY on durable completion (cursor_advanced),
15
+ * so a crash before completion replays the event (at-least-once) and the
16
+ * ledger makes the replay a no-op-or-resume (exactly-once effect).
17
+ * - Each transition is flushed to local SQLite synchronously on commit (WAL +
18
+ * NORMAL), so a same-machine crash-restart never double-acts.
19
+ * - A stuck `processing` entry past maxProcessingMs (old holder fenced
20
+ * mid-turn) is re-runnable by the current holder from its stored input.
21
+ *
22
+ * Substrate is SQLite (the proven PendingRelayStore/CommitmentTracker path) —
23
+ * NOT a new ad-hoc JSON file and NOT a git-synced blob. Schema self-initializes
24
+ * on first access (no PostUpdateMigrator step needed). Per-agent-id isolation.
25
+ */
26
+ import Database from 'better-sqlite3';
27
+ import crypto from 'node:crypto';
28
+ import fs from 'node:fs';
29
+ import path from 'node:path';
30
+ const SCHEMA = `
31
+ CREATE TABLE IF NOT EXISTS message_ledger (
32
+ dedupe_key TEXT PRIMARY KEY,
33
+ platform TEXT NOT NULL,
34
+ topic TEXT,
35
+ state TEXT NOT NULL,
36
+ received_at TEXT NOT NULL,
37
+ processing_started_at TEXT,
38
+ reply_committed_at TEXT,
39
+ cursor_advanced_at TEXT,
40
+ reply_idempotency_key TEXT,
41
+ reply_epoch INTEGER,
42
+ input_snapshot TEXT,
43
+ attempts INTEGER NOT NULL DEFAULT 0
44
+ );
45
+ CREATE INDEX IF NOT EXISTS idx_message_ledger_state ON message_ledger(state);
46
+ `;
47
+ export function resolveMessageLedgerPath(stateDir, agentId) {
48
+ const safe = agentId.replace(/[^A-Za-z0-9._-]/g, '_') || 'default';
49
+ return path.join(stateDir, 'state', `message-ledger.${safe}.sqlite`);
50
+ }
51
+ /**
52
+ * Deterministic outbound idempotency key — any machine re-running the same
53
+ * inbound event reproduces it identically (spec §8 G3a). Used so a failover
54
+ * re-send is recognized as the same reply.
55
+ */
56
+ export function computeReplyIdempotencyKey(dedupeKey, replyIndex) {
57
+ return crypto.createHash('sha256').update(`${dedupeKey}::${replyIndex}`).digest('hex').slice(0, 32);
58
+ }
59
+ export class MessageProcessingLedger {
60
+ db;
61
+ path;
62
+ constructor(db, dbPath) {
63
+ this.db = db;
64
+ this.path = dbPath;
65
+ }
66
+ static open(agentId, stateDir) {
67
+ const dbPath = resolveMessageLedgerPath(stateDir, agentId);
68
+ fs.mkdirSync(path.dirname(dbPath), { recursive: true });
69
+ const db = new Database(dbPath);
70
+ try {
71
+ fs.chmodSync(dbPath, 0o600);
72
+ }
73
+ catch { /* best-effort */ }
74
+ db.pragma('journal_mode = WAL');
75
+ db.pragma('synchronous = NORMAL');
76
+ db.pragma('busy_timeout = 5000');
77
+ db.exec(SCHEMA);
78
+ return new MessageProcessingLedger(db, dbPath);
79
+ }
80
+ /** Open an in-memory ledger (tests). */
81
+ static openMemory() {
82
+ const db = new Database(':memory:');
83
+ db.pragma('busy_timeout = 5000');
84
+ db.exec(SCHEMA);
85
+ return new MessageProcessingLedger(db, ':memory:');
86
+ }
87
+ /**
88
+ * Record an inbound event. Idempotent on dedupeKey (INSERT OR IGNORE).
89
+ * Returns whether this is the first time we've seen it + the current state.
90
+ * A caller seeing firstSeen:false with an acted-on state must DROP the event.
91
+ */
92
+ record(dedupeKey, opts) {
93
+ const now = new Date().toISOString();
94
+ const info = this.db
95
+ .prepare(`INSERT OR IGNORE INTO message_ledger (dedupe_key, platform, topic, state, received_at, input_snapshot)
96
+ VALUES (?, ?, ?, 'received', ?, ?)`)
97
+ .run(dedupeKey, opts.platform, opts.topic ?? null, now, opts.input ?? null);
98
+ const row = this.get(dedupeKey);
99
+ return { firstSeen: info.changes === 1, state: row.state };
100
+ }
101
+ /** Has this event already been acted on (reply committed or cursor advanced)? */
102
+ isActedOn(dedupeKey) {
103
+ const row = this.get(dedupeKey);
104
+ return !!row && (row.state === 'reply_committed' || row.state === 'cursor_advanced');
105
+ }
106
+ /**
107
+ * Claim an event for processing under the given fencing epoch. Returns false
108
+ * if it has already been acted on (drop). A stuck 'processing' entry can be
109
+ * re-claimed (the old holder was fenced); attempts is incremented.
110
+ */
111
+ beginProcessing(dedupeKey, epoch) {
112
+ const row = this.get(dedupeKey);
113
+ if (!row)
114
+ return false;
115
+ if (row.state === 'reply_committed' || row.state === 'cursor_advanced')
116
+ return false;
117
+ this.db
118
+ .prepare(`UPDATE message_ledger
119
+ SET state = 'processing', processing_started_at = ?, reply_epoch = ?, attempts = attempts + 1
120
+ WHERE dedupe_key = ?`)
121
+ .run(new Date().toISOString(), epoch, dedupeKey);
122
+ return true;
123
+ }
124
+ /**
125
+ * Commit that a reply was sent. Records the deterministic idempotency key and
126
+ * the fencing epoch it was committed under. Idempotent — committing twice is
127
+ * a no-op (the marker already exists).
128
+ */
129
+ commitReply(dedupeKey, replyIdempotencyKey, epoch) {
130
+ this.db
131
+ .prepare(`UPDATE message_ledger
132
+ SET state = 'reply_committed', reply_committed_at = ?, reply_idempotency_key = ?, reply_epoch = ?
133
+ WHERE dedupe_key = ? AND state NOT IN ('reply_committed','cursor_advanced')`)
134
+ .run(new Date().toISOString(), replyIdempotencyKey, epoch, dedupeKey);
135
+ }
136
+ /** Advance the ingress cursor — only call after durable reply commit. */
137
+ advanceCursor(dedupeKey) {
138
+ this.db
139
+ .prepare(`UPDATE message_ledger SET state = 'cursor_advanced', cursor_advanced_at = ?
140
+ WHERE dedupe_key = ? AND state = 'reply_committed'`)
141
+ .run(new Date().toISOString(), dedupeKey);
142
+ }
143
+ /**
144
+ * Apply a reply_committed marker propagated from another machine (dual-medium
145
+ * marker, spec §8 G3a): if we have the entry and it's not yet acted on, mark
146
+ * it committed so a failover does not re-send. Creates the entry if unknown
147
+ * (the other machine saw it first).
148
+ */
149
+ applyRemoteReplyMarker(dedupeKey, opts) {
150
+ const now = new Date().toISOString();
151
+ this.db
152
+ .prepare(`INSERT INTO message_ledger (dedupe_key, platform, topic, state, received_at, reply_committed_at, reply_idempotency_key, reply_epoch)
153
+ VALUES (?, ?, ?, 'reply_committed', ?, ?, ?, ?)
154
+ ON CONFLICT(dedupe_key) DO UPDATE SET
155
+ state = CASE WHEN message_ledger.state IN ('reply_committed','cursor_advanced') THEN message_ledger.state ELSE 'reply_committed' END,
156
+ reply_committed_at = COALESCE(message_ledger.reply_committed_at, excluded.reply_committed_at),
157
+ reply_idempotency_key = COALESCE(message_ledger.reply_idempotency_key, excluded.reply_idempotency_key),
158
+ reply_epoch = COALESCE(message_ledger.reply_epoch, excluded.reply_epoch)`)
159
+ .run(dedupeKey, opts.platform, opts.topic ?? null, now, now, opts.replyIdempotencyKey, opts.epoch);
160
+ }
161
+ /**
162
+ * Entries stuck in 'processing' past maxProcessingMs — eligible for re-run by
163
+ * the current lease holder from their stored input (spec §8 G3a). The old
164
+ * holder's abandoned output is discarded (the new holder re-executes).
165
+ */
166
+ reclaimStuck(maxProcessingMs, nowMs = Date.now()) {
167
+ const rows = this.db
168
+ .prepare(`SELECT * FROM message_ledger WHERE state = 'processing'`)
169
+ .all();
170
+ return rows
171
+ .map(rowToEntry)
172
+ .filter((e) => {
173
+ if (!e.processingStartedAt)
174
+ return false;
175
+ const startedMs = Date.parse(e.processingStartedAt);
176
+ return !Number.isNaN(startedMs) && nowMs - startedMs > maxProcessingMs;
177
+ });
178
+ }
179
+ get(dedupeKey) {
180
+ const row = this.db.prepare(`SELECT * FROM message_ledger WHERE dedupe_key = ?`).get(dedupeKey);
181
+ return row ? rowToEntry(row) : null;
182
+ }
183
+ close() {
184
+ try {
185
+ this.db.close();
186
+ }
187
+ catch { /* best-effort */ }
188
+ }
189
+ }
190
+ function rowToEntry(row) {
191
+ return {
192
+ dedupeKey: row.dedupe_key,
193
+ platform: row.platform,
194
+ topic: row.topic ?? null,
195
+ state: row.state,
196
+ receivedAt: row.received_at,
197
+ processingStartedAt: row.processing_started_at ?? null,
198
+ replyCommittedAt: row.reply_committed_at ?? null,
199
+ cursorAdvancedAt: row.cursor_advanced_at ?? null,
200
+ replyIdempotencyKey: row.reply_idempotency_key ?? null,
201
+ replyEpoch: row.reply_epoch ?? null,
202
+ inputSnapshot: row.input_snapshot ?? null,
203
+ attempts: row.attempts ?? 0,
204
+ };
205
+ }
206
+ //# sourceMappingURL=MessageProcessingLedger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MessageProcessingLedger.js","sourceRoot":"","sources":["../../src/messaging/MessageProcessingLedger.ts"],"names":[],"mappings":"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"}
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * No external dependencies — uses native fetch for Telegram API calls.
10
10
  */
11
- import type { MessagingAdapter, Message, OutgoingMessage, IntelligenceProvider } from '../core/types.js';
11
+ import type { MessagingAdapter, Message, OutgoingMessage, IntelligenceProvider, IngressPosition } from '../core/types.js';
12
12
  import { NotificationBatcher, NotificationTier } from './NotificationBatcher.js';
13
13
  import type { ContentValidationConfig } from './TopicContentValidator.js';
14
14
  import { MessagingEventBus } from './shared/MessagingEventBus.js';
@@ -320,6 +320,23 @@ export declare class TelegramAdapter implements MessagingAdapter {
320
320
  private registerSharedCommands;
321
321
  start(): Promise<void>;
322
322
  stop(): Promise<void>;
323
+ /**
324
+ * Stable provider-level identity for an inbound update — the Telegram
325
+ * update_id. Used by the message-processing ledger so a redelivered update
326
+ * (retry / reconnect / transfer-window overlap) is recognized and not
327
+ * re-acted-on.
328
+ */
329
+ dedupeKey(rawEvent: unknown): string;
330
+ /** The durable resumable position — the long-poll update_id offset. */
331
+ getIngressPosition(): IngressPosition;
332
+ /**
333
+ * Stop the inbound loop deterministically and return the durable position
334
+ * AFTER the stop (the persisted offset), so a handoff resumes from exactly
335
+ * where this machine left off — never replaying or skipping.
336
+ */
337
+ stopConsuming(): Promise<IngressPosition>;
338
+ /** Resume the inbound loop from exactly the given position. */
339
+ resumeConsuming(position: IngressPosition): Promise<void>;
323
340
  send(message: OutgoingMessage): Promise<SendResult>;
324
341
  /**
325
342
  * Log an inbound user message that arrived via an external path (e.g. Lifeline