macp-sdk-typescript 0.10.0 → 0.12.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 +465 -0
- package/README.md +2 -2
- package/dist/agent/index.d.ts +1 -1
- package/dist/agent/index.js +2 -1
- package/dist/agent/participant.d.ts +47 -4
- package/dist/agent/participant.js +88 -10
- package/dist/agent/strategies.js +23 -7
- package/dist/agent/transports.d.ts +19 -1
- package/dist/agent/transports.js +108 -66
- package/dist/auth.d.ts +15 -4
- package/dist/auth.js +23 -7
- package/dist/base-session.js +13 -3
- package/dist/client.js +58 -15
- package/dist/commitment-hash.d.ts +33 -3
- package/dist/commitment-hash.js +39 -5
- package/dist/decision.d.ts +1 -0
- package/dist/decision.js +11 -9
- package/dist/envelope.js +9 -1
- package/dist/handoff.d.ts +1 -0
- package/dist/handoff.js +19 -6
- package/dist/index.d.ts +2 -2
- package/dist/index.js +18 -7
- package/dist/policy.d.ts +54 -7
- package/dist/policy.js +143 -19
- package/dist/projections/base.d.ts +71 -14
- package/dist/projections/base.js +106 -16
- package/dist/projections/decision.d.ts +5 -49
- package/dist/projections/decision.js +30 -85
- package/dist/projections/handoff.d.ts +5 -48
- package/dist/projections/handoff.js +44 -93
- package/dist/projections/proposal.d.ts +5 -48
- package/dist/projections/proposal.js +21 -79
- package/dist/projections/quorum.d.ts +5 -49
- package/dist/projections/quorum.js +20 -82
- package/dist/projections/task.d.ts +31 -51
- package/dist/projections/task.js +49 -90
- package/dist/proposal.d.ts +1 -0
- package/dist/proposal.js +2 -2
- package/dist/proto-registry.d.ts +85 -5
- package/dist/proto-registry.js +101 -15
- package/dist/quorum.d.ts +1 -0
- package/dist/quorum.js +4 -4
- package/dist/task.d.ts +1 -0
- package/dist/task.js +24 -11
- package/dist/types.d.ts +14 -7
- package/dist/validation.d.ts +14 -0
- package/dist/validation.js +39 -5
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +6 -5
|
@@ -2,66 +2,16 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.DecisionProjection = void 0;
|
|
4
4
|
const constants_1 = require("../constants");
|
|
5
|
-
const
|
|
6
|
-
class DecisionProjection {
|
|
5
|
+
const base_1 = require("./base");
|
|
6
|
+
class DecisionProjection extends base_1.BaseProjection {
|
|
7
|
+
mode = constants_1.MODE_DECISION;
|
|
7
8
|
proposals = new Map();
|
|
8
9
|
evaluations = [];
|
|
9
10
|
objections = [];
|
|
10
11
|
votes = new Map();
|
|
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
|
-
*/
|
|
17
|
-
transcript = [];
|
|
18
|
-
/**
|
|
19
|
-
* Cardinality anomalies recorded while replaying this projection's accepted
|
|
20
|
-
* transcript (e.g. a duplicate `Vote` from the same sender for the same
|
|
21
|
-
* `proposal_id`). See `BaseProjection.anomalies` (`src/projections/base.ts`)
|
|
22
|
-
* for the canonical description; duplicated here only as a one-line
|
|
23
|
-
* pointer.
|
|
24
|
-
*/
|
|
25
|
-
anomalies = [];
|
|
26
12
|
phase = 'Proposal';
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
* `message_id`s already applied to this projection. See
|
|
30
|
-
* `BaseProjection.seenMessageIds` (`src/projections/base.ts`) for the full
|
|
31
|
-
* redelivery-idempotence rationale (RFC-MACP-0006 §3.2); duplicated here
|
|
32
|
-
* only as a one-line pointer.
|
|
33
|
-
*/
|
|
34
|
-
seenMessageIds = new Set();
|
|
35
|
-
/**
|
|
36
|
-
* Apply one envelope to this projection's in-process state.
|
|
37
|
-
*
|
|
38
|
-
* Input contract: **accepted-only**, caller-maintained (`Envelope` carries
|
|
39
|
-
* no acceptance marker). Canonical source: `schemas/conformance/README.md`
|
|
40
|
-
* "Notes:", RFC-MACP-0007 §5.3, RFC-MACP-0011 §5. Full rationale and failure
|
|
41
|
-
* mode are documented once, on `BaseProjection.applyEnvelope`
|
|
42
|
-
* (`src/projections/base.ts`), and duplicated here only as a one-line
|
|
43
|
-
* pointer so six independent copies of the same prose cannot drift.
|
|
44
|
-
*
|
|
45
|
-
* Redelivery idempotence: a redelivered envelope (same `message_id`) is a
|
|
46
|
-
* non-event — not appended to `transcript`, not passed to the `switch`.
|
|
47
|
-
* See `BaseProjection.applyEnvelope` for the full RFC-MACP-0006 §3.2
|
|
48
|
-
* citation; duplicated here only as a one-line pointer.
|
|
49
|
-
*/
|
|
50
|
-
applyEnvelope(envelope, protoRegistry) {
|
|
51
|
-
if (envelope.mode !== constants_1.MODE_DECISION)
|
|
52
|
-
return;
|
|
53
|
-
if (envelope.messageId) {
|
|
54
|
-
if (this.seenMessageIds.has(envelope.messageId)) {
|
|
55
|
-
logging_1.logger.debug('projection redelivery ignored', {
|
|
56
|
-
messageId: envelope.messageId,
|
|
57
|
-
mode: envelope.mode,
|
|
58
|
-
messageType: envelope.messageType,
|
|
59
|
-
});
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
this.seenMessageIds.add(envelope.messageId);
|
|
63
|
-
}
|
|
64
|
-
this.transcript.push(envelope);
|
|
13
|
+
/** Handle a Decision-mode envelope. See `BaseProjection.applyEnvelope` for the accepted-only input contract and redelivery/rollback semantics shared by all built-in modes. */
|
|
14
|
+
applyMode(envelope, protoRegistry) {
|
|
65
15
|
const payload = protoRegistry.decodeKnownPayload(envelope.mode, envelope.messageType, envelope.payload);
|
|
66
16
|
switch (envelope.messageType) {
|
|
67
17
|
case 'Proposal': {
|
|
@@ -87,6 +37,23 @@ class DecisionProjection {
|
|
|
87
37
|
}
|
|
88
38
|
case 'Vote': {
|
|
89
39
|
const record = payload;
|
|
40
|
+
// RFC-MACP-0007 §5 rule 2: "Evaluation, Objection, and Vote MUST
|
|
41
|
+
// reference an existing proposal_id." A conforming runtime never
|
|
42
|
+
// admits a Vote for an unknown proposal_id into accepted history, but
|
|
43
|
+
// per the accepted-only input contract (`BaseProjection.applyEnvelope`)
|
|
44
|
+
// this guards the same class of unfiltered-transcript input that
|
|
45
|
+
// `HandoffAccept`/`HandoffDecline` already guard against (issue #119,
|
|
46
|
+
// mirrors issue #111's task.ts fix). Evaluation/Objection need no
|
|
47
|
+
// matching guard: they only append to audit arrays (see
|
|
48
|
+
// `docs/api/projections.md` "List accumulators and logical
|
|
49
|
+
// duplicates"), so an unknown-proposalId record there fabricates no
|
|
50
|
+
// Map state and moves no phase. Vote is different: without this
|
|
51
|
+
// guard, `this.votes.get(...) ?? new Map()` below would fabricate a
|
|
52
|
+
// new `votes` entry for an unknown proposalId, inflating
|
|
53
|
+
// `voteTotals()`/`majorityWinner()`'s denominators even though no
|
|
54
|
+
// known proposal exists.
|
|
55
|
+
if (!this.proposals.has(record.proposalId))
|
|
56
|
+
break;
|
|
90
57
|
const bySender = this.votes.get(record.proposalId) ?? new Map();
|
|
91
58
|
const kept = bySender.get(envelope.sender);
|
|
92
59
|
if (kept !== undefined) {
|
|
@@ -94,7 +61,7 @@ class DecisionProjection {
|
|
|
94
61
|
// runtime NACKs the duplicate (macp-runtime decision.rs), so reaching
|
|
95
62
|
// here means the transcript was not filtered to a conforming
|
|
96
63
|
// runtime's accepted history.
|
|
97
|
-
|
|
64
|
+
this.recordAnomaly({
|
|
98
65
|
kind: 'duplicate_vote',
|
|
99
66
|
mode: envelope.mode,
|
|
100
67
|
messageType: envelope.messageType,
|
|
@@ -102,9 +69,7 @@ class DecisionProjection {
|
|
|
102
69
|
sender: envelope.sender,
|
|
103
70
|
subjectId: record.proposalId,
|
|
104
71
|
detail: `sender ${envelope.sender} already voted '${kept.vote}' on proposal ${record.proposalId}; discarded '${record.vote}'`,
|
|
105
|
-
};
|
|
106
|
-
this.anomalies.push(anomaly);
|
|
107
|
-
logging_1.logger.warn('projection anomaly', anomaly);
|
|
72
|
+
});
|
|
108
73
|
break;
|
|
109
74
|
}
|
|
110
75
|
bySender.set(envelope.sender, { ...record, sender: envelope.sender });
|
|
@@ -120,41 +85,21 @@ class DecisionProjection {
|
|
|
120
85
|
// not a normative MACP term (it appears once, in passing, at
|
|
121
86
|
// RFC-MACP-0012 `:211`); this guard is justified by session
|
|
122
87
|
// terminality, not by a phase specification. An anomaly would be
|
|
123
|
-
// recorded here too, but
|
|
124
|
-
//
|
|
125
|
-
//
|
|
88
|
+
// recorded here too, but unlike five of the six sites that were
|
|
89
|
+
// "frozen pending cross-SDK agreement" (issue #126/#128, now
|
|
90
|
+
// resolved for `duplicate_task_accept`/`settled_handoff` — see
|
|
91
|
+
// `base.ts`), this one is a session-terminality phase-regression
|
|
92
|
+
// guard, not a settlement discard, and remains genuinely undecided
|
|
93
|
+
// per #128's own explicit scoping. No behavior change here.
|
|
126
94
|
if (this.phase !== 'Committed') {
|
|
127
95
|
this.phase = 'Voting';
|
|
128
96
|
}
|
|
129
97
|
break;
|
|
130
98
|
}
|
|
131
|
-
case 'Commitment': {
|
|
132
|
-
this.commitment = payload;
|
|
133
|
-
this.phase = 'Committed';
|
|
134
|
-
break;
|
|
135
|
-
}
|
|
136
99
|
default:
|
|
137
100
|
break;
|
|
138
101
|
}
|
|
139
102
|
}
|
|
140
|
-
/**
|
|
141
|
-
* True once at least one `ProjectionAnomaly` has been recorded. See
|
|
142
|
-
* `BaseProjection.hasAnomalies` (`src/projections/base.ts`) for the
|
|
143
|
-
* canonical description; duplicated here only as a one-line pointer.
|
|
144
|
-
*/
|
|
145
|
-
get hasAnomalies() {
|
|
146
|
-
return this.anomalies.length > 0;
|
|
147
|
-
}
|
|
148
|
-
get isCommitted() {
|
|
149
|
-
return this.commitment !== undefined;
|
|
150
|
-
}
|
|
151
|
-
get isPositiveOutcome() {
|
|
152
|
-
if (!this.commitment)
|
|
153
|
-
return undefined;
|
|
154
|
-
const val = this.commitment.outcomePositive ??
|
|
155
|
-
this.commitment.outcome_positive;
|
|
156
|
-
return val !== undefined ? Boolean(val) : true;
|
|
157
|
-
}
|
|
158
103
|
voteTotals() {
|
|
159
104
|
const totals = {};
|
|
160
105
|
for (const [proposalId, senderVotes] of this.votes.entries()) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { BaseProjection } from './base';
|
|
2
2
|
import type { Envelope } from '../types';
|
|
3
3
|
import type { ProtoRegistry } from '../proto-registry';
|
|
4
4
|
export interface HandoffRecord {
|
|
@@ -18,55 +18,12 @@ export interface HandoffRecord {
|
|
|
18
18
|
*/
|
|
19
19
|
implicit?: boolean;
|
|
20
20
|
}
|
|
21
|
-
export declare class HandoffProjection {
|
|
21
|
+
export declare class HandoffProjection extends BaseProjection {
|
|
22
|
+
protected readonly mode = "macp.mode.handoff.v1";
|
|
22
23
|
readonly handoffs: Map<string, HandoffRecord>;
|
|
23
|
-
/**
|
|
24
|
-
* The session's accepted history, one envelope per unique `message_id`. See
|
|
25
|
-
* `BaseProjection.transcript` (`src/projections/base.ts`) for the full
|
|
26
|
-
* redelivery-idempotence contract (RFC-MACP-0006 §3.2); duplicated here
|
|
27
|
-
* only as a one-line pointer.
|
|
28
|
-
*/
|
|
29
|
-
readonly transcript: Envelope[];
|
|
30
|
-
/**
|
|
31
|
-
* Cardinality anomalies recorded while replaying this projection's accepted
|
|
32
|
-
* transcript. See `BaseProjection.anomalies` (`src/projections/base.ts`)
|
|
33
|
-
* for the canonical description; duplicated here only as a one-line
|
|
34
|
-
* pointer. No built-in detection populates this for Handoff mode.
|
|
35
|
-
*/
|
|
36
|
-
readonly anomalies: ProjectionAnomaly[];
|
|
37
24
|
phase: 'Pending' | 'OfferPending' | 'ContextSharing' | 'Accepted' | 'Declined' | 'Committed';
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
* `message_id`s already applied to this projection. See
|
|
41
|
-
* `BaseProjection.seenMessageIds` (`src/projections/base.ts`) for the full
|
|
42
|
-
* redelivery-idempotence rationale (RFC-MACP-0006 §3.2); duplicated here
|
|
43
|
-
* only as a one-line pointer.
|
|
44
|
-
*/
|
|
45
|
-
private readonly seenMessageIds;
|
|
46
|
-
/**
|
|
47
|
-
* Apply one envelope to this projection's in-process state.
|
|
48
|
-
*
|
|
49
|
-
* Input contract: **accepted-only**, caller-maintained (`Envelope` carries
|
|
50
|
-
* no acceptance marker). Canonical source: `schemas/conformance/README.md`
|
|
51
|
-
* "Notes:", RFC-MACP-0007 §5.3, RFC-MACP-0011 §5. Full rationale and failure
|
|
52
|
-
* mode are documented once, on `BaseProjection.applyEnvelope`
|
|
53
|
-
* (`src/projections/base.ts`), and duplicated here only as a one-line
|
|
54
|
-
* pointer so six independent copies of the same prose cannot drift.
|
|
55
|
-
*
|
|
56
|
-
* Redelivery idempotence: a redelivered envelope (same `message_id`) is a
|
|
57
|
-
* non-event — not appended to `transcript`, not passed to the `switch`.
|
|
58
|
-
* See `BaseProjection.applyEnvelope` for the full RFC-MACP-0006 §3.2
|
|
59
|
-
* citation; duplicated here only as a one-line pointer.
|
|
60
|
-
*/
|
|
61
|
-
applyEnvelope(envelope: Envelope, protoRegistry: ProtoRegistry): void;
|
|
62
|
-
/**
|
|
63
|
-
* True once at least one `ProjectionAnomaly` has been recorded. See
|
|
64
|
-
* `BaseProjection.hasAnomalies` (`src/projections/base.ts`) for the
|
|
65
|
-
* canonical description; duplicated here only as a one-line pointer.
|
|
66
|
-
*/
|
|
67
|
-
get hasAnomalies(): boolean;
|
|
68
|
-
get isCommitted(): boolean;
|
|
69
|
-
get isPositiveOutcome(): boolean | undefined;
|
|
25
|
+
/** Handle a Handoff-mode envelope. See `BaseProjection.applyEnvelope` for the accepted-only input contract and redelivery/rollback semantics shared by all built-in modes. */
|
|
26
|
+
protected applyMode(envelope: Envelope, protoRegistry: ProtoRegistry): void;
|
|
70
27
|
getHandoff(handoffId: string): HandoffRecord | undefined;
|
|
71
28
|
isAccepted(handoffId: string): boolean;
|
|
72
29
|
isDeclined(handoffId: string): boolean;
|
|
@@ -2,62 +2,13 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.HandoffProjection = void 0;
|
|
4
4
|
const constants_1 = require("../constants");
|
|
5
|
-
const
|
|
6
|
-
class HandoffProjection {
|
|
5
|
+
const base_1 = require("./base");
|
|
6
|
+
class HandoffProjection extends base_1.BaseProjection {
|
|
7
|
+
mode = constants_1.MODE_HANDOFF;
|
|
7
8
|
handoffs = new Map();
|
|
8
|
-
/**
|
|
9
|
-
* The session's accepted history, one envelope per unique `message_id`. See
|
|
10
|
-
* `BaseProjection.transcript` (`src/projections/base.ts`) for the full
|
|
11
|
-
* redelivery-idempotence contract (RFC-MACP-0006 §3.2); duplicated here
|
|
12
|
-
* only as a one-line pointer.
|
|
13
|
-
*/
|
|
14
|
-
transcript = [];
|
|
15
|
-
/**
|
|
16
|
-
* Cardinality anomalies recorded while replaying this projection's accepted
|
|
17
|
-
* transcript. See `BaseProjection.anomalies` (`src/projections/base.ts`)
|
|
18
|
-
* for the canonical description; duplicated here only as a one-line
|
|
19
|
-
* pointer. No built-in detection populates this for Handoff mode.
|
|
20
|
-
*/
|
|
21
|
-
anomalies = [];
|
|
22
9
|
phase = 'Pending';
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
* `message_id`s already applied to this projection. See
|
|
26
|
-
* `BaseProjection.seenMessageIds` (`src/projections/base.ts`) for the full
|
|
27
|
-
* redelivery-idempotence rationale (RFC-MACP-0006 §3.2); duplicated here
|
|
28
|
-
* only as a one-line pointer.
|
|
29
|
-
*/
|
|
30
|
-
seenMessageIds = new Set();
|
|
31
|
-
/**
|
|
32
|
-
* Apply one envelope to this projection's in-process state.
|
|
33
|
-
*
|
|
34
|
-
* Input contract: **accepted-only**, caller-maintained (`Envelope` carries
|
|
35
|
-
* no acceptance marker). Canonical source: `schemas/conformance/README.md`
|
|
36
|
-
* "Notes:", RFC-MACP-0007 §5.3, RFC-MACP-0011 §5. Full rationale and failure
|
|
37
|
-
* mode are documented once, on `BaseProjection.applyEnvelope`
|
|
38
|
-
* (`src/projections/base.ts`), and duplicated here only as a one-line
|
|
39
|
-
* pointer so six independent copies of the same prose cannot drift.
|
|
40
|
-
*
|
|
41
|
-
* Redelivery idempotence: a redelivered envelope (same `message_id`) is a
|
|
42
|
-
* non-event — not appended to `transcript`, not passed to the `switch`.
|
|
43
|
-
* See `BaseProjection.applyEnvelope` for the full RFC-MACP-0006 §3.2
|
|
44
|
-
* citation; duplicated here only as a one-line pointer.
|
|
45
|
-
*/
|
|
46
|
-
applyEnvelope(envelope, protoRegistry) {
|
|
47
|
-
if (envelope.mode !== constants_1.MODE_HANDOFF)
|
|
48
|
-
return;
|
|
49
|
-
if (envelope.messageId) {
|
|
50
|
-
if (this.seenMessageIds.has(envelope.messageId)) {
|
|
51
|
-
logging_1.logger.debug('projection redelivery ignored', {
|
|
52
|
-
messageId: envelope.messageId,
|
|
53
|
-
mode: envelope.mode,
|
|
54
|
-
messageType: envelope.messageType,
|
|
55
|
-
});
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
this.seenMessageIds.add(envelope.messageId);
|
|
59
|
-
}
|
|
60
|
-
this.transcript.push(envelope);
|
|
10
|
+
/** Handle a Handoff-mode envelope. See `BaseProjection.applyEnvelope` for the accepted-only input contract and redelivery/rollback semantics shared by all built-in modes. */
|
|
11
|
+
applyMode(envelope, protoRegistry) {
|
|
61
12
|
const payload = protoRegistry.decodeKnownPayload(envelope.mode, envelope.messageType, envelope.payload);
|
|
62
13
|
switch (envelope.messageType) {
|
|
63
14
|
case 'HandoffOffer': {
|
|
@@ -76,6 +27,11 @@ class HandoffProjection {
|
|
|
76
27
|
case 'HandoffContext': {
|
|
77
28
|
const record = payload;
|
|
78
29
|
const handoff = this.handoffs.get(record.handoffId);
|
|
30
|
+
// RFC-MACP-0010 §5 rule 2: HandoffContext MUST reference an existing
|
|
31
|
+
// handoff_id, so the phase transition belongs inside this guard too
|
|
32
|
+
// (issue #119, same shape as issue #111) — a HandoffContext for an
|
|
33
|
+
// unknown handoff_id must not advance `phase` past `'OfferPending'`
|
|
34
|
+
// while a different, real handoff is still pending.
|
|
79
35
|
if (handoff) {
|
|
80
36
|
// Per RFC-MACP-0010 §2.1: context after accept is permitted as supplementary docs.
|
|
81
37
|
// Only update status if not already accepted/declined.
|
|
@@ -83,9 +39,9 @@ class HandoffProjection {
|
|
|
83
39
|
handoff.status = 'context_sent';
|
|
84
40
|
}
|
|
85
41
|
handoff.contextContentType = record.contentType;
|
|
42
|
+
if (this.phase === 'OfferPending')
|
|
43
|
+
this.phase = 'ContextSharing';
|
|
86
44
|
}
|
|
87
|
-
if (this.phase === 'OfferPending')
|
|
88
|
-
this.phase = 'ContextSharing';
|
|
89
45
|
break;
|
|
90
46
|
}
|
|
91
47
|
case 'HandoffAccept': {
|
|
@@ -96,9 +52,10 @@ class HandoffProjection {
|
|
|
96
52
|
// and HandoffDecline MUST reference an existing handoff_id." An
|
|
97
53
|
// accept for an unknown/never-offered handoff_id is invalid input
|
|
98
54
|
// (e.g. an unfiltered transcript) and MUST NOT mutate `phase` or
|
|
99
|
-
// fabricate a handoff record.
|
|
100
|
-
//
|
|
101
|
-
//
|
|
55
|
+
// fabricate a handoff record. Confirmed (issue #126/#128): no
|
|
56
|
+
// anomaly is recorded for this case — an unknown `handoff_id` is
|
|
57
|
+
// not caller misuse (a projection that joined mid-session may never
|
|
58
|
+
// have observed the original offer).
|
|
102
59
|
break;
|
|
103
60
|
}
|
|
104
61
|
// RFC-MACP-0010 §5 rule 4 (`:68`): "Once an offer has been
|
|
@@ -107,10 +64,7 @@ class HandoffProjection {
|
|
|
107
64
|
// direction too: a `handoff_id` transitions
|
|
108
65
|
// offered -> accepted | declined exactly once. Only settle if this
|
|
109
66
|
// handoff hasn't already resolved — the same shape as the
|
|
110
|
-
// HandoffContext guard just above.
|
|
111
|
-
// this guard discards a competing accept, but `ProjectionAnomalyKind`
|
|
112
|
-
// (`base.ts:8-9`) is deliberately frozen pending cross-SDK agreement
|
|
113
|
-
// with macp-sdk-python.
|
|
67
|
+
// HandoffContext guard just above.
|
|
114
68
|
if (handoff.status === 'offered' || handoff.status === 'context_sent') {
|
|
115
69
|
handoff.status = 'accepted';
|
|
116
70
|
handoff.acceptedBy = record.acceptedBy;
|
|
@@ -120,6 +74,17 @@ class HandoffProjection {
|
|
|
120
74
|
handoff.implicit = record.implicit ?? false;
|
|
121
75
|
this.phase = 'Accepted';
|
|
122
76
|
}
|
|
77
|
+
else {
|
|
78
|
+
this.recordAnomaly({
|
|
79
|
+
kind: 'settled_handoff',
|
|
80
|
+
mode: envelope.mode,
|
|
81
|
+
messageType: envelope.messageType,
|
|
82
|
+
messageId: envelope.messageId,
|
|
83
|
+
sender: envelope.sender,
|
|
84
|
+
subjectId: record.handoffId,
|
|
85
|
+
detail: `sender ${envelope.sender} attempted HandoffAccept for ${record.handoffId}, but it already settled as '${handoff.status}'`,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
123
88
|
break;
|
|
124
89
|
}
|
|
125
90
|
case 'HandoffDecline': {
|
|
@@ -130,51 +95,37 @@ class HandoffProjection {
|
|
|
130
95
|
// and HandoffDecline MUST reference an existing handoff_id." A
|
|
131
96
|
// decline for an unknown/never-offered handoff_id is invalid input
|
|
132
97
|
// (e.g. an unfiltered transcript) and MUST NOT mutate `phase` or
|
|
133
|
-
// fabricate a handoff record.
|
|
134
|
-
//
|
|
135
|
-
//
|
|
98
|
+
// fabricate a handoff record. Confirmed (issue #126/#128): no
|
|
99
|
+
// anomaly is recorded for this case — an unknown `handoff_id` is
|
|
100
|
+
// not caller misuse (a projection that joined mid-session may never
|
|
101
|
+
// have observed the original offer).
|
|
136
102
|
break;
|
|
137
103
|
}
|
|
138
104
|
// RFC-MACP-0010 §5 rule 4 (`:68`) + §5.1(4) (`:113-116`): a
|
|
139
105
|
// `handoff_id` settles once. A decline after the handoff already
|
|
140
|
-
// settled (accepted or declined) is invalid and ignored.
|
|
141
|
-
// would be recorded here too, but `ProjectionAnomalyKind`
|
|
142
|
-
// (`base.ts:8-9`) is deliberately frozen pending cross-SDK agreement
|
|
143
|
-
// with macp-sdk-python.
|
|
106
|
+
// settled (accepted or declined) is invalid and ignored.
|
|
144
107
|
if (handoff.status === 'offered' || handoff.status === 'context_sent') {
|
|
145
108
|
handoff.status = 'declined';
|
|
146
109
|
handoff.declinedBy = record.declinedBy;
|
|
147
110
|
this.phase = 'Declined';
|
|
148
111
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
112
|
+
else {
|
|
113
|
+
this.recordAnomaly({
|
|
114
|
+
kind: 'settled_handoff',
|
|
115
|
+
mode: envelope.mode,
|
|
116
|
+
messageType: envelope.messageType,
|
|
117
|
+
messageId: envelope.messageId,
|
|
118
|
+
sender: envelope.sender,
|
|
119
|
+
subjectId: record.handoffId,
|
|
120
|
+
detail: `sender ${envelope.sender} attempted HandoffDecline for ${record.handoffId}, but it already settled as '${handoff.status}'`,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
154
123
|
break;
|
|
155
124
|
}
|
|
156
125
|
default:
|
|
157
126
|
break;
|
|
158
127
|
}
|
|
159
128
|
}
|
|
160
|
-
/**
|
|
161
|
-
* True once at least one `ProjectionAnomaly` has been recorded. See
|
|
162
|
-
* `BaseProjection.hasAnomalies` (`src/projections/base.ts`) for the
|
|
163
|
-
* canonical description; duplicated here only as a one-line pointer.
|
|
164
|
-
*/
|
|
165
|
-
get hasAnomalies() {
|
|
166
|
-
return this.anomalies.length > 0;
|
|
167
|
-
}
|
|
168
|
-
get isCommitted() {
|
|
169
|
-
return this.commitment !== undefined;
|
|
170
|
-
}
|
|
171
|
-
get isPositiveOutcome() {
|
|
172
|
-
if (!this.commitment)
|
|
173
|
-
return undefined;
|
|
174
|
-
const val = this.commitment.outcomePositive ??
|
|
175
|
-
this.commitment.outcome_positive;
|
|
176
|
-
return val !== undefined ? Boolean(val) : true;
|
|
177
|
-
}
|
|
178
129
|
getHandoff(handoffId) {
|
|
179
130
|
return this.handoffs.get(handoffId);
|
|
180
131
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { BaseProjection } from './base';
|
|
2
2
|
import type { Envelope } from '../types';
|
|
3
3
|
import type { ProtoRegistry } from '../proto-registry';
|
|
4
4
|
export interface ProposalRecord {
|
|
@@ -21,7 +21,8 @@ export interface ProposalRejectRecord {
|
|
|
21
21
|
reason?: string;
|
|
22
22
|
sender: string;
|
|
23
23
|
}
|
|
24
|
-
export declare class ProposalProjection {
|
|
24
|
+
export declare class ProposalProjection extends BaseProjection {
|
|
25
|
+
protected readonly mode = "macp.mode.proposal.v1";
|
|
25
26
|
readonly proposals: Map<string, ProposalRecord>;
|
|
26
27
|
readonly accepts: ProposalAcceptRecord[];
|
|
27
28
|
readonly rejections: ProposalRejectRecord[];
|
|
@@ -39,53 +40,9 @@ export declare class ProposalProjection {
|
|
|
39
40
|
* acceptance set, and the same commitment eligibility."
|
|
40
41
|
*/
|
|
41
42
|
private readonly latestAcceptBySender;
|
|
42
|
-
/**
|
|
43
|
-
* The session's accepted history, one envelope per unique `message_id`. See
|
|
44
|
-
* `BaseProjection.transcript` (`src/projections/base.ts`) for the full
|
|
45
|
-
* redelivery-idempotence contract (RFC-MACP-0006 §3.2); duplicated here
|
|
46
|
-
* only as a one-line pointer.
|
|
47
|
-
*/
|
|
48
|
-
readonly transcript: Envelope[];
|
|
49
|
-
/**
|
|
50
|
-
* Cardinality anomalies recorded while replaying this projection's accepted
|
|
51
|
-
* transcript. See `BaseProjection.anomalies` (`src/projections/base.ts`)
|
|
52
|
-
* for the canonical description; duplicated here only as a one-line
|
|
53
|
-
* pointer. No built-in detection populates this for Proposal mode.
|
|
54
|
-
*/
|
|
55
|
-
readonly anomalies: ProjectionAnomaly[];
|
|
56
43
|
phase: 'Negotiating' | 'TerminalRejected' | 'Committed';
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
* `message_id`s already applied to this projection. See
|
|
60
|
-
* `BaseProjection.seenMessageIds` (`src/projections/base.ts`) for the full
|
|
61
|
-
* redelivery-idempotence rationale (RFC-MACP-0006 §3.2); duplicated here
|
|
62
|
-
* only as a one-line pointer.
|
|
63
|
-
*/
|
|
64
|
-
private readonly seenMessageIds;
|
|
65
|
-
/**
|
|
66
|
-
* Apply one envelope to this projection's in-process state.
|
|
67
|
-
*
|
|
68
|
-
* Input contract: **accepted-only**, caller-maintained (`Envelope` carries
|
|
69
|
-
* no acceptance marker). Canonical source: `schemas/conformance/README.md`
|
|
70
|
-
* "Notes:", RFC-MACP-0007 §5.3, RFC-MACP-0011 §5. Full rationale and failure
|
|
71
|
-
* mode are documented once, on `BaseProjection.applyEnvelope`
|
|
72
|
-
* (`src/projections/base.ts`), and duplicated here only as a one-line
|
|
73
|
-
* pointer so six independent copies of the same prose cannot drift.
|
|
74
|
-
*
|
|
75
|
-
* Redelivery idempotence: a redelivered envelope (same `message_id`) is a
|
|
76
|
-
* non-event — not appended to `transcript`, not passed to the `switch`.
|
|
77
|
-
* See `BaseProjection.applyEnvelope` for the full RFC-MACP-0006 §3.2
|
|
78
|
-
* citation; duplicated here only as a one-line pointer.
|
|
79
|
-
*/
|
|
80
|
-
applyEnvelope(envelope: Envelope, protoRegistry: ProtoRegistry): void;
|
|
81
|
-
/**
|
|
82
|
-
* True once at least one `ProjectionAnomaly` has been recorded. See
|
|
83
|
-
* `BaseProjection.hasAnomalies` (`src/projections/base.ts`) for the
|
|
84
|
-
* canonical description; duplicated here only as a one-line pointer.
|
|
85
|
-
*/
|
|
86
|
-
get hasAnomalies(): boolean;
|
|
87
|
-
get isCommitted(): boolean;
|
|
88
|
-
get isPositiveOutcome(): boolean | undefined;
|
|
44
|
+
/** Handle a Proposal-mode envelope. See `BaseProjection.applyEnvelope` for the accepted-only input contract and redelivery/rollback semantics shared by all built-in modes. */
|
|
45
|
+
protected applyMode(envelope: Envelope, protoRegistry: ProtoRegistry): void;
|
|
89
46
|
activeProposals(): ProposalRecord[];
|
|
90
47
|
latestProposal(): ProposalRecord | undefined;
|
|
91
48
|
/**
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ProposalProjection = void 0;
|
|
4
4
|
const constants_1 = require("../constants");
|
|
5
|
-
const
|
|
6
|
-
class ProposalProjection {
|
|
5
|
+
const base_1 = require("./base");
|
|
6
|
+
class ProposalProjection extends base_1.BaseProjection {
|
|
7
|
+
mode = constants_1.MODE_PROPOSAL;
|
|
7
8
|
proposals = new Map();
|
|
8
9
|
accepts = [];
|
|
9
10
|
rejections = [];
|
|
@@ -21,59 +22,9 @@ class ProposalProjection {
|
|
|
21
22
|
* acceptance set, and the same commitment eligibility."
|
|
22
23
|
*/
|
|
23
24
|
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
|
-
*/
|
|
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 = [];
|
|
38
25
|
phase = 'Negotiating';
|
|
39
|
-
|
|
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
|
-
*/
|
|
62
|
-
applyEnvelope(envelope, protoRegistry) {
|
|
63
|
-
if (envelope.mode !== constants_1.MODE_PROPOSAL)
|
|
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
|
-
}
|
|
76
|
-
this.transcript.push(envelope);
|
|
26
|
+
/** Handle a Proposal-mode envelope. See `BaseProjection.applyEnvelope` for the accepted-only input contract and redelivery/rollback semantics shared by all built-in modes. */
|
|
27
|
+
applyMode(envelope, protoRegistry) {
|
|
77
28
|
const payload = protoRegistry.decodeKnownPayload(envelope.mode, envelope.messageType, envelope.payload);
|
|
78
29
|
switch (envelope.messageType) {
|
|
79
30
|
case 'Proposal': {
|
|
@@ -118,11 +69,25 @@ class ProposalProjection {
|
|
|
118
69
|
reason: record.reason,
|
|
119
70
|
sender: envelope.sender,
|
|
120
71
|
});
|
|
72
|
+
// RFC-MACP-0008 §5 rule 3 requires `Reject` to reference an existing
|
|
73
|
+
// proposal_id, so `this.phase` must only move to the terminal state
|
|
74
|
+
// when a proposal record for it actually exists here (issue #119,
|
|
75
|
+
// same shape as issue #111's TaskComplete/TaskFail fix) — otherwise
|
|
76
|
+
// an unfiltered transcript could flip `phase` to `'TerminalRejected'`
|
|
77
|
+
// (a TERMINAL_PHASES member, see `src/agent/participant.ts`) on a
|
|
78
|
+
// never-seen proposalId, ending a live Participant's run() loop for a
|
|
79
|
+
// session that never actually terminated. `this.rejections.push(...)`
|
|
80
|
+
// above stays unconditional — it's an append-only audit log (see
|
|
81
|
+
// `docs/api/projections.md` "List accumulators and logical
|
|
82
|
+
// duplicates"), so `isTerminallyRejected`/`hasTerminalRejection` can
|
|
83
|
+
// still report `true` for an unknown id even though `phase` does not
|
|
84
|
+
// move; that split is intentional, not a residual bug.
|
|
121
85
|
if (terminal) {
|
|
122
86
|
const proposal = this.proposals.get(record.proposalId);
|
|
123
|
-
if (proposal)
|
|
87
|
+
if (proposal) {
|
|
124
88
|
proposal.status = 'rejected';
|
|
125
|
-
|
|
89
|
+
this.phase = 'TerminalRejected';
|
|
90
|
+
}
|
|
126
91
|
}
|
|
127
92
|
break;
|
|
128
93
|
}
|
|
@@ -133,33 +98,10 @@ class ProposalProjection {
|
|
|
133
98
|
proposal.status = 'withdrawn';
|
|
134
99
|
break;
|
|
135
100
|
}
|
|
136
|
-
case 'Commitment': {
|
|
137
|
-
this.commitment = payload;
|
|
138
|
-
this.phase = 'Committed';
|
|
139
|
-
break;
|
|
140
|
-
}
|
|
141
101
|
default:
|
|
142
102
|
break;
|
|
143
103
|
}
|
|
144
104
|
}
|
|
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
|
-
}
|
|
153
|
-
get isCommitted() {
|
|
154
|
-
return this.commitment !== undefined;
|
|
155
|
-
}
|
|
156
|
-
get isPositiveOutcome() {
|
|
157
|
-
if (!this.commitment)
|
|
158
|
-
return undefined;
|
|
159
|
-
const val = this.commitment.outcomePositive ??
|
|
160
|
-
this.commitment.outcome_positive;
|
|
161
|
-
return val !== undefined ? Boolean(val) : true;
|
|
162
|
-
}
|
|
163
105
|
activeProposals() {
|
|
164
106
|
return [...this.proposals.values()].filter((p) => p.status === 'open');
|
|
165
107
|
}
|