macp-sdk-typescript 0.7.1 → 0.9.0
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/CHANGELOG.md +94 -0
- package/README.md +1 -1
- package/dist/agent/transports.d.ts +30 -5
- package/dist/agent/transports.js +103 -13
- package/dist/agent/types.d.ts +12 -0
- package/dist/client.d.ts +7 -3
- package/dist/client.js +7 -3
- package/dist/projections/base.d.ts +149 -0
- package/dist/projections/base.js +126 -0
- package/dist/projections/decision.d.ts +43 -0
- package/dist/projections/decision.js +92 -1
- package/dist/projections/handoff.d.ts +42 -0
- package/dist/projections/handoff.js +95 -4
- package/dist/projections/proposal.d.ts +72 -0
- package/dist/projections/proposal.js +97 -4
- package/dist/projections/quorum.d.ts +43 -0
- package/dist/projections/quorum.js +91 -5
- package/dist/projections/task.d.ts +42 -0
- package/dist/projections/task.js +84 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,100 @@ All notable changes to `macp-sdk-typescript` are documented here. The format
|
|
|
4
4
|
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the
|
|
5
5
|
project uses [Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [0.9.0](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/compare/v0.8.0...v0.9.0) (2026-09-01)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### ⚠ BREAKING CHANGES
|
|
11
|
+
|
|
12
|
+
* **projections:** ProposalProjection.isAccepted()/acceptedProposal(), TaskProjection's `assignee` field, and HandoffProjection's `status` field can now derive different values than before for a transcript containing the message sequences described above. Conforming transcripts (no superseded accept, no second TaskAccept/contradictory handoff accept-decline) are unaffected.
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
|
|
16
|
+
* **agent:** don't count an envelope into the resume cursor until it's asked for again ([#72](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/72)) ([f1b7015](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/commit/f1b70155cb3293bc7f36c0389cbf880359a01e58)), closes [#66](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/66)
|
|
17
|
+
* **agent:** resume GrpcTransportAdapter from its own cursor, and count distinct envelopes ([#65](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/65)) ([7535763](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/commit/75357632351addde3a32a61b1200e823f2ef70da))
|
|
18
|
+
* **projections:** enforce cardinality rules issues [#59](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/59)/[#60](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/60) said didn't exist ([#68](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/68)) ([956d881](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/commit/956d881e19591bcd9a04c6496bff07213d1ee6d3))
|
|
19
|
+
|
|
20
|
+
## [Unreleased]
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- **BREAKING: four projection sites now enforce the cardinality rule their
|
|
25
|
+
governing RFC section actually states, instead of last-write-wins.**
|
|
26
|
+
Issues #59 and #60 argued that no cardinality/ordering site in the
|
|
27
|
+
built-in projections had a normative rule behind it; a full re-read of the
|
|
28
|
+
four mode RFCs plus RFC-MACP-0001 found governing rules for five of the
|
|
29
|
+
ten sites they raised, three of which were shipped conformance
|
|
30
|
+
violations:
|
|
31
|
+
- `ProposalProjection` (breaking): a later `Accept` from a participant now
|
|
32
|
+
supersedes their earlier one (RFC-MACP-0008 §5 rule 5, `:70`), so
|
|
33
|
+
`isAccepted` and `acceptedProposal()` read the live acceptance set
|
|
34
|
+
instead of raw accept history. Previously `acceptedProposal()` could
|
|
35
|
+
return `undefined` after a legal re-accept, where the RFC's
|
|
36
|
+
determinism clause (§7, `:89`) makes the accepted proposal unambiguous.
|
|
37
|
+
- `TaskProjection` (breaking): the first accepted `TaskAccept` now wins —
|
|
38
|
+
a later `TaskAccept` for the same task no longer overwrites `assignee`
|
|
39
|
+
(RFC-MACP-0009 §5 rules 3/3a, `:69-71`). The policy-gated reassignment
|
|
40
|
+
path (rule 3c) is intentionally not modelled yet; `TaskProjection` has
|
|
41
|
+
no session-policy input.
|
|
42
|
+
- `HandoffProjection` (breaking): a `handoff_id` now settles (`offered` →
|
|
43
|
+
`accepted`/`declined`) exactly once — a later contradictory
|
|
44
|
+
`HandoffAccept`/`HandoffDecline` for an already-settled `handoff_id`,
|
|
45
|
+
or for a `handoff_id` that was never offered, is ignored in both
|
|
46
|
+
directions and can no longer mutate `phase` (RFC-MACP-0010 §5 rule 2
|
|
47
|
+
`:65`, rule 4 `:68`, §5.1(4) `:113-116`).
|
|
48
|
+
- `DecisionProjection` (not breaking for conforming callers): `phase` no
|
|
49
|
+
longer regresses out of `'Committed'` if a `Vote` is replayed after a
|
|
50
|
+
`Commitment` — such a message cannot legally exist in accepted history
|
|
51
|
+
under a conforming runtime (RFC-MACP-0001 §7.2 `:218`, §7.3
|
|
52
|
+
`:240`/`:249`), but a caller violating the accepted-only contract no
|
|
53
|
+
longer corrupts `phase` if it happens.
|
|
54
|
+
|
|
55
|
+
`ProposalProjection.isAccepted()`/`acceptedProposal()`, `TaskProjection`'s
|
|
56
|
+
`assignee` field, and `HandoffProjection`'s `status`/`phase` can now
|
|
57
|
+
derive different values than before for a transcript containing the
|
|
58
|
+
message sequences described above — existing callers reading those
|
|
59
|
+
surfaces should treat this as a breaking change. Conforming transcripts
|
|
60
|
+
(no superseded accept, no second `TaskAccept`, no contradictory or
|
|
61
|
+
unknown-`handoff_id` handoff accept/decline) are unaffected.
|
|
62
|
+
|
|
63
|
+
Three other sites raised by #59 (Decision `Evaluation`/`Objection`
|
|
64
|
+
cardinality, Task `TaskUpdate` accumulation) were confirmed to be silent
|
|
65
|
+
*coherently* and are unchanged. `TaskComplete`/`TaskFail` cardinality is
|
|
66
|
+
genuinely ambiguous in RFC-MACP-0009 and is tracked as an upstream spec
|
|
67
|
+
question rather than an SDK change.
|
|
68
|
+
|
|
69
|
+
### Changed
|
|
70
|
+
|
|
71
|
+
- **`GrpcTransportAdapter` resumes instead of replaying on a second `start()`.**
|
|
72
|
+
The adapter now calls `sendSubscribe(sessionId, this.lastSequence)` instead of
|
|
73
|
+
always subscribing from `0`, so a caller that `stop()`s and re-`start()`s a
|
|
74
|
+
reused adapter — the only reachable "reconnect" in this SDK; there is no
|
|
75
|
+
built-in retry loop — no longer replays the entire session history. Requires
|
|
76
|
+
`macp-runtime` ≥ 0.5.0 for the `after_sequence` ordinal contract
|
|
77
|
+
(RFC-MACP-0006 §3.2); older runtimes compared it inclusively against a raw
|
|
78
|
+
log index. `lastSequence` itself is corrected in the same change to count
|
|
79
|
+
**distinct** accepted envelopes (keyed on `message_id`) instead of raw
|
|
80
|
+
delivery events — passing the old, uncorrected counter as a resume cursor
|
|
81
|
+
would have silently skipped history on resume (RFC-MACP-0006 §3.2
|
|
82
|
+
Redelivery). Anyone relying on a reused adapter's second `start()` to rebuild
|
|
83
|
+
a *fresh* projection from full history will now see only the envelopes
|
|
84
|
+
accepted since the first pass ended; open a new `GrpcTransportAdapter` (or a
|
|
85
|
+
raw `MacpStream` with `sendSubscribe(sessionId, 0)`) for a full replay.
|
|
86
|
+
- `MacpStream.sendSubscribe`'s docblock no longer claims `IncomingMessage.seq`
|
|
87
|
+
is the RFC-MACP-0006 ordinal — `seq` is a 0-based delivery index that
|
|
88
|
+
advances on redelivery, unlike the ordinal.
|
|
89
|
+
|
|
90
|
+
## [0.8.0](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/compare/v0.7.1...v0.8.0) (2026-08-31)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
### ⚠ BREAKING CHANGES
|
|
94
|
+
|
|
95
|
+
* **projections:** DecisionProjection and QuorumProjection now keep the FIRST accepted Vote/ballot per sender (per proposal_id / request_id) and discard later ones, where they previously kept the last. For a transcript containing a genuine duplicate, derived state changes — most consequentially QuorumProjection.hasQuorum(), which could previously be fabricated by a single sender voting twice under requiredApprovals: 1. Conforming transcripts are unaffected. The discarded message is recorded on the new `anomalies` surface.
|
|
96
|
+
|
|
97
|
+
### Features
|
|
98
|
+
|
|
99
|
+
* **projections:** the first accepted vote or ballot stands ([#61](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/61)) ([ac407c3](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/commit/ac407c3767e785b2a1cf82e1ae735a9ebf1138be))
|
|
100
|
+
|
|
7
101
|
## [0.7.1](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/compare/v0.7.0...v0.7.1) (2026-08-31)
|
|
8
102
|
|
|
9
103
|
|
package/README.md
CHANGED
|
@@ -367,7 +367,7 @@ await stream.sendSubscribe(sessionId); // full replay then live
|
|
|
367
367
|
await stream.sendSubscribe(sessionId, lastSeq); // resume from cursor
|
|
368
368
|
```
|
|
369
369
|
|
|
370
|
-
The agent framework's `GrpcTransportAdapter` calls this for you on start, so non-initiator agents see `SessionStart` regardless of join order.
|
|
370
|
+
The agent framework's `GrpcTransportAdapter` calls this for you on start, so non-initiator agents see `SessionStart` regardless of join order — and it passes its own resume cursor on a second `start()` (after a `stop()`), so that call resumes rather than replaying the whole session again.
|
|
371
371
|
|
|
372
372
|
### Registry & Root Watchers
|
|
373
373
|
|
|
@@ -12,14 +12,39 @@ export declare class GrpcTransportAdapter implements TransportAdapter {
|
|
|
12
12
|
private stream;
|
|
13
13
|
private seq;
|
|
14
14
|
private delivered;
|
|
15
|
+
/**
|
|
16
|
+
* `message_id`s already counted into `delivered`, so the resume cursor
|
|
17
|
+
* advances once per distinct accepted envelope rather than once per raw
|
|
18
|
+
* delivery (RFC-MACP-0006 §3.2 Redelivery). Deliberately unbounded, for the
|
|
19
|
+
* same reason `BaseProjection` leaves its own copy of this set unbounded
|
|
20
|
+
* (`src/projections/base.ts:118-129`): this set is strictly dominated by
|
|
21
|
+
* memory this SDK already commits to per session — a `Participant` using
|
|
22
|
+
* this adapter also holds a `BaseProjection` with an identical
|
|
23
|
+
* `message_id` set *plus* a full `transcript` of every envelope, payload
|
|
24
|
+
* bytes included. The runtime keeps an unbounded per-session dedup set of
|
|
25
|
+
* its own regardless of what any client does
|
|
26
|
+
* (`macp-runtime/crates/macp-modes/src/step.rs:48/:89`, field declared at
|
|
27
|
+
* `macp-runtime/crates/macp-core/src/session.rs:69`), and sessions are
|
|
28
|
+
* TTL-bounded by protocol.
|
|
29
|
+
*/
|
|
30
|
+
private readonly seenMessageIds;
|
|
15
31
|
constructor(client: MacpClient, sessionId: string, auth?: AuthConfig | undefined);
|
|
16
32
|
/**
|
|
17
33
|
* The server passive-subscribe ordinal of the last delivered envelope (the
|
|
18
|
-
* 1-based count of accepted envelopes delivered on this stream,
|
|
19
|
-
* §3.2). `0` before anything is delivered.
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
34
|
+
* 1-based count of *distinct* accepted envelopes delivered on this stream,
|
|
35
|
+
* RFC-MACP-0006 §3.2). `0` before anything is delivered. `start()` passes
|
|
36
|
+
* this value as `afterSequence` to `MacpStream.sendSubscribe` itself, so a
|
|
37
|
+
* caller that `stop()`s and calls `start()` again (the only reachable
|
|
38
|
+
* "reconnect" in this SDK — there is no built-in retry loop) resumes
|
|
39
|
+
* rather than replays the whole session. Ordinals are stable across
|
|
40
|
+
* compaction/restart. Counts distinct `message_id`s, never raw delivery
|
|
41
|
+
* events: RFC-MACP-0006 §3.2 Redelivery requires that "a redelivery MUST
|
|
42
|
+
* NOT advance the client's sequence position; only a distinct accepted
|
|
43
|
+
* envelope does," and a client that counts raw deliveries "arrives at a
|
|
44
|
+
* position ahead of the true one, and its next resume silently skips
|
|
45
|
+
* history." Distinct from `IncomingMessage.seq`, which is a client-local
|
|
46
|
+
* 0-based delivery index that advances on every delivery, redeliveries
|
|
47
|
+
* included.
|
|
23
48
|
*/
|
|
24
49
|
get lastSequence(): number;
|
|
25
50
|
start(): AsyncIterable<IncomingMessage>;
|
package/dist/agent/transports.js
CHANGED
|
@@ -19,6 +19,22 @@ class GrpcTransportAdapter {
|
|
|
19
19
|
stream = null;
|
|
20
20
|
seq = 0;
|
|
21
21
|
delivered = 0;
|
|
22
|
+
/**
|
|
23
|
+
* `message_id`s already counted into `delivered`, so the resume cursor
|
|
24
|
+
* advances once per distinct accepted envelope rather than once per raw
|
|
25
|
+
* delivery (RFC-MACP-0006 §3.2 Redelivery). Deliberately unbounded, for the
|
|
26
|
+
* same reason `BaseProjection` leaves its own copy of this set unbounded
|
|
27
|
+
* (`src/projections/base.ts:118-129`): this set is strictly dominated by
|
|
28
|
+
* memory this SDK already commits to per session — a `Participant` using
|
|
29
|
+
* this adapter also holds a `BaseProjection` with an identical
|
|
30
|
+
* `message_id` set *plus* a full `transcript` of every envelope, payload
|
|
31
|
+
* bytes included. The runtime keeps an unbounded per-session dedup set of
|
|
32
|
+
* its own regardless of what any client does
|
|
33
|
+
* (`macp-runtime/crates/macp-modes/src/step.rs:48/:89`, field declared at
|
|
34
|
+
* `macp-runtime/crates/macp-core/src/session.rs:69`), and sessions are
|
|
35
|
+
* TTL-bounded by protocol.
|
|
36
|
+
*/
|
|
37
|
+
seenMessageIds = new Set();
|
|
22
38
|
constructor(client, sessionId, auth) {
|
|
23
39
|
this.client = client;
|
|
24
40
|
this.sessionId = sessionId;
|
|
@@ -26,27 +42,92 @@ class GrpcTransportAdapter {
|
|
|
26
42
|
}
|
|
27
43
|
/**
|
|
28
44
|
* The server passive-subscribe ordinal of the last delivered envelope (the
|
|
29
|
-
* 1-based count of accepted envelopes delivered on this stream,
|
|
30
|
-
* §3.2). `0` before anything is delivered.
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
45
|
+
* 1-based count of *distinct* accepted envelopes delivered on this stream,
|
|
46
|
+
* RFC-MACP-0006 §3.2). `0` before anything is delivered. `start()` passes
|
|
47
|
+
* this value as `afterSequence` to `MacpStream.sendSubscribe` itself, so a
|
|
48
|
+
* caller that `stop()`s and calls `start()` again (the only reachable
|
|
49
|
+
* "reconnect" in this SDK — there is no built-in retry loop) resumes
|
|
50
|
+
* rather than replays the whole session. Ordinals are stable across
|
|
51
|
+
* compaction/restart. Counts distinct `message_id`s, never raw delivery
|
|
52
|
+
* events: RFC-MACP-0006 §3.2 Redelivery requires that "a redelivery MUST
|
|
53
|
+
* NOT advance the client's sequence position; only a distinct accepted
|
|
54
|
+
* envelope does," and a client that counts raw deliveries "arrives at a
|
|
55
|
+
* position ahead of the true one, and its next resume silently skips
|
|
56
|
+
* history." Distinct from `IncomingMessage.seq`, which is a client-local
|
|
57
|
+
* 0-based delivery index that advances on every delivery, redeliveries
|
|
58
|
+
* included.
|
|
34
59
|
*/
|
|
35
60
|
get lastSequence() {
|
|
36
61
|
return this.delivered;
|
|
37
62
|
}
|
|
38
63
|
async *start() {
|
|
64
|
+
// A prior stream from an earlier start() (without an intervening stop())
|
|
65
|
+
// must not keep feeding this adapter's counter — two live generators
|
|
66
|
+
// would both advance `delivered` from two differently-positioned
|
|
67
|
+
// replays. `MacpStream.close()` is `this.call.end()` (`src/client.ts:223-227`),
|
|
68
|
+
// a write-side half-close — it does not itself force the server to stop
|
|
69
|
+
// pushing. It is enough here because macp-runtime's StreamSession loop
|
|
70
|
+
// reads the resulting end-of-request-stream as `StreamAction::ClientDone`,
|
|
71
|
+
// whose arm drains any already-buffered envelopes once and then breaks,
|
|
72
|
+
// ending that stream's response side too
|
|
73
|
+
// (macp-runtime/src/server.rs:654-666). Close it before opening the new
|
|
74
|
+
// one so re-entrant start() and stop()-then-start() behave identically.
|
|
75
|
+
if (this.stream) {
|
|
76
|
+
this.stream.close();
|
|
77
|
+
this.stream = null;
|
|
78
|
+
}
|
|
39
79
|
this.stream = this.client.openStream({ auth: this.auth });
|
|
40
|
-
// RFC-MACP-0006
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
|
|
80
|
+
// RFC-MACP-0006 §3.2: subscribe to the session, resuming from this
|
|
81
|
+
// adapter's own cursor. `this.delivered` is 0 before anything has been
|
|
82
|
+
// delivered, by construction, and `afterSequence = 0` is normatively
|
|
83
|
+
// "replay from the session's first accepted envelope" — so the first
|
|
84
|
+
// subscribe and every later one (e.g. after stop()+start()) are the same
|
|
85
|
+
// expression. There is no separate first-subscribe/reconnect branch to
|
|
86
|
+
// keep in sync with the counter.
|
|
87
|
+
await this.stream.sendSubscribe(this.sessionId, this.delivered);
|
|
45
88
|
for await (const envelope of this.stream.responses()) {
|
|
46
89
|
if (envelope.sessionId !== this.sessionId)
|
|
47
90
|
continue;
|
|
48
|
-
this
|
|
49
|
-
|
|
91
|
+
// Count this envelope into the resume cursor only *after* the
|
|
92
|
+
// consumer has come back for more, not when it is handed off. `yield`
|
|
93
|
+
// suspends here until the consumer's `for await` either calls
|
|
94
|
+
// `.next()` again (it took this envelope and kept iterating — the
|
|
95
|
+
// code below runs) or closes the iterator via `break`/`return`/throw
|
|
96
|
+
// (e.g. `Participant.run()` observing `!this.running` and breaking
|
|
97
|
+
// out of its loop without processing this envelope). In the latter
|
|
98
|
+
// case `.return()` on this generator resumes at this `yield` as if a
|
|
99
|
+
// `return` had been written here, so nothing below it runs: the
|
|
100
|
+
// cursor is never advanced past an envelope the consumer never
|
|
101
|
+
// actually took past the loop boundary, and the next `start()`
|
|
102
|
+
// redelivers it instead of silently skipping it. See #66 — counting
|
|
103
|
+
// before yielding let a stop-mid-stream permanently drop an envelope
|
|
104
|
+
// once #65 made the resume cursor authoritative.
|
|
105
|
+
const message = normalizeEnvelope(envelope, (mode, mt, p) => this.client.protoRegistry.decodeKnownPayload(mode, mt, p), this.seq++);
|
|
106
|
+
yield message;
|
|
107
|
+
// RFC-MACP-0006 §3.2 Redelivery: a redelivery MUST NOT advance the
|
|
108
|
+
// resume cursor, and a consumer that accumulates state per envelope
|
|
109
|
+
// MUST be idempotent w.r.t. `message_id`. An empty/absent messageId
|
|
110
|
+
// has no identity to dedup on and increments unconditionally — the
|
|
111
|
+
// same carve-out the projection guard documents at
|
|
112
|
+
// `src/projections/base.ts:224-227`, so the two dedup sites read the
|
|
113
|
+
// same way. In practice the `else` below can never run against a
|
|
114
|
+
// conformant runtime: `validate_envelope_shape` rejects any envelope
|
|
115
|
+
// with an empty `message_id` as `InvalidEnvelope` before it can be
|
|
116
|
+
// accepted (`macp-runtime/src/server.rs:118`), and RFC-MACP-0001 §8.2
|
|
117
|
+
// makes `message_id` the runtime's dedup identity, so no accepted
|
|
118
|
+
// envelope in any session history can carry one. The branch stays
|
|
119
|
+
// anyway — it costs nothing and errs toward still counting the
|
|
120
|
+
// envelope instead of silently dropping it if a non-conformant server
|
|
121
|
+
// ever sends one.
|
|
122
|
+
if (envelope.messageId) {
|
|
123
|
+
if (!this.seenMessageIds.has(envelope.messageId)) {
|
|
124
|
+
this.seenMessageIds.add(envelope.messageId);
|
|
125
|
+
this.delivered++;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
this.delivered++;
|
|
130
|
+
}
|
|
50
131
|
}
|
|
51
132
|
}
|
|
52
133
|
async stop() {
|
|
@@ -88,6 +169,15 @@ class HttpTransportAdapter {
|
|
|
88
169
|
if (itemSeq > this.lastSeq) {
|
|
89
170
|
this.lastSeq = itemSeq;
|
|
90
171
|
}
|
|
172
|
+
// Normalize message_id -> messageId before yielding: without
|
|
173
|
+
// this, `raw` is the raw snake_case JSON straight off the wire,
|
|
174
|
+
// so `raw.messageId` is `undefined` and the projection's
|
|
175
|
+
// redelivery guard (`if (envelope.messageId)`,
|
|
176
|
+
// `src/projections/base.ts`) short-circuits — every polled
|
|
177
|
+
// envelope looks id-less and skips dedup entirely, leaving HTTP
|
|
178
|
+
// polling silently unprotected against RFC-MACP-0006 §3.2
|
|
179
|
+
// redelivery.
|
|
180
|
+
const messageId = item.message_id ?? item.messageId ?? '';
|
|
91
181
|
yield {
|
|
92
182
|
messageType: item.message_type ?? item.messageType ?? '',
|
|
93
183
|
sender: item.sender ?? '',
|
|
@@ -95,7 +185,7 @@ class HttpTransportAdapter {
|
|
|
95
185
|
? item.payload
|
|
96
186
|
: this.tryParsePayload(item.payload),
|
|
97
187
|
proposalId: item.proposal_id ?? item.proposalId,
|
|
98
|
-
raw: item,
|
|
188
|
+
raw: { ...item, messageId },
|
|
99
189
|
seq: this.seq++,
|
|
100
190
|
};
|
|
101
191
|
}
|
package/dist/agent/types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ProjectionAnomaly } from '../projections/base';
|
|
1
2
|
import type { Envelope } from '../types';
|
|
2
3
|
export interface IncomingMessage {
|
|
3
4
|
messageType: string;
|
|
@@ -62,6 +63,17 @@ export interface ParticipantLike {
|
|
|
62
63
|
export interface ProjectionLike {
|
|
63
64
|
readonly phase: string;
|
|
64
65
|
readonly transcript: Envelope[];
|
|
66
|
+
/**
|
|
67
|
+
* Cardinality anomalies observed and discarded by the projection, in transcript order.
|
|
68
|
+
*
|
|
69
|
+
* OPTIONAL, and it must STAY optional. `ProjectionLike` is structurally
|
|
70
|
+
* implementable by consumers — test fakes, custom transports, alternative
|
|
71
|
+
* projection implementations — none of which this SDK controls. Making this
|
|
72
|
+
* member required would break every such implementer at compile time for a
|
|
73
|
+
* field most of them have no reason to provide. The guard below turns a
|
|
74
|
+
* later "tightening" into a build failure rather than a silent break.
|
|
75
|
+
*/
|
|
76
|
+
readonly anomalies?: readonly ProjectionAnomaly[];
|
|
65
77
|
}
|
|
66
78
|
export type MessageHandler = (event: IncomingMessage, ctx: HandlerContext) => void | Promise<void>;
|
|
67
79
|
export type TerminalHandler = (result: TerminalResult) => void | Promise<void>;
|
package/dist/client.d.ts
CHANGED
|
@@ -48,9 +48,13 @@ export declare class MacpStream {
|
|
|
48
48
|
*
|
|
49
49
|
* `afterSequence` is the **1-based accepted-envelope ordinal**, exclusive:
|
|
50
50
|
* `0` (default) replays from the very start; `k` replays envelopes with
|
|
51
|
-
* ordinal `> k`. Clients derive the ordinal by counting
|
|
52
|
-
*
|
|
53
|
-
*
|
|
51
|
+
* ordinal `> k`. Clients derive the ordinal by counting **distinct**
|
|
52
|
+
* accepted envelopes delivered, keyed on `message_id` — RFC-MACP-0006 §3.2
|
|
53
|
+
* Redelivery requires that a repeat of an already-observed envelope MUST
|
|
54
|
+
* NOT advance the count. This is **not** the same as `IncomingMessage.seq`,
|
|
55
|
+
* which is a client-local 0-based delivery index that advances on every
|
|
56
|
+
* delivery, redeliveries included; see `GrpcTransportAdapter.lastSequence`
|
|
57
|
+
* for a cursor that tracks the ordinal correctly.
|
|
54
58
|
* Ordinals are stable across log compaction and runtime restart. Resuming
|
|
55
59
|
* below a compacted base returns `FAILED_PRECONDITION` (inspectable via
|
|
56
60
|
* `MacpTransportError.code`) rather than silently skipping history (runtime
|
package/dist/client.js
CHANGED
|
@@ -160,9 +160,13 @@ class MacpStream {
|
|
|
160
160
|
*
|
|
161
161
|
* `afterSequence` is the **1-based accepted-envelope ordinal**, exclusive:
|
|
162
162
|
* `0` (default) replays from the very start; `k` replays envelopes with
|
|
163
|
-
* ordinal `> k`. Clients derive the ordinal by counting
|
|
164
|
-
*
|
|
165
|
-
*
|
|
163
|
+
* ordinal `> k`. Clients derive the ordinal by counting **distinct**
|
|
164
|
+
* accepted envelopes delivered, keyed on `message_id` — RFC-MACP-0006 §3.2
|
|
165
|
+
* Redelivery requires that a repeat of an already-observed envelope MUST
|
|
166
|
+
* NOT advance the count. This is **not** the same as `IncomingMessage.seq`,
|
|
167
|
+
* which is a client-local 0-based delivery index that advances on every
|
|
168
|
+
* delivery, redeliveries included; see `GrpcTransportAdapter.lastSequence`
|
|
169
|
+
* for a cursor that tracks the ordinal correctly.
|
|
166
170
|
* Ordinals are stable across log compaction and runtime restart. Resuming
|
|
167
171
|
* below a compacted base returns `FAILED_PRECONDITION` (inspectable via
|
|
168
172
|
* `MacpTransportError.code`) rather than silently skipping history (runtime
|
|
@@ -1,5 +1,53 @@
|
|
|
1
1
|
import type { ProtoRegistry } from '../proto-registry';
|
|
2
2
|
import type { Envelope } from '../types';
|
|
3
|
+
/**
|
|
4
|
+
* The kind of cardinality anomaly a projection recorded while replaying an
|
|
5
|
+
* accepted transcript. Cross-SDK frozen contract (`macp-sdk-python` adopted
|
|
6
|
+
* the identical field set, snake_case there) — do not add a kind without
|
|
7
|
+
* cross-SDK agreement.
|
|
8
|
+
*/
|
|
9
|
+
export type ProjectionAnomalyKind = 'duplicate_vote' | 'duplicate_ballot';
|
|
10
|
+
/**
|
|
11
|
+
* A recorded observation that a second distinct `Vote` from this sender for
|
|
12
|
+
* this proposal (RFC-MACP-0007 §5.3), or a second distinct ballot across
|
|
13
|
+
* `Approve`/`Reject`/`Abstain` from this sender for this request
|
|
14
|
+
* (RFC-MACP-0011 §5 rule 3), was observed and discarded — the first stands.
|
|
15
|
+
* (RFC-MACP-0011 §5 rule 3 caps *how many* ballots and is silent on *which of
|
|
16
|
+
* two* stands; first-ballot-wins here is parity with RFC-MACP-0007 §5.3 plus
|
|
17
|
+
* runtime-enforced behaviour.)
|
|
18
|
+
*
|
|
19
|
+
* **Deliberately narrow claim — do not overclaim "this transcript violates
|
|
20
|
+
* the spec".** A projection cannot tell a genuinely non-conforming source
|
|
21
|
+
* from a conforming source fed through an unfiltered loader, because
|
|
22
|
+
* acceptance is not a wire property (`Envelope` carries no acceptance
|
|
23
|
+
* marker — see `applyEnvelope`'s "Input contract: accepted-only" docblock).
|
|
24
|
+
* An anomaly records only what was observed and what this projection did
|
|
25
|
+
* about it (discarded the second one, kept the first); it does not, and
|
|
26
|
+
* structurally cannot, assert that a conforming runtime was actually
|
|
27
|
+
* bypassed. Both `macp-sdk-python` and this SDK state it in these terms —
|
|
28
|
+
* agreed cross-SDK wording, not a style preference.
|
|
29
|
+
*
|
|
30
|
+
* Cross-SDK frozen contract, seven fields (camelCase here, snake_case in
|
|
31
|
+
* `macp-sdk-python`). Do not add, rename, or remove a field without
|
|
32
|
+
* cross-SDK agreement.
|
|
33
|
+
*
|
|
34
|
+
* Only `DecisionProjection` and `QuorumProjection` populate this today.
|
|
35
|
+
*/
|
|
36
|
+
export interface ProjectionAnomaly {
|
|
37
|
+
kind: ProjectionAnomalyKind;
|
|
38
|
+
mode: string;
|
|
39
|
+
messageType: string;
|
|
40
|
+
messageId: string;
|
|
41
|
+
sender: string;
|
|
42
|
+
/** `proposal_id` (Decision) or `request_id` (Quorum) the duplicate targeted. */
|
|
43
|
+
subjectId: string;
|
|
44
|
+
/**
|
|
45
|
+
* Human-readable detail. For a cross-type Quorum duplicate, `messageType`
|
|
46
|
+
* is what distinguishes the discarded ballot's type (Approve/Reject/Abstain)
|
|
47
|
+
* from the kept one — `detail` should name both.
|
|
48
|
+
*/
|
|
49
|
+
detail: string;
|
|
50
|
+
}
|
|
3
51
|
/**
|
|
4
52
|
* Abstract base for in-process mode-state tracking — parity with python-sdk's
|
|
5
53
|
* `macp_sdk.base_projection.BaseProjection`. Maintains a shared transcript,
|
|
@@ -8,13 +56,114 @@ import type { Envelope } from '../types';
|
|
|
8
56
|
* handled here so custom modes get the common lifecycle for free.
|
|
9
57
|
*/
|
|
10
58
|
export declare abstract class BaseProjection {
|
|
59
|
+
/**
|
|
60
|
+
* The session's accepted history as a conforming runtime holds it: one
|
|
61
|
+
* envelope per unique `message_id`, in application order. Redelivered
|
|
62
|
+
* envelopes (same `message_id` — expected under MACP's at-least-once
|
|
63
|
+
* delivery, RFC-MACP-0001 §8, and normatively idempotent per RFC-MACP-0006
|
|
64
|
+
* §3.2 Redelivery) and envelopes for other modes are not appended; a
|
|
65
|
+
* redelivery is logged at debug level and has no effect on projection
|
|
66
|
+
* state.
|
|
67
|
+
*/
|
|
11
68
|
readonly transcript: Envelope[];
|
|
12
69
|
phase: string;
|
|
13
70
|
commitment?: Record<string, unknown>;
|
|
71
|
+
/**
|
|
72
|
+
* `message_id`s already applied to this projection, used to make
|
|
73
|
+
* `applyEnvelope` idempotent under redelivery (RFC-MACP-0006 §3.2:
|
|
74
|
+
* "A consumer that accumulates state per envelope ... MUST be idempotent
|
|
75
|
+
* with respect to `message_id`", `:136`). Deliberately unbounded: this
|
|
76
|
+
* class already retains every full envelope (payload bytes included) in
|
|
77
|
+
* `transcript`, so a set of id strings is strictly dominated by that; the
|
|
78
|
+
* runtime keeps an unbounded per-message dedup set itself
|
|
79
|
+
* (`macp-runtime/crates/macp-modes/src/step.rs:48/:89`, field declared at
|
|
80
|
+
* `crates/macp-core/src/session.rs:69`), and sessions are TTL-bounded by
|
|
81
|
+
* protocol.
|
|
82
|
+
*/
|
|
83
|
+
private readonly seenMessageIds;
|
|
84
|
+
/**
|
|
85
|
+
* Cardinality anomalies recorded while replaying this projection's
|
|
86
|
+
* accepted transcript (e.g. a duplicate vote or ballot from the same
|
|
87
|
+
* sender). See `ProjectionAnomaly`. Empty unless a subclass calls
|
|
88
|
+
* `recordAnomaly`. NOTE: as of this SDK's built-in modes, nothing calls
|
|
89
|
+
* `recordAnomaly` — `DecisionProjection` and `QuorumProjection` do not
|
|
90
|
+
* extend `BaseProjection` (see the class docblock) and inline their own
|
|
91
|
+
* two lines instead. This field and `recordAnomaly` exist for ext-mode
|
|
92
|
+
* `BaseProjection` subclasses outside this repo.
|
|
93
|
+
*/
|
|
94
|
+
readonly anomalies: ProjectionAnomaly[];
|
|
14
95
|
protected abstract readonly mode: string;
|
|
96
|
+
/**
|
|
97
|
+
* True once at least one `ProjectionAnomaly` has been recorded — i.e. this
|
|
98
|
+
* projection observed and discarded a duplicate vote or ballot from the
|
|
99
|
+
* same sender (see `anomalies` above). This getter is exactly
|
|
100
|
+
* `anomalies.length > 0`; it never asserts "this transcript violates the
|
|
101
|
+
* spec" (see `ProjectionAnomaly`'s docblock, and `applyEnvelope`'s "Input
|
|
102
|
+
* contract: accepted-only" section above, for why a projection cannot make
|
|
103
|
+
* that claim).
|
|
104
|
+
*/
|
|
105
|
+
get hasAnomalies(): boolean;
|
|
15
106
|
get isCommitted(): boolean;
|
|
16
107
|
get isPositiveOutcome(): boolean | undefined;
|
|
108
|
+
/**
|
|
109
|
+
* Apply one envelope to this projection's in-process state.
|
|
110
|
+
*
|
|
111
|
+
* ## Input contract: accepted-only
|
|
112
|
+
*
|
|
113
|
+
* `applyEnvelope` assumes every envelope it is given was **accepted** by a
|
|
114
|
+
* conforming MACP runtime — i.e. it is part of the session's authoritative
|
|
115
|
+
* accepted history, never a submission the runtime rejected. This is a
|
|
116
|
+
* **caller-maintained invariant**, not something this method can check:
|
|
117
|
+
* `Envelope` (`src/types.ts`) carries no acceptance marker on the wire, so a
|
|
118
|
+
* projection has no field to inspect to tell an accepted envelope from a
|
|
119
|
+
* rejected one. Canonical source, verbatim: `schemas/conformance/README.md`
|
|
120
|
+
* "Notes:" — "SDKs replay only `accept` messages through their projections
|
|
121
|
+
* (reject-path fixtures replay their accepted *prefix*)." The rule this
|
|
122
|
+
* supports is spelled out in RFC-MACP-0007 §5 rule 3 (this repo's shorthand:
|
|
123
|
+
* "§5.3" — the RFC has no literal `### 5.3` heading) — "the first accepted
|
|
124
|
+
* `Vote` stands" — and RFC-MACP-0011 §5 rule 3 — at most one ballot per
|
|
125
|
+
* participant. Both describe what a conforming runtime enforces on ACCEPTED
|
|
126
|
+
* history; a projection reconstructing that history is only correct when fed
|
|
127
|
+
* the same set the runtime held.
|
|
128
|
+
*
|
|
129
|
+
* This SDK's own send paths uphold the invariant already: each of the five
|
|
130
|
+
* built-in mode sessions has its own private `sendAndTrack` that calls
|
|
131
|
+
* `this.projection.applyEnvelope(...)` only `if (ack.ok)`. `BaseSession`'s
|
|
132
|
+
* own `sendAndTrack` (`src/base-session.ts`) — the equivalent path for
|
|
133
|
+
* custom modes built on the ext-mode extension point — does the same. The
|
|
134
|
+
* conformance harness upholds it too: its `acceptedMessages` filter in
|
|
135
|
+
* `tests/conformance/conformance.test.ts` keeps only messages with
|
|
136
|
+
* `expect === 'accept'` before replay.
|
|
137
|
+
*
|
|
138
|
+
* **Failure mode if the contract is violated:** wiring this method to raw
|
|
139
|
+
* captured or hand-built traffic — including envelopes a runtime rejected —
|
|
140
|
+
* replays a timeline that no conforming runtime ever held, fabricating
|
|
141
|
+
* projection state. Concretely: a session whose only `Commitment` was
|
|
142
|
+
* rejected can still end up with `isCommitted === true` and
|
|
143
|
+
* `phase === 'Committed'` if that rejected envelope is replayed anyway. See
|
|
144
|
+
* `tests/unit/projections/accepted-only-contract.test.ts` for an executable
|
|
145
|
+
* demonstration, both with the corruption present and with it avoided.
|
|
146
|
+
*
|
|
147
|
+
* @param envelope - MUST be an envelope the runtime accepted; the caller is
|
|
148
|
+
* responsible for filtering out rejected submissions before calling this
|
|
149
|
+
* method. Envelopes for a different `mode` are ignored (the `if
|
|
150
|
+
* (envelope.mode !== this.mode) return;` guard below, in this method's own
|
|
151
|
+
* body), but that check is unrelated to this contract and does not
|
|
152
|
+
* substitute for it.
|
|
153
|
+
*/
|
|
17
154
|
applyEnvelope(envelope: Envelope, protoRegistry: ProtoRegistry): void;
|
|
18
155
|
/** Handle a mode-specific (non-Commitment) envelope. */
|
|
19
156
|
protected abstract applyMode(envelope: Envelope, protoRegistry: ProtoRegistry): void;
|
|
157
|
+
/**
|
|
158
|
+
* Record a cardinality anomaly: push it onto `anomalies` and emit it via
|
|
159
|
+
* `logger.warn('projection anomaly', anomaly)`. The `anomalies` array is
|
|
160
|
+
* the canonical, cross-SDK-agreed semantic; the log call is explicitly
|
|
161
|
+
* NON-contractual observability and may differ per SDK. `logger.warn` is
|
|
162
|
+
* visible by default (this SDK's default log level is `warn`,
|
|
163
|
+
* `src/logging.ts`) — observing and discarding a duplicate is deliberately
|
|
164
|
+
* not silent. To quiet it without losing the `anomalies` array, configure a
|
|
165
|
+
* higher level: `configureLogging({ level: 'error' })` or the
|
|
166
|
+
* `MACP_LOG_LEVEL` environment variable.
|
|
167
|
+
*/
|
|
168
|
+
protected recordAnomaly(anomaly: ProjectionAnomaly): void;
|
|
20
169
|
}
|