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.
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +104 -6
- package/dist/commands/server.js.map +1 -1
- package/dist/core/FencedLease.d.ts +126 -0
- package/dist/core/FencedLease.d.ts.map +1 -0
- package/dist/core/FencedLease.js +189 -0
- package/dist/core/FencedLease.js.map +1 -0
- package/dist/core/GitLeaseStore.d.ts +46 -0
- package/dist/core/GitLeaseStore.d.ts.map +1 -0
- package/dist/core/GitLeaseStore.js +79 -0
- package/dist/core/GitLeaseStore.js.map +1 -0
- package/dist/core/GitSync.d.ts +19 -0
- package/dist/core/GitSync.d.ts.map +1 -1
- package/dist/core/GitSync.js +77 -0
- package/dist/core/GitSync.js.map +1 -1
- package/dist/core/HandoffSentinel.d.ts +80 -0
- package/dist/core/HandoffSentinel.d.ts.map +1 -0
- package/dist/core/HandoffSentinel.js +141 -0
- package/dist/core/HandoffSentinel.js.map +1 -0
- package/dist/core/LeaseCoordinator.d.ts +141 -0
- package/dist/core/LeaseCoordinator.d.ts.map +1 -0
- package/dist/core/LeaseCoordinator.js +226 -0
- package/dist/core/LeaseCoordinator.js.map +1 -0
- package/dist/core/LiveTailBuffer.d.ts +64 -0
- package/dist/core/LiveTailBuffer.d.ts.map +1 -0
- package/dist/core/LiveTailBuffer.js +122 -0
- package/dist/core/LiveTailBuffer.js.map +1 -0
- package/dist/core/MultiMachineCoordinator.d.ts +49 -0
- package/dist/core/MultiMachineCoordinator.d.ts.map +1 -1
- package/dist/core/MultiMachineCoordinator.js +136 -0
- package/dist/core/MultiMachineCoordinator.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +24 -0
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/RegistrySyncDebouncer.d.ts +77 -0
- package/dist/core/RegistrySyncDebouncer.d.ts.map +1 -0
- package/dist/core/RegistrySyncDebouncer.js +131 -0
- package/dist/core/RegistrySyncDebouncer.js.map +1 -0
- package/dist/core/liveTailRedaction.d.ts +36 -0
- package/dist/core/liveTailRedaction.d.ts.map +1 -0
- package/dist/core/liveTailRedaction.js +57 -0
- package/dist/core/liveTailRedaction.js.map +1 -0
- package/dist/core/registryReplayGuard.d.ts +59 -0
- package/dist/core/registryReplayGuard.d.ts.map +1 -0
- package/dist/core/registryReplayGuard.js +86 -0
- package/dist/core/registryReplayGuard.js.map +1 -0
- package/dist/core/seamlessnessConfig.d.ts +79 -0
- package/dist/core/seamlessnessConfig.d.ts.map +1 -0
- package/dist/core/seamlessnessConfig.js +131 -0
- package/dist/core/seamlessnessConfig.js.map +1 -0
- package/dist/core/types.d.ts +117 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/core/wireRegistrySync.d.ts +32 -0
- package/dist/core/wireRegistrySync.d.ts.map +1 -0
- package/dist/core/wireRegistrySync.js +31 -0
- package/dist/core/wireRegistrySync.js.map +1 -0
- package/dist/feedback-factory/dispatch/dispatch.d.ts +47 -0
- package/dist/feedback-factory/dispatch/dispatch.d.ts.map +1 -0
- package/dist/feedback-factory/dispatch/dispatch.js +72 -0
- package/dist/feedback-factory/dispatch/dispatch.js.map +1 -0
- package/dist/messaging/FencedOutbox.d.ts +42 -0
- package/dist/messaging/FencedOutbox.d.ts.map +1 -0
- package/dist/messaging/FencedOutbox.js +60 -0
- package/dist/messaging/FencedOutbox.js.map +1 -0
- package/dist/messaging/MessageProcessingLedger.d.ts +105 -0
- package/dist/messaging/MessageProcessingLedger.d.ts.map +1 -0
- package/dist/messaging/MessageProcessingLedger.js +206 -0
- package/dist/messaging/MessageProcessingLedger.js.map +1 -0
- package/dist/messaging/TelegramAdapter.d.ts +18 -1
- package/dist/messaging/TelegramAdapter.d.ts.map +1 -1
- package/dist/messaging/TelegramAdapter.js +50 -0
- package/dist/messaging/TelegramAdapter.js.map +1 -1
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +7 -0
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +1 -0
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/routes.d.ts +2 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +6 -0
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +64 -64
- package/src/scaffold/templates.ts +7 -0
- package/upgrades/1.3.19.md +23 -0
- package/upgrades/side-effects/PR-BODY-cross-machine-seamlessness.md +63 -0
- package/upgrades/side-effects/cross-machine-seamlessness-g1-lease.md +71 -0
- package/upgrades/side-effects/cross-machine-seamlessness-g2-foundation.md +75 -0
- package/upgrades/side-effects/cross-machine-seamlessness-g3-telegram-contract.md +36 -0
- package/upgrades/side-effects/cross-machine-seamlessness-g3a-ledger-handoff.md +56 -0
- package/upgrades/side-effects/cross-machine-seamlessness-g3bc-livetail-outbox.md +51 -0
- package/upgrades/side-effects/cross-machine-seamlessness-p4-awareness-observability.md +54 -0
- 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
|