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
|
@@ -2,16 +2,77 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ProposalProjection = void 0;
|
|
4
4
|
const constants_1 = require("../constants");
|
|
5
|
+
const logging_1 = require("../logging");
|
|
5
6
|
class ProposalProjection {
|
|
6
7
|
proposals = new Map();
|
|
7
8
|
accepts = [];
|
|
8
9
|
rejections = [];
|
|
10
|
+
/**
|
|
11
|
+
* Each sender's current (unsuperseded) `Accept`, keyed by sender.
|
|
12
|
+
* RFC-MACP-0008 §5 rule 5 (`:70`): "A participant MAY change its
|
|
13
|
+
* acceptance target by sending a later `Accept` for a different live
|
|
14
|
+
* proposal. The latest accepted `Accept` from a participant supersedes
|
|
15
|
+
* earlier accepts from the same participant." `accepts` above retains the
|
|
16
|
+
* full append-only history (including superseded accepts) for audit
|
|
17
|
+
* purposes; `isAccepted` and `acceptedProposal` read from this map instead
|
|
18
|
+
* so they reflect the live acceptance set — required for determinism by
|
|
19
|
+
* §7 (`:89`): "Given the same accepted history and the same version-bound
|
|
20
|
+
* rules, implementations MUST derive the same live proposal set, the same
|
|
21
|
+
* acceptance set, and the same commitment eligibility."
|
|
22
|
+
*/
|
|
23
|
+
latestAcceptBySender = new Map();
|
|
24
|
+
/**
|
|
25
|
+
* The session's accepted history, one envelope per unique `message_id`. See
|
|
26
|
+
* `BaseProjection.transcript` (`src/projections/base.ts`) for the full
|
|
27
|
+
* redelivery-idempotence contract (RFC-MACP-0006 §3.2); duplicated here
|
|
28
|
+
* only as a one-line pointer.
|
|
29
|
+
*/
|
|
9
30
|
transcript = [];
|
|
31
|
+
/**
|
|
32
|
+
* Cardinality anomalies recorded while replaying this projection's accepted
|
|
33
|
+
* transcript. See `BaseProjection.anomalies` (`src/projections/base.ts`)
|
|
34
|
+
* for the canonical description; duplicated here only as a one-line
|
|
35
|
+
* pointer. No built-in detection populates this for Proposal mode.
|
|
36
|
+
*/
|
|
37
|
+
anomalies = [];
|
|
10
38
|
phase = 'Negotiating';
|
|
11
39
|
commitment;
|
|
40
|
+
/**
|
|
41
|
+
* `message_id`s already applied to this projection. See
|
|
42
|
+
* `BaseProjection.seenMessageIds` (`src/projections/base.ts`) for the full
|
|
43
|
+
* redelivery-idempotence rationale (RFC-MACP-0006 §3.2); duplicated here
|
|
44
|
+
* only as a one-line pointer.
|
|
45
|
+
*/
|
|
46
|
+
seenMessageIds = new Set();
|
|
47
|
+
/**
|
|
48
|
+
* Apply one envelope to this projection's in-process state.
|
|
49
|
+
*
|
|
50
|
+
* Input contract: **accepted-only**, caller-maintained (`Envelope` carries
|
|
51
|
+
* no acceptance marker). Canonical source: `schemas/conformance/README.md`
|
|
52
|
+
* "Notes:", RFC-MACP-0007 §5.3, RFC-MACP-0011 §5. Full rationale and failure
|
|
53
|
+
* mode are documented once, on `BaseProjection.applyEnvelope`
|
|
54
|
+
* (`src/projections/base.ts`), and duplicated here only as a one-line
|
|
55
|
+
* pointer so six independent copies of the same prose cannot drift.
|
|
56
|
+
*
|
|
57
|
+
* Redelivery idempotence: a redelivered envelope (same `message_id`) is a
|
|
58
|
+
* non-event — not appended to `transcript`, not passed to the `switch`.
|
|
59
|
+
* See `BaseProjection.applyEnvelope` for the full RFC-MACP-0006 §3.2
|
|
60
|
+
* citation; duplicated here only as a one-line pointer.
|
|
61
|
+
*/
|
|
12
62
|
applyEnvelope(envelope, protoRegistry) {
|
|
13
63
|
if (envelope.mode !== constants_1.MODE_PROPOSAL)
|
|
14
64
|
return;
|
|
65
|
+
if (envelope.messageId) {
|
|
66
|
+
if (this.seenMessageIds.has(envelope.messageId)) {
|
|
67
|
+
logging_1.logger.debug('projection redelivery ignored', {
|
|
68
|
+
messageId: envelope.messageId,
|
|
69
|
+
mode: envelope.mode,
|
|
70
|
+
messageType: envelope.messageType,
|
|
71
|
+
});
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
this.seenMessageIds.add(envelope.messageId);
|
|
75
|
+
}
|
|
15
76
|
this.transcript.push(envelope);
|
|
16
77
|
const payload = protoRegistry.decodeKnownPayload(envelope.mode, envelope.messageType, envelope.payload);
|
|
17
78
|
switch (envelope.messageType) {
|
|
@@ -41,7 +102,11 @@ class ProposalProjection {
|
|
|
41
102
|
}
|
|
42
103
|
case 'Accept': {
|
|
43
104
|
const record = payload;
|
|
44
|
-
|
|
105
|
+
const accept = { ...record, sender: envelope.sender };
|
|
106
|
+
this.accepts.push(accept);
|
|
107
|
+
// RFC-MACP-0008 §5 rule 5 (`:70`): this Accept supersedes any earlier
|
|
108
|
+
// one from the same sender for the live acceptance set.
|
|
109
|
+
this.latestAcceptBySender.set(envelope.sender, accept);
|
|
45
110
|
break;
|
|
46
111
|
}
|
|
47
112
|
case 'Reject': {
|
|
@@ -77,6 +142,14 @@ class ProposalProjection {
|
|
|
77
142
|
break;
|
|
78
143
|
}
|
|
79
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* True once at least one `ProjectionAnomaly` has been recorded. See
|
|
147
|
+
* `BaseProjection.hasAnomalies` (`src/projections/base.ts`) for the
|
|
148
|
+
* canonical description; duplicated here only as a one-line pointer.
|
|
149
|
+
*/
|
|
150
|
+
get hasAnomalies() {
|
|
151
|
+
return this.anomalies.length > 0;
|
|
152
|
+
}
|
|
80
153
|
get isCommitted() {
|
|
81
154
|
return this.commitment !== undefined;
|
|
82
155
|
}
|
|
@@ -94,8 +167,19 @@ class ProposalProjection {
|
|
|
94
167
|
const all = [...this.proposals.values()];
|
|
95
168
|
return all[all.length - 1];
|
|
96
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* True if some participant's *current* (unsuperseded) `Accept` targets
|
|
172
|
+
* `proposalId` — RFC-MACP-0008 §5 rule 5 (`:70`). Note this reads from the
|
|
173
|
+
* live acceptance set, not from `accepts`' full history: a participant
|
|
174
|
+
* whose only accept for `proposalId` was later superseded by a re-accept
|
|
175
|
+
* of a different proposal is not counted.
|
|
176
|
+
*/
|
|
97
177
|
isAccepted(proposalId) {
|
|
98
|
-
|
|
178
|
+
for (const accept of this.latestAcceptBySender.values()) {
|
|
179
|
+
if (accept.proposalId === proposalId)
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
return false;
|
|
99
183
|
}
|
|
100
184
|
isTerminallyRejected(proposalId) {
|
|
101
185
|
return this.rejections.some((r) => r.proposalId === proposalId && r.terminal);
|
|
@@ -108,10 +192,19 @@ class ProposalProjection {
|
|
|
108
192
|
}
|
|
109
193
|
return result;
|
|
110
194
|
}
|
|
195
|
+
/**
|
|
196
|
+
* The single proposal every participant's *current* accept targets, or
|
|
197
|
+
* `undefined` if no one has an outstanding accept or outstanding accepts
|
|
198
|
+
* are split across more than one proposal. Computed from the live
|
|
199
|
+
* acceptance set (see `isAccepted`), not from `accepts`' full history — a
|
|
200
|
+
* participant who accepts `p1` and later re-accepts `p2` (RFC-MACP-0008 §5
|
|
201
|
+
* rule 5, `:70`) contributes only `p2`, so this correctly returns `p2`
|
|
202
|
+
* rather than `undefined`.
|
|
203
|
+
*/
|
|
111
204
|
acceptedProposal() {
|
|
112
|
-
if (this.
|
|
205
|
+
if (this.latestAcceptBySender.size === 0)
|
|
113
206
|
return undefined;
|
|
114
|
-
const ids = new Set(this.
|
|
207
|
+
const ids = new Set([...this.latestAcceptBySender.values()].map((a) => a.proposalId));
|
|
115
208
|
if (ids.size === 1)
|
|
116
209
|
return ids.values().next().value;
|
|
117
210
|
return undefined;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ProjectionAnomaly } from './base';
|
|
1
2
|
import type { Envelope } from '../types';
|
|
2
3
|
import type { ProtoRegistry } from '../proto-registry';
|
|
3
4
|
export interface ApprovalRequestRecord {
|
|
@@ -16,10 +17,52 @@ export interface BallotRecord {
|
|
|
16
17
|
export declare class QuorumProjection {
|
|
17
18
|
readonly requests: Map<string, ApprovalRequestRecord>;
|
|
18
19
|
readonly ballots: Map<string, Map<string, BallotRecord>>;
|
|
20
|
+
/**
|
|
21
|
+
* The session's accepted history, one envelope per unique `message_id`. See
|
|
22
|
+
* `BaseProjection.transcript` (`src/projections/base.ts`) for the full
|
|
23
|
+
* redelivery-idempotence contract (RFC-MACP-0006 §3.2); duplicated here
|
|
24
|
+
* only as a one-line pointer.
|
|
25
|
+
*/
|
|
19
26
|
readonly transcript: Envelope[];
|
|
27
|
+
/**
|
|
28
|
+
* Cardinality anomalies recorded while replaying this projection's accepted
|
|
29
|
+
* transcript (e.g. a duplicate ballot across `Approve`/`Reject`/`Abstain`
|
|
30
|
+
* from the same sender for the same `request_id`). See
|
|
31
|
+
* `BaseProjection.anomalies` (`src/projections/base.ts`) for the canonical
|
|
32
|
+
* description; duplicated here only as a one-line pointer.
|
|
33
|
+
*/
|
|
34
|
+
readonly anomalies: ProjectionAnomaly[];
|
|
20
35
|
phase: 'Pending' | 'Voting' | 'Committed';
|
|
21
36
|
commitment?: Record<string, unknown>;
|
|
37
|
+
/**
|
|
38
|
+
* `message_id`s already applied to this projection. See
|
|
39
|
+
* `BaseProjection.seenMessageIds` (`src/projections/base.ts`) for the full
|
|
40
|
+
* redelivery-idempotence rationale (RFC-MACP-0006 §3.2); duplicated here
|
|
41
|
+
* only as a one-line pointer.
|
|
42
|
+
*/
|
|
43
|
+
private readonly seenMessageIds;
|
|
44
|
+
/**
|
|
45
|
+
* Apply one envelope to this projection's in-process state.
|
|
46
|
+
*
|
|
47
|
+
* Input contract: **accepted-only**, caller-maintained (`Envelope` carries
|
|
48
|
+
* no acceptance marker). Canonical source: `schemas/conformance/README.md`
|
|
49
|
+
* "Notes:", RFC-MACP-0007 §5.3, RFC-MACP-0011 §5. Full rationale and failure
|
|
50
|
+
* mode are documented once, on `BaseProjection.applyEnvelope`
|
|
51
|
+
* (`src/projections/base.ts`), and duplicated here only as a one-line
|
|
52
|
+
* pointer so six independent copies of the same prose cannot drift.
|
|
53
|
+
*
|
|
54
|
+
* Redelivery idempotence: a redelivered envelope (same `message_id`) is a
|
|
55
|
+
* non-event — not appended to `transcript`, not passed to the `switch`.
|
|
56
|
+
* See `BaseProjection.applyEnvelope` for the full RFC-MACP-0006 §3.2
|
|
57
|
+
* citation; duplicated here only as a one-line pointer.
|
|
58
|
+
*/
|
|
22
59
|
applyEnvelope(envelope: Envelope, protoRegistry: ProtoRegistry): void;
|
|
60
|
+
/**
|
|
61
|
+
* True once at least one `ProjectionAnomaly` has been recorded. See
|
|
62
|
+
* `BaseProjection.hasAnomalies` (`src/projections/base.ts`) for the
|
|
63
|
+
* canonical description; duplicated here only as a one-line pointer.
|
|
64
|
+
*/
|
|
65
|
+
get hasAnomalies(): boolean;
|
|
23
66
|
get isCommitted(): boolean;
|
|
24
67
|
get isPositiveOutcome(): boolean | undefined;
|
|
25
68
|
private setBallot;
|
|
@@ -2,15 +2,63 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.QuorumProjection = void 0;
|
|
4
4
|
const constants_1 = require("../constants");
|
|
5
|
+
const logging_1 = require("../logging");
|
|
5
6
|
class QuorumProjection {
|
|
6
7
|
requests = new Map();
|
|
7
8
|
ballots = new Map();
|
|
9
|
+
/**
|
|
10
|
+
* The session's accepted history, one envelope per unique `message_id`. See
|
|
11
|
+
* `BaseProjection.transcript` (`src/projections/base.ts`) for the full
|
|
12
|
+
* redelivery-idempotence contract (RFC-MACP-0006 §3.2); duplicated here
|
|
13
|
+
* only as a one-line pointer.
|
|
14
|
+
*/
|
|
8
15
|
transcript = [];
|
|
16
|
+
/**
|
|
17
|
+
* Cardinality anomalies recorded while replaying this projection's accepted
|
|
18
|
+
* transcript (e.g. a duplicate ballot across `Approve`/`Reject`/`Abstain`
|
|
19
|
+
* from the same sender for the same `request_id`). See
|
|
20
|
+
* `BaseProjection.anomalies` (`src/projections/base.ts`) for the canonical
|
|
21
|
+
* description; duplicated here only as a one-line pointer.
|
|
22
|
+
*/
|
|
23
|
+
anomalies = [];
|
|
9
24
|
phase = 'Pending';
|
|
10
25
|
commitment;
|
|
26
|
+
/**
|
|
27
|
+
* `message_id`s already applied to this projection. See
|
|
28
|
+
* `BaseProjection.seenMessageIds` (`src/projections/base.ts`) for the full
|
|
29
|
+
* redelivery-idempotence rationale (RFC-MACP-0006 §3.2); duplicated here
|
|
30
|
+
* only as a one-line pointer.
|
|
31
|
+
*/
|
|
32
|
+
seenMessageIds = new Set();
|
|
33
|
+
/**
|
|
34
|
+
* Apply one envelope to this projection's in-process state.
|
|
35
|
+
*
|
|
36
|
+
* Input contract: **accepted-only**, caller-maintained (`Envelope` carries
|
|
37
|
+
* no acceptance marker). Canonical source: `schemas/conformance/README.md`
|
|
38
|
+
* "Notes:", RFC-MACP-0007 §5.3, RFC-MACP-0011 §5. Full rationale and failure
|
|
39
|
+
* mode are documented once, on `BaseProjection.applyEnvelope`
|
|
40
|
+
* (`src/projections/base.ts`), and duplicated here only as a one-line
|
|
41
|
+
* pointer so six independent copies of the same prose cannot drift.
|
|
42
|
+
*
|
|
43
|
+
* Redelivery idempotence: a redelivered envelope (same `message_id`) is a
|
|
44
|
+
* non-event — not appended to `transcript`, not passed to the `switch`.
|
|
45
|
+
* See `BaseProjection.applyEnvelope` for the full RFC-MACP-0006 §3.2
|
|
46
|
+
* citation; duplicated here only as a one-line pointer.
|
|
47
|
+
*/
|
|
11
48
|
applyEnvelope(envelope, protoRegistry) {
|
|
12
49
|
if (envelope.mode !== constants_1.MODE_QUORUM)
|
|
13
50
|
return;
|
|
51
|
+
if (envelope.messageId) {
|
|
52
|
+
if (this.seenMessageIds.has(envelope.messageId)) {
|
|
53
|
+
logging_1.logger.debug('projection redelivery ignored', {
|
|
54
|
+
messageId: envelope.messageId,
|
|
55
|
+
mode: envelope.mode,
|
|
56
|
+
messageType: envelope.messageType,
|
|
57
|
+
});
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
this.seenMessageIds.add(envelope.messageId);
|
|
61
|
+
}
|
|
14
62
|
this.transcript.push(envelope);
|
|
15
63
|
const payload = protoRegistry.decodeKnownPayload(envelope.mode, envelope.messageType, envelope.payload);
|
|
16
64
|
switch (envelope.messageType) {
|
|
@@ -22,17 +70,17 @@ class QuorumProjection {
|
|
|
22
70
|
}
|
|
23
71
|
case 'Approve': {
|
|
24
72
|
const record = payload;
|
|
25
|
-
this.setBallot(record.requestId,
|
|
73
|
+
this.setBallot(envelope, record.requestId, 'approve', record.reason);
|
|
26
74
|
break;
|
|
27
75
|
}
|
|
28
76
|
case 'Reject': {
|
|
29
77
|
const record = payload;
|
|
30
|
-
this.setBallot(record.requestId,
|
|
78
|
+
this.setBallot(envelope, record.requestId, 'reject', record.reason);
|
|
31
79
|
break;
|
|
32
80
|
}
|
|
33
81
|
case 'Abstain': {
|
|
34
82
|
const record = payload;
|
|
35
|
-
this.setBallot(record.requestId,
|
|
83
|
+
this.setBallot(envelope, record.requestId, 'abstain', record.reason);
|
|
36
84
|
break;
|
|
37
85
|
}
|
|
38
86
|
case 'Commitment': {
|
|
@@ -44,6 +92,14 @@ class QuorumProjection {
|
|
|
44
92
|
break;
|
|
45
93
|
}
|
|
46
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* True once at least one `ProjectionAnomaly` has been recorded. See
|
|
97
|
+
* `BaseProjection.hasAnomalies` (`src/projections/base.ts`) for the
|
|
98
|
+
* canonical description; duplicated here only as a one-line pointer.
|
|
99
|
+
*/
|
|
100
|
+
get hasAnomalies() {
|
|
101
|
+
return this.anomalies.length > 0;
|
|
102
|
+
}
|
|
47
103
|
get isCommitted() {
|
|
48
104
|
return this.commitment !== undefined;
|
|
49
105
|
}
|
|
@@ -54,9 +110,39 @@ class QuorumProjection {
|
|
|
54
110
|
this.commitment.outcome_positive;
|
|
55
111
|
return val !== undefined ? Boolean(val) : true;
|
|
56
112
|
}
|
|
57
|
-
setBallot(
|
|
113
|
+
setBallot(envelope, requestId, vote, reason) {
|
|
58
114
|
const senderMap = this.ballots.get(requestId) ?? new Map();
|
|
59
|
-
senderMap.
|
|
115
|
+
const kept = senderMap.get(envelope.sender);
|
|
116
|
+
if (kept !== undefined) {
|
|
117
|
+
// RFC-MACP-0011 §5 rule 3 (`:67`): participation is MAY (a participant need
|
|
118
|
+
// not cast a ballot), but the cap of one ballot per participant ACROSS
|
|
119
|
+
// Approve, Reject, and Abstain is enforced under §5's opening sentence,
|
|
120
|
+
// "Implementations MUST enforce the following:" (`:63`). Do NOT phrase this
|
|
121
|
+
// as "a participant MUST cast at most one ballot" — RFC-0011 puts the MUST
|
|
122
|
+
// on the implementation, not the participant (contrast RFC-MACP-0007 §5.3,
|
|
123
|
+
// which puts it directly on the participant; same obligation, different
|
|
124
|
+
// addressee). A later ballot of a *different* type is still a duplicate,
|
|
125
|
+
// not a change of vote, so this guard is keyed on the sender ALONE within
|
|
126
|
+
// the request — never on sender + vote. macp-runtime enforces this
|
|
127
|
+
// identically in all three arms (quorum.rs:164/184/204).
|
|
128
|
+
// Keeping the sender's FIRST ballot (rather than the last) is not stated
|
|
129
|
+
// by RFC-0011 either; it is parity with RFC-MACP-0007 §5 item 3's
|
|
130
|
+
// explicit first-stands rule for `Vote`, plus macp-runtime's enforced
|
|
131
|
+
// first-wins behaviour.
|
|
132
|
+
const anomaly = {
|
|
133
|
+
kind: 'duplicate_ballot',
|
|
134
|
+
mode: envelope.mode,
|
|
135
|
+
messageType: envelope.messageType,
|
|
136
|
+
messageId: envelope.messageId,
|
|
137
|
+
sender: envelope.sender,
|
|
138
|
+
subjectId: requestId,
|
|
139
|
+
detail: `sender ${envelope.sender} already cast '${kept.vote}' on request ${requestId}; discarded '${vote}'`,
|
|
140
|
+
};
|
|
141
|
+
this.anomalies.push(anomaly);
|
|
142
|
+
logging_1.logger.warn('projection anomaly', anomaly);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
senderMap.set(envelope.sender, { requestId, vote, reason, sender: envelope.sender });
|
|
60
146
|
this.ballots.set(requestId, senderMap);
|
|
61
147
|
}
|
|
62
148
|
approvalCount(requestId) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ProjectionAnomaly } from './base';
|
|
1
2
|
import type { Envelope } from '../types';
|
|
2
3
|
import type { ProtoRegistry } from '../proto-registry';
|
|
3
4
|
export interface TaskRecord {
|
|
@@ -37,10 +38,51 @@ export declare class TaskProjection {
|
|
|
37
38
|
readonly updates: TaskUpdateRecord[];
|
|
38
39
|
readonly completions: TaskCompletionRecord[];
|
|
39
40
|
readonly failures: TaskFailureRecord[];
|
|
41
|
+
/**
|
|
42
|
+
* The session's accepted history, one envelope per unique `message_id`. See
|
|
43
|
+
* `BaseProjection.transcript` (`src/projections/base.ts`) for the full
|
|
44
|
+
* redelivery-idempotence contract (RFC-MACP-0006 §3.2); duplicated here
|
|
45
|
+
* only as a one-line pointer.
|
|
46
|
+
*/
|
|
40
47
|
readonly transcript: Envelope[];
|
|
48
|
+
/**
|
|
49
|
+
* Cardinality anomalies recorded while replaying this projection's accepted
|
|
50
|
+
* transcript. See `BaseProjection.anomalies` (`src/projections/base.ts`)
|
|
51
|
+
* for the canonical description; duplicated here only as a one-line
|
|
52
|
+
* pointer. No built-in detection populates this for Task mode.
|
|
53
|
+
*/
|
|
54
|
+
readonly anomalies: ProjectionAnomaly[];
|
|
41
55
|
phase: 'Pending' | 'Requested' | 'InProgress' | 'Completed' | 'Failed' | 'Committed';
|
|
42
56
|
commitment?: Record<string, unknown>;
|
|
57
|
+
/**
|
|
58
|
+
* `message_id`s already applied to this projection. See
|
|
59
|
+
* `BaseProjection.seenMessageIds` (`src/projections/base.ts`) for the full
|
|
60
|
+
* redelivery-idempotence rationale (RFC-MACP-0006 §3.2); duplicated here
|
|
61
|
+
* only as a one-line pointer.
|
|
62
|
+
*/
|
|
63
|
+
private readonly seenMessageIds;
|
|
64
|
+
/**
|
|
65
|
+
* Apply one envelope to this projection's in-process state.
|
|
66
|
+
*
|
|
67
|
+
* Input contract: **accepted-only**, caller-maintained (`Envelope` carries
|
|
68
|
+
* no acceptance marker). Canonical source: `schemas/conformance/README.md`
|
|
69
|
+
* "Notes:", RFC-MACP-0007 §5.3, RFC-MACP-0011 §5. Full rationale and failure
|
|
70
|
+
* mode are documented once, on `BaseProjection.applyEnvelope`
|
|
71
|
+
* (`src/projections/base.ts`), and duplicated here only as a one-line
|
|
72
|
+
* pointer so six independent copies of the same prose cannot drift.
|
|
73
|
+
*
|
|
74
|
+
* Redelivery idempotence: a redelivered envelope (same `message_id`) is a
|
|
75
|
+
* non-event — not appended to `transcript`, not passed to the `switch`.
|
|
76
|
+
* See `BaseProjection.applyEnvelope` for the full RFC-MACP-0006 §3.2
|
|
77
|
+
* citation; duplicated here only as a one-line pointer.
|
|
78
|
+
*/
|
|
43
79
|
applyEnvelope(envelope: Envelope, protoRegistry: ProtoRegistry): void;
|
|
80
|
+
/**
|
|
81
|
+
* True once at least one `ProjectionAnomaly` has been recorded. See
|
|
82
|
+
* `BaseProjection.hasAnomalies` (`src/projections/base.ts`) for the
|
|
83
|
+
* canonical description; duplicated here only as a one-line pointer.
|
|
84
|
+
*/
|
|
85
|
+
get hasAnomalies(): boolean;
|
|
44
86
|
get isCommitted(): boolean;
|
|
45
87
|
get isPositiveOutcome(): boolean | undefined;
|
|
46
88
|
getTask(taskId: string): TaskRecord | undefined;
|
package/dist/projections/task.js
CHANGED
|
@@ -2,17 +2,64 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.TaskProjection = void 0;
|
|
4
4
|
const constants_1 = require("../constants");
|
|
5
|
+
const logging_1 = require("../logging");
|
|
5
6
|
class TaskProjection {
|
|
6
7
|
tasks = new Map();
|
|
7
8
|
updates = [];
|
|
8
9
|
completions = [];
|
|
9
10
|
failures = [];
|
|
11
|
+
/**
|
|
12
|
+
* The session's accepted history, one envelope per unique `message_id`. See
|
|
13
|
+
* `BaseProjection.transcript` (`src/projections/base.ts`) for the full
|
|
14
|
+
* redelivery-idempotence contract (RFC-MACP-0006 §3.2); duplicated here
|
|
15
|
+
* only as a one-line pointer.
|
|
16
|
+
*/
|
|
10
17
|
transcript = [];
|
|
18
|
+
/**
|
|
19
|
+
* Cardinality anomalies recorded while replaying this projection's accepted
|
|
20
|
+
* transcript. See `BaseProjection.anomalies` (`src/projections/base.ts`)
|
|
21
|
+
* for the canonical description; duplicated here only as a one-line
|
|
22
|
+
* pointer. No built-in detection populates this for Task mode.
|
|
23
|
+
*/
|
|
24
|
+
anomalies = [];
|
|
11
25
|
phase = 'Pending';
|
|
12
26
|
commitment;
|
|
27
|
+
/**
|
|
28
|
+
* `message_id`s already applied to this projection. See
|
|
29
|
+
* `BaseProjection.seenMessageIds` (`src/projections/base.ts`) for the full
|
|
30
|
+
* redelivery-idempotence rationale (RFC-MACP-0006 §3.2); duplicated here
|
|
31
|
+
* only as a one-line pointer.
|
|
32
|
+
*/
|
|
33
|
+
seenMessageIds = new Set();
|
|
34
|
+
/**
|
|
35
|
+
* Apply one envelope to this projection's in-process state.
|
|
36
|
+
*
|
|
37
|
+
* Input contract: **accepted-only**, caller-maintained (`Envelope` carries
|
|
38
|
+
* no acceptance marker). Canonical source: `schemas/conformance/README.md`
|
|
39
|
+
* "Notes:", RFC-MACP-0007 §5.3, RFC-MACP-0011 §5. Full rationale and failure
|
|
40
|
+
* mode are documented once, on `BaseProjection.applyEnvelope`
|
|
41
|
+
* (`src/projections/base.ts`), and duplicated here only as a one-line
|
|
42
|
+
* pointer so six independent copies of the same prose cannot drift.
|
|
43
|
+
*
|
|
44
|
+
* Redelivery idempotence: a redelivered envelope (same `message_id`) is a
|
|
45
|
+
* non-event — not appended to `transcript`, not passed to the `switch`.
|
|
46
|
+
* See `BaseProjection.applyEnvelope` for the full RFC-MACP-0006 §3.2
|
|
47
|
+
* citation; duplicated here only as a one-line pointer.
|
|
48
|
+
*/
|
|
13
49
|
applyEnvelope(envelope, protoRegistry) {
|
|
14
50
|
if (envelope.mode !== constants_1.MODE_TASK)
|
|
15
51
|
return;
|
|
52
|
+
if (envelope.messageId) {
|
|
53
|
+
if (this.seenMessageIds.has(envelope.messageId)) {
|
|
54
|
+
logging_1.logger.debug('projection redelivery ignored', {
|
|
55
|
+
messageId: envelope.messageId,
|
|
56
|
+
mode: envelope.mode,
|
|
57
|
+
messageType: envelope.messageType,
|
|
58
|
+
});
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
this.seenMessageIds.add(envelope.messageId);
|
|
62
|
+
}
|
|
16
63
|
this.transcript.push(envelope);
|
|
17
64
|
const payload = protoRegistry.decodeKnownPayload(envelope.mode, envelope.messageType, envelope.payload);
|
|
18
65
|
switch (envelope.messageType) {
|
|
@@ -34,7 +81,30 @@ class TaskProjection {
|
|
|
34
81
|
case 'TaskAccept': {
|
|
35
82
|
const record = payload;
|
|
36
83
|
const task = this.tasks.get(record.taskId);
|
|
37
|
-
|
|
84
|
+
// RFC-MACP-0009 §5 rules 3/3a (`:69-71`): "Only one assignee may
|
|
85
|
+
// become active for the Session in base v1. The first accepted
|
|
86
|
+
// `TaskAccept` from any eligible participant designates that
|
|
87
|
+
// participant as the active assignee. Subsequent `TaskAccept`
|
|
88
|
+
// messages for the same session MUST be rejected if an active
|
|
89
|
+
// assignee is already designated." A conforming runtime rejects the
|
|
90
|
+
// second `TaskAccept` before it reaches accepted history; this guard
|
|
91
|
+
// makes the projection first-accept-wins too, so a rogue second one
|
|
92
|
+
// (e.g. an unfiltered transcript) cannot silently reassign the task.
|
|
93
|
+
//
|
|
94
|
+
// Rule 3c's policy-gated reassignment path (`allow_reassignment_on_reject`,
|
|
95
|
+
// RFC-MACP-0012 `:135`) is deliberately NOT modelled here:
|
|
96
|
+
// `TaskProjection` has no session-policy input today, and rule 3b
|
|
97
|
+
// makes an unconditional reassignment wrong by default ("`TaskAccept`
|
|
98
|
+
// is irrevocable unless policy explicitly permits reassignment").
|
|
99
|
+
// Plumbing policy into this projection needs its own decision (see
|
|
100
|
+
// plans/cross-repo/macp-sdk-typescript-conformance-and-transport.md,
|
|
101
|
+
// Phase 3 open question 4) before that path can be implemented.
|
|
102
|
+
//
|
|
103
|
+
// An anomaly would be recorded when this guard discards a second
|
|
104
|
+
// `TaskAccept`, but `ProjectionAnomalyKind` (`base.ts:8-9`) is
|
|
105
|
+
// deliberately frozen pending cross-SDK agreement with
|
|
106
|
+
// macp-sdk-python.
|
|
107
|
+
if (task && task.assignee === undefined) {
|
|
38
108
|
task.assignee = record.assignee;
|
|
39
109
|
task.status = 'accepted';
|
|
40
110
|
}
|
|
@@ -44,6 +114,11 @@ class TaskProjection {
|
|
|
44
114
|
case 'TaskReject': {
|
|
45
115
|
const record = payload;
|
|
46
116
|
const task = this.tasks.get(record.taskId);
|
|
117
|
+
// Deliberately does not clear `task.assignee`: rule 3c's
|
|
118
|
+
// policy-gated reassignment path is not modelled here (see the
|
|
119
|
+
// `TaskAccept` case above), so `assignee` is treated as sticky once
|
|
120
|
+
// set, per rule 3b's default ("irrevocable unless policy explicitly
|
|
121
|
+
// permits reassignment").
|
|
47
122
|
if (task)
|
|
48
123
|
task.status = 'rejected';
|
|
49
124
|
break;
|
|
@@ -87,6 +162,14 @@ class TaskProjection {
|
|
|
87
162
|
break;
|
|
88
163
|
}
|
|
89
164
|
}
|
|
165
|
+
/**
|
|
166
|
+
* True once at least one `ProjectionAnomaly` has been recorded. See
|
|
167
|
+
* `BaseProjection.hasAnomalies` (`src/projections/base.ts`) for the
|
|
168
|
+
* canonical description; duplicated here only as a one-line pointer.
|
|
169
|
+
*/
|
|
170
|
+
get hasAnomalies() {
|
|
171
|
+
return this.anomalies.length > 0;
|
|
172
|
+
}
|
|
90
173
|
get isCommitted() {
|
|
91
174
|
return this.commitment !== undefined;
|
|
92
175
|
}
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "macp-sdk-typescript",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "TypeScript SDK for the MACP Rust runtime",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"dependencies": {
|
|
56
56
|
"@grpc/grpc-js": "^1.14.3",
|
|
57
57
|
"@grpc/proto-loader": "^0.8.0",
|
|
58
|
-
"@multiagentcoordinationprotocol/proto": "^0.1.
|
|
58
|
+
"@multiagentcoordinationprotocol/proto": "^0.1.9",
|
|
59
59
|
"protobufjs": "^8.6.5"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|