macp-sdk-typescript 0.7.0 → 0.8.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.
@@ -2,14 +2,61 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.HandoffProjection = void 0;
4
4
  const constants_1 = require("../constants");
5
+ const logging_1 = require("../logging");
5
6
  class HandoffProjection {
6
7
  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
+ */
7
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 = [];
8
22
  phase = 'Pending';
9
23
  commitment;
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
+ */
10
46
  applyEnvelope(envelope, protoRegistry) {
11
47
  if (envelope.mode !== constants_1.MODE_HANDOFF)
12
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
+ }
13
60
  this.transcript.push(envelope);
14
61
  const payload = protoRegistry.decodeKnownPayload(envelope.mode, envelope.messageType, envelope.payload);
15
62
  switch (envelope.messageType) {
@@ -74,6 +121,14 @@ class HandoffProjection {
74
121
  break;
75
122
  }
76
123
  }
124
+ /**
125
+ * True once at least one `ProjectionAnomaly` has been recorded. See
126
+ * `BaseProjection.hasAnomalies` (`src/projections/base.ts`) for the
127
+ * canonical description; duplicated here only as a one-line pointer.
128
+ */
129
+ get hasAnomalies() {
130
+ return this.anomalies.length > 0;
131
+ }
77
132
  get isCommitted() {
78
133
  return this.commitment !== undefined;
79
134
  }
@@ -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 ProposalRecord {
@@ -24,10 +25,51 @@ export declare class ProposalProjection {
24
25
  readonly proposals: Map<string, ProposalRecord>;
25
26
  readonly accepts: ProposalAcceptRecord[];
26
27
  readonly rejections: ProposalRejectRecord[];
28
+ /**
29
+ * The session's accepted history, one envelope per unique `message_id`. See
30
+ * `BaseProjection.transcript` (`src/projections/base.ts`) for the full
31
+ * redelivery-idempotence contract (RFC-MACP-0006 §3.2); duplicated here
32
+ * only as a one-line pointer.
33
+ */
27
34
  readonly transcript: Envelope[];
35
+ /**
36
+ * Cardinality anomalies recorded while replaying this projection's accepted
37
+ * transcript. See `BaseProjection.anomalies` (`src/projections/base.ts`)
38
+ * for the canonical description; duplicated here only as a one-line
39
+ * pointer. No built-in detection populates this for Proposal mode.
40
+ */
41
+ readonly anomalies: ProjectionAnomaly[];
28
42
  phase: 'Negotiating' | 'TerminalRejected' | 'Committed';
29
43
  commitment?: Record<string, unknown>;
44
+ /**
45
+ * `message_id`s already applied to this projection. See
46
+ * `BaseProjection.seenMessageIds` (`src/projections/base.ts`) for the full
47
+ * redelivery-idempotence rationale (RFC-MACP-0006 §3.2); duplicated here
48
+ * only as a one-line pointer.
49
+ */
50
+ private readonly seenMessageIds;
51
+ /**
52
+ * Apply one envelope to this projection's in-process state.
53
+ *
54
+ * Input contract: **accepted-only**, caller-maintained (`Envelope` carries
55
+ * no acceptance marker). Canonical source: `schemas/conformance/README.md`
56
+ * "Notes:", RFC-MACP-0007 §5.3, RFC-MACP-0011 §5. Full rationale and failure
57
+ * mode are documented once, on `BaseProjection.applyEnvelope`
58
+ * (`src/projections/base.ts`), and duplicated here only as a one-line
59
+ * pointer so six independent copies of the same prose cannot drift.
60
+ *
61
+ * Redelivery idempotence: a redelivered envelope (same `message_id`) is a
62
+ * non-event — not appended to `transcript`, not passed to the `switch`.
63
+ * See `BaseProjection.applyEnvelope` for the full RFC-MACP-0006 §3.2
64
+ * citation; duplicated here only as a one-line pointer.
65
+ */
30
66
  applyEnvelope(envelope: Envelope, protoRegistry: ProtoRegistry): void;
67
+ /**
68
+ * True once at least one `ProjectionAnomaly` has been recorded. See
69
+ * `BaseProjection.hasAnomalies` (`src/projections/base.ts`) for the
70
+ * canonical description; duplicated here only as a one-line pointer.
71
+ */
72
+ get hasAnomalies(): boolean;
31
73
  get isCommitted(): boolean;
32
74
  get isPositiveOutcome(): boolean | undefined;
33
75
  activeProposals(): ProposalRecord[];
@@ -2,16 +2,63 @@
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
+ * The session's accepted history, one envelope per unique `message_id`. See
12
+ * `BaseProjection.transcript` (`src/projections/base.ts`) for the full
13
+ * redelivery-idempotence contract (RFC-MACP-0006 §3.2); duplicated here
14
+ * only as a one-line pointer.
15
+ */
9
16
  transcript = [];
17
+ /**
18
+ * Cardinality anomalies recorded while replaying this projection's accepted
19
+ * transcript. See `BaseProjection.anomalies` (`src/projections/base.ts`)
20
+ * for the canonical description; duplicated here only as a one-line
21
+ * pointer. No built-in detection populates this for Proposal mode.
22
+ */
23
+ anomalies = [];
10
24
  phase = 'Negotiating';
11
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
+ */
12
48
  applyEnvelope(envelope, protoRegistry) {
13
49
  if (envelope.mode !== constants_1.MODE_PROPOSAL)
14
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
+ }
15
62
  this.transcript.push(envelope);
16
63
  const payload = protoRegistry.decodeKnownPayload(envelope.mode, envelope.messageType, envelope.payload);
17
64
  switch (envelope.messageType) {
@@ -77,6 +124,14 @@ class ProposalProjection {
77
124
  break;
78
125
  }
79
126
  }
127
+ /**
128
+ * True once at least one `ProjectionAnomaly` has been recorded. See
129
+ * `BaseProjection.hasAnomalies` (`src/projections/base.ts`) for the
130
+ * canonical description; duplicated here only as a one-line pointer.
131
+ */
132
+ get hasAnomalies() {
133
+ return this.anomalies.length > 0;
134
+ }
80
135
  get isCommitted() {
81
136
  return this.commitment !== undefined;
82
137
  }
@@ -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, envelope.sender, 'approve', record.reason);
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, envelope.sender, 'reject', record.reason);
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, envelope.sender, 'abstain', record.reason);
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(requestId, sender, vote, reason) {
113
+ setBallot(envelope, requestId, vote, reason) {
58
114
  const senderMap = this.ballots.get(requestId) ?? new Map();
59
- senderMap.set(sender, { requestId, vote, reason, sender });
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;
@@ -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) {
@@ -87,6 +134,14 @@ class TaskProjection {
87
134
  break;
88
135
  }
89
136
  }
137
+ /**
138
+ * True once at least one `ProjectionAnomaly` has been recorded. See
139
+ * `BaseProjection.hasAnomalies` (`src/projections/base.ts`) for the
140
+ * canonical description; duplicated here only as a one-line pointer.
141
+ */
142
+ get hasAnomalies() {
143
+ return this.anomalies.length > 0;
144
+ }
90
145
  get isCommitted() {
91
146
  return this.commitment !== undefined;
92
147
  }
package/dist/proposal.js CHANGED
@@ -16,7 +16,7 @@ class ProposalSession {
16
16
  projection = new proposal_1.ProposalProjection();
17
17
  constructor(client, options = {}) {
18
18
  this.client = client;
19
- if (options.sessionId)
19
+ if (options.sessionId !== undefined)
20
20
  (0, validation_1.validateSessionId)(options.sessionId);
21
21
  this.sessionId = options.sessionId ?? (0, envelope_1.newSessionId)();
22
22
  this.modeVersion = options.modeVersion ?? constants_1.DEFAULT_MODE_VERSION;
package/dist/quorum.js CHANGED
@@ -16,7 +16,7 @@ class QuorumSession {
16
16
  projection = new quorum_1.QuorumProjection();
17
17
  constructor(client, options = {}) {
18
18
  this.client = client;
19
- if (options.sessionId)
19
+ if (options.sessionId !== undefined)
20
20
  (0, validation_1.validateSessionId)(options.sessionId);
21
21
  this.sessionId = options.sessionId ?? (0, envelope_1.newSessionId)();
22
22
  this.modeVersion = options.modeVersion ?? constants_1.DEFAULT_MODE_VERSION;
package/dist/task.js CHANGED
@@ -16,7 +16,7 @@ class TaskSession {
16
16
  projection = new task_1.TaskProjection();
17
17
  constructor(client, options = {}) {
18
18
  this.client = client;
19
- if (options.sessionId)
19
+ if (options.sessionId !== undefined)
20
20
  (0, validation_1.validateSessionId)(options.sessionId);
21
21
  this.sessionId = options.sessionId ?? (0, envelope_1.newSessionId)();
22
22
  this.modeVersion = options.modeVersion ?? constants_1.DEFAULT_MODE_VERSION;
package/dist/version.d.ts CHANGED
@@ -4,4 +4,4 @@
4
4
  * Kept in sync with package.json by release-please (see the
5
5
  * `x-release-please-version` annotation).
6
6
  */
7
- export declare const VERSION = "0.7.0";
7
+ export declare const VERSION = "0.8.0";
package/dist/version.js CHANGED
@@ -7,4 +7,4 @@ exports.VERSION = void 0;
7
7
  * Kept in sync with package.json by release-please (see the
8
8
  * `x-release-please-version` annotation).
9
9
  */
10
- exports.VERSION = '0.7.0'; // x-release-please-version
10
+ exports.VERSION = '0.8.0'; // x-release-please-version
package/dist/watchers.js CHANGED
@@ -96,6 +96,14 @@ function serverStreamToAsyncGenerator(stream) {
96
96
  stream.cancel();
97
97
  throw err;
98
98
  },
99
+ // Required since TypeScript 6 / @types/node 26, which make `AsyncGenerator`
100
+ // extend `AsyncDisposable` (lib.esnext.disposable). Native `async function*`
101
+ // generators get this for free; this hand-rolled one has to declare it. It
102
+ // also makes every watcher stream usable with `await using`, which cancels
103
+ // the underlying gRPC stream on scope exit exactly like `return()` does.
104
+ async [Symbol.asyncDispose]() {
105
+ stream.cancel();
106
+ },
99
107
  };
100
108
  }
101
109
  class ModeRegistryWatcher {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "macp-sdk-typescript",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "TypeScript SDK for the MACP Rust runtime",
5
5
  "license": "Apache-2.0",
6
6
  "type": "commonjs",
@@ -55,18 +55,18 @@
55
55
  "dependencies": {
56
56
  "@grpc/grpc-js": "^1.14.3",
57
57
  "@grpc/proto-loader": "^0.8.0",
58
- "@multiagentcoordinationprotocol/proto": "^0.1.8",
58
+ "@multiagentcoordinationprotocol/proto": "^0.1.9",
59
59
  "protobufjs": "^8.6.5"
60
60
  },
61
61
  "devDependencies": {
62
- "@types/node": "^22.14.0",
63
- "@typescript-eslint/eslint-plugin": "^8.0.0",
64
- "@typescript-eslint/parser": "^8.0.0",
65
- "@vitest/coverage-v8": "^3.0.0",
66
- "eslint": "^9.0.0",
62
+ "@types/node": "^26.4.0",
63
+ "@typescript-eslint/eslint-plugin": "^8.68.0",
64
+ "@typescript-eslint/parser": "^8.68.0",
65
+ "@vitest/coverage-v8": "^4.1.11",
66
+ "eslint": "^10.9.1",
67
67
  "prettier": "^3.0.0",
68
68
  "tsx": "^4.21.0",
69
- "typescript": "^5.8.3",
70
- "vitest": "^3.0.0"
69
+ "typescript": "^6.0.3",
70
+ "vitest": "^4.1.11"
71
71
  }
72
72
  }