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.
Files changed (50) hide show
  1. package/CHANGELOG.md +465 -0
  2. package/README.md +2 -2
  3. package/dist/agent/index.d.ts +1 -1
  4. package/dist/agent/index.js +2 -1
  5. package/dist/agent/participant.d.ts +47 -4
  6. package/dist/agent/participant.js +88 -10
  7. package/dist/agent/strategies.js +23 -7
  8. package/dist/agent/transports.d.ts +19 -1
  9. package/dist/agent/transports.js +108 -66
  10. package/dist/auth.d.ts +15 -4
  11. package/dist/auth.js +23 -7
  12. package/dist/base-session.js +13 -3
  13. package/dist/client.js +58 -15
  14. package/dist/commitment-hash.d.ts +33 -3
  15. package/dist/commitment-hash.js +39 -5
  16. package/dist/decision.d.ts +1 -0
  17. package/dist/decision.js +11 -9
  18. package/dist/envelope.js +9 -1
  19. package/dist/handoff.d.ts +1 -0
  20. package/dist/handoff.js +19 -6
  21. package/dist/index.d.ts +2 -2
  22. package/dist/index.js +18 -7
  23. package/dist/policy.d.ts +54 -7
  24. package/dist/policy.js +143 -19
  25. package/dist/projections/base.d.ts +71 -14
  26. package/dist/projections/base.js +106 -16
  27. package/dist/projections/decision.d.ts +5 -49
  28. package/dist/projections/decision.js +30 -85
  29. package/dist/projections/handoff.d.ts +5 -48
  30. package/dist/projections/handoff.js +44 -93
  31. package/dist/projections/proposal.d.ts +5 -48
  32. package/dist/projections/proposal.js +21 -79
  33. package/dist/projections/quorum.d.ts +5 -49
  34. package/dist/projections/quorum.js +20 -82
  35. package/dist/projections/task.d.ts +31 -51
  36. package/dist/projections/task.js +49 -90
  37. package/dist/proposal.d.ts +1 -0
  38. package/dist/proposal.js +2 -2
  39. package/dist/proto-registry.d.ts +85 -5
  40. package/dist/proto-registry.js +101 -15
  41. package/dist/quorum.d.ts +1 -0
  42. package/dist/quorum.js +4 -4
  43. package/dist/task.d.ts +1 -0
  44. package/dist/task.js +24 -11
  45. package/dist/types.d.ts +14 -7
  46. package/dist/validation.d.ts +14 -0
  47. package/dist/validation.js +39 -5
  48. package/dist/version.d.ts +1 -1
  49. package/dist/version.js +1 -1
  50. package/package.json +6 -5
@@ -1,4 +1,4 @@
1
- import type { ProjectionAnomaly } from './base';
1
+ import { BaseProjection } from './base';
2
2
  import type { Envelope } from '../types';
3
3
  import type { ProtoRegistry } from '../proto-registry';
4
4
  export interface ApprovalRequestRecord {
@@ -14,57 +14,13 @@ export interface BallotRecord {
14
14
  reason?: string;
15
15
  sender: string;
16
16
  }
17
- export declare class QuorumProjection {
17
+ export declare class QuorumProjection extends BaseProjection {
18
+ protected readonly mode = "macp.mode.quorum.v1";
18
19
  readonly requests: Map<string, ApprovalRequestRecord>;
19
20
  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
- */
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[];
35
21
  phase: 'Pending' | 'Voting' | 'Committed';
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
- */
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;
66
- get isCommitted(): boolean;
67
- get isPositiveOutcome(): boolean | undefined;
22
+ /** Handle a Quorum-mode envelope. See `BaseProjection.applyEnvelope` for the accepted-only input contract and redelivery/rollback semantics shared by all built-in modes. */
23
+ protected applyMode(envelope: Envelope, protoRegistry: ProtoRegistry): void;
68
24
  private setBallot;
69
25
  approvalCount(requestId: string): number;
70
26
  rejectionCount(requestId: string): number;
@@ -2,64 +2,14 @@
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");
6
- class QuorumProjection {
5
+ const base_1 = require("./base");
6
+ class QuorumProjection extends base_1.BaseProjection {
7
+ mode = constants_1.MODE_QUORUM;
7
8
  requests = new Map();
8
9
  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
- */
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 = [];
24
10
  phase = 'Pending';
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
- */
48
- applyEnvelope(envelope, protoRegistry) {
49
- if (envelope.mode !== constants_1.MODE_QUORUM)
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
- }
62
- this.transcript.push(envelope);
11
+ /** Handle a Quorum-mode envelope. See `BaseProjection.applyEnvelope` for the accepted-only input contract and redelivery/rollback semantics shared by all built-in modes. */
12
+ applyMode(envelope, protoRegistry) {
63
13
  const payload = protoRegistry.decodeKnownPayload(envelope.mode, envelope.messageType, envelope.payload);
64
14
  switch (envelope.messageType) {
65
15
  case 'ApprovalRequest': {
@@ -83,34 +33,24 @@ class QuorumProjection {
83
33
  this.setBallot(envelope, record.requestId, 'abstain', record.reason);
84
34
  break;
85
35
  }
86
- case 'Commitment': {
87
- this.commitment = payload;
88
- this.phase = 'Committed';
89
- break;
90
- }
91
36
  default:
92
37
  break;
93
38
  }
94
39
  }
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
- }
103
- get isCommitted() {
104
- return this.commitment !== undefined;
105
- }
106
- get isPositiveOutcome() {
107
- if (!this.commitment)
108
- return undefined;
109
- const val = this.commitment.outcomePositive ??
110
- this.commitment.outcome_positive;
111
- return val !== undefined ? Boolean(val) : true;
112
- }
113
40
  setBallot(envelope, requestId, vote, reason) {
41
+ // RFC-MACP-0011 §5 rule 3 (`rfcs/RFC-MACP-0011-quorum-mode.md:69`): a ballot
42
+ // MUST reference the Session's accepted request_id. `applyEnvelope`'s input
43
+ // contract is accepted-only (see accepted-only-contract.test.ts), not a
44
+ // guarantee every field inside an accepted envelope is itself valid, so an
45
+ // unfiltered/multi-session transcript could still carry a ballot for a
46
+ // request_id this projection never saw an ApprovalRequest for. Without this
47
+ // guard that would fabricate a `ballots` Map entry: votedSenders() and
48
+ // approvalCount()/rejectionCount()/abstentionCount() would report on a
49
+ // request that doesn't exist, and a second such ballot from the same sender
50
+ // would fire a spurious duplicate_ballot anomaly against it (issue #121,
51
+ // same bug shape as issue #119's proposal.ts/handoff.ts/decision.ts sites).
52
+ if (!this.requests.has(requestId))
53
+ return;
114
54
  const senderMap = this.ballots.get(requestId) ?? new Map();
115
55
  const kept = senderMap.get(envelope.sender);
116
56
  if (kept !== undefined) {
@@ -129,7 +69,7 @@ class QuorumProjection {
129
69
  // by RFC-0011 either; it is parity with RFC-MACP-0007 §5 item 3's
130
70
  // explicit first-stands rule for `Vote`, plus macp-runtime's enforced
131
71
  // first-wins behaviour.
132
- const anomaly = {
72
+ this.recordAnomaly({
133
73
  kind: 'duplicate_ballot',
134
74
  mode: envelope.mode,
135
75
  messageType: envelope.messageType,
@@ -137,9 +77,7 @@ class QuorumProjection {
137
77
  sender: envelope.sender,
138
78
  subjectId: requestId,
139
79
  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);
80
+ });
143
81
  return;
144
82
  }
145
83
  senderMap.set(envelope.sender, { requestId, vote, reason, sender: envelope.sender });
@@ -1,4 +1,4 @@
1
- import type { ProjectionAnomaly } from './base';
1
+ import { BaseProjection } from './base';
2
2
  import type { Envelope } from '../types';
3
3
  import type { ProtoRegistry } from '../proto-registry';
4
4
  export interface TaskRecord {
@@ -19,13 +19,21 @@ export interface TaskUpdateRecord {
19
19
  message?: string;
20
20
  sender: string;
21
21
  }
22
- export interface TaskCompletionRecord {
22
+ export interface TaskCompleteRecord {
23
23
  taskId: string;
24
24
  assignee: string;
25
25
  summary?: string;
26
26
  sender: string;
27
27
  }
28
- export interface TaskFailureRecord {
28
+ /**
29
+ * @deprecated Use {@link TaskCompleteRecord}. Renamed for cross-SDK naming
30
+ * parity (multiagentcoordinationprotocol#135, issue #138) — the record now
31
+ * mirrors its triggering `TaskComplete` message type the way its sibling
32
+ * {@link TaskUpdateRecord} already mirrors `TaskUpdate`, and matches
33
+ * `macp-sdk-python`'s `TaskCompleteRecord`. Scheduled for removal in 0.13.0.
34
+ */
35
+ export type TaskCompletionRecord = TaskCompleteRecord;
36
+ export interface TaskFailRecord {
29
37
  taskId: string;
30
38
  assignee: string;
31
39
  errorCode?: string;
@@ -33,34 +41,21 @@ export interface TaskFailureRecord {
33
41
  retryable: boolean;
34
42
  sender: string;
35
43
  }
36
- export declare class TaskProjection {
44
+ /**
45
+ * @deprecated Use {@link TaskFailRecord}. Renamed for cross-SDK naming
46
+ * parity (multiagentcoordinationprotocol#135, issue #138) — the record now
47
+ * mirrors its triggering `TaskFail` message type the way its sibling
48
+ * {@link TaskUpdateRecord} already mirrors `TaskUpdate`, and matches
49
+ * `macp-sdk-python`'s `TaskFailRecord`. Scheduled for removal in 0.13.0.
50
+ */
51
+ export type TaskFailureRecord = TaskFailRecord;
52
+ export declare class TaskProjection extends BaseProjection {
53
+ protected readonly mode = "macp.mode.task.v1";
37
54
  readonly tasks: Map<string, TaskRecord>;
38
55
  readonly updates: TaskUpdateRecord[];
39
- readonly completions: TaskCompletionRecord[];
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
- */
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[];
56
+ readonly completions: TaskCompleteRecord[];
57
+ readonly failures: TaskFailRecord[];
55
58
  phase: 'Pending' | 'Requested' | 'InProgress' | 'Completed' | 'Failed' | 'Committed';
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
59
  /**
65
60
  * The **session-level** active assignment: who currently holds the session's
66
61
  * single assignee slot, and which `task_id` they took it on.
@@ -81,31 +76,16 @@ export declare class TaskProjection {
81
76
  * the right `TaskRecord.assignee` even on a multi-task transcript.
82
77
  */
83
78
  private activeAssignment?;
79
+ /** Handle a Task-mode envelope. See `BaseProjection.applyEnvelope` for the accepted-only input contract and redelivery/rollback semantics shared by all built-in modes. */
80
+ protected applyMode(envelope: Envelope, protoRegistry: ProtoRegistry): void;
81
+ getTask(taskId: string): TaskRecord | undefined;
82
+ isCompleted(taskId: string): boolean;
84
83
  /**
85
- * Apply one envelope to this projection's in-process state.
86
- *
87
- * Input contract: **accepted-only**, caller-maintained (`Envelope` carries
88
- * no acceptance marker). Canonical source: `schemas/conformance/README.md`
89
- * "Notes:", RFC-MACP-0007 §5.3, RFC-MACP-0011 §5. Full rationale and failure
90
- * mode are documented once, on `BaseProjection.applyEnvelope`
91
- * (`src/projections/base.ts`), and duplicated here only as a one-line
92
- * pointer so six independent copies of the same prose cannot drift.
93
- *
94
- * Redelivery idempotence: a redelivered envelope (same `message_id`) is a
95
- * non-event — not appended to `transcript`, not passed to the `switch`.
96
- * See `BaseProjection.applyEnvelope` for the full RFC-MACP-0006 §3.2
97
- * citation; duplicated here only as a one-line pointer.
98
- */
99
- applyEnvelope(envelope: Envelope, protoRegistry: ProtoRegistry): void;
100
- /**
101
- * True once at least one `ProjectionAnomaly` has been recorded. See
102
- * `BaseProjection.hasAnomalies` (`src/projections/base.ts`) for the
103
- * canonical description; duplicated here only as a one-line pointer.
84
+ * @deprecated Use {@link isCompleted}. Renamed for cross-SDK naming parity
85
+ * (multiagentcoordinationprotocol#135, issue #138) — every other `is*`
86
+ * predicate in this SDK is past-participle, and `macp-sdk-python` has
87
+ * always spelled this `is_completed`. Scheduled for removal in 0.13.0.
104
88
  */
105
- get hasAnomalies(): boolean;
106
- get isCommitted(): boolean;
107
- get isPositiveOutcome(): boolean | undefined;
108
- getTask(taskId: string): TaskRecord | undefined;
109
89
  isComplete(taskId: string): boolean;
110
90
  isFailed(taskId: string): boolean;
111
91
  isRetryable(taskId: string): boolean;
@@ -2,35 +2,14 @@
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");
6
- class TaskProjection {
5
+ const base_1 = require("./base");
6
+ class TaskProjection extends base_1.BaseProjection {
7
+ mode = constants_1.MODE_TASK;
7
8
  tasks = new Map();
8
9
  updates = [];
9
10
  completions = [];
10
11
  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
- */
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 = [];
25
12
  phase = 'Pending';
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
13
  /**
35
14
  * The **session-level** active assignment: who currently holds the session's
36
15
  * single assignee slot, and which `task_id` they took it on.
@@ -51,36 +30,8 @@ class TaskProjection {
51
30
  * the right `TaskRecord.assignee` even on a multi-task transcript.
52
31
  */
53
32
  activeAssignment;
54
- /**
55
- * Apply one envelope to this projection's in-process state.
56
- *
57
- * Input contract: **accepted-only**, caller-maintained (`Envelope` carries
58
- * no acceptance marker). Canonical source: `schemas/conformance/README.md`
59
- * "Notes:", RFC-MACP-0007 §5.3, RFC-MACP-0011 §5. Full rationale and failure
60
- * mode are documented once, on `BaseProjection.applyEnvelope`
61
- * (`src/projections/base.ts`), and duplicated here only as a one-line
62
- * pointer so six independent copies of the same prose cannot drift.
63
- *
64
- * Redelivery idempotence: a redelivered envelope (same `message_id`) is a
65
- * non-event — not appended to `transcript`, not passed to the `switch`.
66
- * See `BaseProjection.applyEnvelope` for the full RFC-MACP-0006 §3.2
67
- * citation; duplicated here only as a one-line pointer.
68
- */
69
- applyEnvelope(envelope, protoRegistry) {
70
- if (envelope.mode !== constants_1.MODE_TASK)
71
- return;
72
- if (envelope.messageId) {
73
- if (this.seenMessageIds.has(envelope.messageId)) {
74
- logging_1.logger.debug('projection redelivery ignored', {
75
- messageId: envelope.messageId,
76
- mode: envelope.mode,
77
- messageType: envelope.messageType,
78
- });
79
- return;
80
- }
81
- this.seenMessageIds.add(envelope.messageId);
82
- }
83
- this.transcript.push(envelope);
33
+ /** Handle a Task-mode envelope. See `BaseProjection.applyEnvelope` for the accepted-only input contract and redelivery/rollback semantics shared by all built-in modes. */
34
+ applyMode(envelope, protoRegistry) {
84
35
  const payload = protoRegistry.decodeKnownPayload(envelope.mode, envelope.messageType, envelope.payload);
85
36
  switch (envelope.messageType) {
86
37
  case 'TaskRequest': {
@@ -123,16 +74,37 @@ class TaskProjection {
123
74
  // below, which frees the slot — see its comment for why that needs no
124
75
  // policy input.
125
76
  //
126
- // An anomaly would be recorded when this guard discards a second
127
- // `TaskAccept`, but `ProjectionAnomalyKind` (`base.ts:8-9`) is
128
- // deliberately frozen pending cross-SDK agreement with
129
- // macp-sdk-python.
130
- if (task && this.activeAssignment === undefined) {
131
- task.assignee = record.assignee;
132
- task.status = 'accepted';
133
- this.activeAssignment = { sender: envelope.sender, taskId: record.taskId };
134
- this.phase = 'InProgress';
77
+ // A discarded second `TaskAccept` records a `duplicate_task_accept`
78
+ // anomaly (issue #126/#128) only when this projection actually has a
79
+ // `TaskRequest` on file for the losing `taskId` (`task` is defined) —
80
+ // an accept for a `taskId` this projection never saw a `TaskRequest`
81
+ // for stays a silent no-op below, since a projection that joined
82
+ // mid-session may never have observed the original request, and that
83
+ // is not caller misuse. Note this guard's `activeAssignment` slot is
84
+ // session-scoped (see the comment above), so the `taskId` that loses
85
+ // here need not be the same one the slot is already held for — it is
86
+ // simply a second, separately-requested task competing for the one
87
+ // slot the session allows.
88
+ if (task) {
89
+ if (this.activeAssignment === undefined) {
90
+ task.assignee = record.assignee;
91
+ task.status = 'accepted';
92
+ this.activeAssignment = { sender: envelope.sender, taskId: record.taskId };
93
+ this.phase = 'InProgress';
94
+ }
95
+ else {
96
+ this.recordAnomaly({
97
+ kind: 'duplicate_task_accept',
98
+ mode: envelope.mode,
99
+ messageType: envelope.messageType,
100
+ messageId: envelope.messageId,
101
+ sender: envelope.sender,
102
+ subjectId: record.taskId,
103
+ detail: `sender ${envelope.sender} attempted TaskAccept for ${record.taskId}, but ${this.activeAssignment.sender} already holds the session's one assignee slot with task ${this.activeAssignment.taskId}`,
104
+ });
105
+ }
135
106
  }
107
+ // unknown taskId (task undefined): stays a silent no-op.
136
108
  break;
137
109
  }
138
110
  case 'TaskReject': {
@@ -186,52 +158,39 @@ class TaskProjection {
186
158
  if (task) {
187
159
  task.status = 'completed';
188
160
  task.progress = 1;
161
+ this.phase = 'Completed';
189
162
  }
190
- this.phase = 'Completed';
191
163
  break;
192
164
  }
193
165
  case 'TaskFail': {
194
166
  const record = payload;
195
167
  this.failures.push({ ...record, retryable: record.retryable ?? false, sender: envelope.sender });
196
168
  const task = this.tasks.get(record.taskId);
197
- if (task)
169
+ if (task) {
198
170
  task.status = 'failed';
199
- this.phase = 'Failed';
200
- break;
201
- }
202
- case 'Commitment': {
203
- this.commitment = payload;
204
- this.phase = 'Committed';
171
+ this.phase = 'Failed';
172
+ }
205
173
  break;
206
174
  }
207
175
  default:
208
176
  break;
209
177
  }
210
178
  }
211
- /**
212
- * True once at least one `ProjectionAnomaly` has been recorded. See
213
- * `BaseProjection.hasAnomalies` (`src/projections/base.ts`) for the
214
- * canonical description; duplicated here only as a one-line pointer.
215
- */
216
- get hasAnomalies() {
217
- return this.anomalies.length > 0;
218
- }
219
- get isCommitted() {
220
- return this.commitment !== undefined;
221
- }
222
- get isPositiveOutcome() {
223
- if (!this.commitment)
224
- return undefined;
225
- const val = this.commitment.outcomePositive ??
226
- this.commitment.outcome_positive;
227
- return val !== undefined ? Boolean(val) : true;
228
- }
229
179
  getTask(taskId) {
230
180
  return this.tasks.get(taskId);
231
181
  }
232
- isComplete(taskId) {
182
+ isCompleted(taskId) {
233
183
  return this.tasks.get(taskId)?.status === 'completed';
234
184
  }
185
+ /**
186
+ * @deprecated Use {@link isCompleted}. Renamed for cross-SDK naming parity
187
+ * (multiagentcoordinationprotocol#135, issue #138) — every other `is*`
188
+ * predicate in this SDK is past-participle, and `macp-sdk-python` has
189
+ * always spelled this `is_completed`. Scheduled for removal in 0.13.0.
190
+ */
191
+ isComplete(taskId) {
192
+ return this.isCompleted(taskId);
193
+ }
235
194
  isFailed(taskId) {
236
195
  return this.tasks.get(taskId)?.status === 'failed';
237
196
  }
@@ -32,6 +32,7 @@ export declare class ProposalSession {
32
32
  }[];
33
33
  maxSuspendMs?: number;
34
34
  sender?: string;
35
+ auth?: AuthConfig;
35
36
  }): Promise<Ack>;
36
37
  propose(input: ProposalModeProposalPayload & {
37
38
  sender?: string;
package/dist/proposal.js CHANGED
@@ -60,10 +60,10 @@ class ProposalSession {
60
60
  mode: constants_1.MODE_PROPOSAL,
61
61
  messageType: 'SessionStart',
62
62
  sessionId: this.sessionId,
63
- sender: this.senderFor(input.sender),
63
+ sender: this.senderFor(input.sender, input.auth),
64
64
  payload: this.client.protoRegistry.encodeKnownPayload(constants_1.MODE_PROPOSAL, 'SessionStart', (0, envelope_1.toProtoPayload)(payload)),
65
65
  });
66
- return this.sendAndTrack(envelope, this.auth);
66
+ return this.sendAndTrack(envelope, input.auth ?? this.auth);
67
67
  }
68
68
  async propose(input) {
69
69
  (0, validation_1.validateRequiredField)('proposalId', input.proposalId);
@@ -10,13 +10,93 @@ export declare class ProtoRegistry {
10
10
  /**
11
11
  * Decode an `ext.multi_round.v1` `Contribute` payload, accepting both wire
12
12
  * formats: legacy JSON (`{"value":"..."}`, still replayed verbatim from
13
- * pre-proto histories) and canonical protobuf (`ContributePayload`). JSON is
14
- * tried first — mirroring the runtime's permanent decode order — and the two
15
- * encodings are disjoint on their first byte (`{` = 0x7b vs proto field-1
16
- * tag 0x0A), so a canonical proto payload never mis-parses as JSON and vice
17
- * versa. Both normalize to `{ value: string }`.
13
+ * pre-proto histories) and canonical protobuf (`ContributePayload`). Both
14
+ * normalize to `{ value }`, but only the protobuf path's `value` is always
15
+ * a `string` — the legacy-JSON path passes `parsed.value` through exactly
16
+ * as `JSON.parse` produced it (issue #124: no `String(...)` coercion, no
17
+ * `?? ''` fallback for an absent key), so `value` may be `undefined`,
18
+ * `null`, a number, a boolean, or an object/array for malformed-but-valid
19
+ * legacy JSON. This method's own contract is observational, not a gate —
20
+ * see the `contribute_acceptance` note below.
21
+ *
22
+ * Parse-then-fallback (issue #93), not a first-byte shortcut: JSON is always
23
+ * attempted first, and only a `JSON.parse` failure falls through to
24
+ * protobuf. A first-byte check (`{` = 0x7b vs proto field-1 tag 0x0A) does
25
+ * not generalize — e.g. legacy JSON with leading whitespace has neither byte
26
+ * first, and used to reach `decodeMessage` and throw. Parity with
27
+ * `macp-sdk-python`'s `_decode_json_first_then_proto`, which has always
28
+ * worked this way.
29
+ *
30
+ * **A successful `JSON.parse` is not proof the bytes are legacy JSON**
31
+ * (issue #104 — this method's prior "essentially never also syntactically
32
+ * valid JSON" claim was false, not merely imprecise): the canonical proto
33
+ * tag byte for field 1 (`0x0A`) is itself insignificant JSON whitespace, so
34
+ * for specific value byte-lengths the length varint — or, when the varint
35
+ * is itself whitespace, the value's own leading byte — becomes the first
36
+ * significant character `JSON.parse` sees, and a genuine `ContributePayload`
37
+ * silently misreads as a JSON number/string/object. Verified directly
38
+ * (encode-then-decode sweep, value lengths 1-127, seven value shapes): every
39
+ * one of lengths 9, 10, 13, 32, 34, 45, 48, 49-57, and 123 corrupted before
40
+ * `isCanonicalProto` below, most to total silent data loss
41
+ * (`String(undefined ?? '') === ''`), zero corrupt after.
42
+ *
43
+ * `isCanonicalProto` resolves the ambiguity by tie-break, not by refusing
44
+ * to try JSON first: bytes that parse as JSON *and* are the exact canonical
45
+ * proto encoding are treated as proto. This has a narrow, priced cost on
46
+ * the reverse direction (legacy JSON misread as proto) that it introduces
47
+ * rather than merely fails to close — see `isCanonicalProto`'s own
48
+ * docblock.
49
+ *
50
+ * An empty payload falls through the same path: `JSON.parse('')` throws, so
51
+ * it reaches `decodeMessage` on zero bytes, which yields `{}` (proto3
52
+ * defaults) rather than a decode error. This SDK's decode layer is
53
+ * observational, not an acceptance gate — whether an empty `Contribute`
54
+ * should be rejected is a runtime-acceptance question (tracked cross-repo,
55
+ * `contribute_acceptance.empty_payload` in `schemas/parity/contract.json`,
56
+ * currently `macp-runtime`-only), not something this method decides.
18
57
  */
19
58
  private decodeMultiRoundContribute;
59
+ /**
60
+ * True iff `payload` is the exact canonical protobuf encoding of
61
+ * `typeName` — used to break the JSON/proto ambiguity in
62
+ * `decodeMultiRoundContribute` (issue #104): bytes that both parse as JSON
63
+ * *and* round-trip byte-identically through the proto message are treated
64
+ * as proto, not JSON.
65
+ *
66
+ * `macp-sdk-python`'s equivalent (`_is_canonical_proto`) needs an explicit
67
+ * `DiscardUnknownFields()` step before comparing, because Python's protobuf
68
+ * runtime preserves unknown fields verbatim through
69
+ * `ParseFromString`/`SerializeToString` by default — without it, a plain
70
+ * round-trip check misclassified several whitespace-prefixed legacy-JSON
71
+ * payloads as canonical proto. **`protobufjs` needs no equivalent step**:
72
+ * verified directly (a synthetic payload with a declared field-1 string
73
+ * plus an undeclared field 2) that `Type.decode()` silently drops fields
74
+ * not in the schema rather than preserving them, so `Type.encode()` can
75
+ * never reproduce bytes it never captured — a byte-exact round-trip here
76
+ * is already proof the payload is *exactly* the single-field encoding, no
77
+ * unknown fields possible. Confirmed this closes the same
78
+ * whitespace-prefixed false-positive class `DiscardUnknownFields` closes
79
+ * in Python (tab/space-prefixed legacy JSON at several lengths).
80
+ *
81
+ * One narrow, symmetric residual survives regardless — genuinely
82
+ * irreducible, not a gap this check merely fails to close: a payload whose
83
+ * first byte is a literal `0x0A` (`\n`) and whose remainder forms a
84
+ * complete, well-formed proto field-1 string with nothing left over has no
85
+ * unknown field for either implementation to expose a mismatch on, because
86
+ * it *is*, byte for byte, both a legal JSON reading and the canonical
87
+ * proto encoding of some (possibly nonsensical) string. Verified instances
88
+ * (this SDK's own `JSON.stringify`, which — unlike Python's `json.dumps`
89
+ * — omits the space after `:`, shifting the colliding lengths by one byte
90
+ * from `macp-sdk-python`'s pinned cases): `'\n' + JSON.stringify({value:
91
+ * 'z'.repeat(112)})`, and with one extra whitespace byte inserted after the
92
+ * `\n`, `'\r'` at value length 1 and `' '` at value length 20. A real
93
+ * legacy-JSON sender would need to deliberately prefix with a literal
94
+ * newline byte for this to matter — this SDK's own JSON producers never
95
+ * emit one — so the trade (fixing the ordinary, naturally-reachable
96
+ * forward-direction corruption above at the cost of this adversarial,
97
+ * unreachable-by-any-known-encoder reverse case) is the right one.
98
+ */
99
+ private isCanonicalProto;
20
100
  private lookupType;
21
101
  private tryDecodeUtf8;
22
102
  }