macp-sdk-typescript 0.11.0 → 0.13.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 (44) hide show
  1. package/CHANGELOG.md +383 -0
  2. package/README.md +1 -1
  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/projections/base.d.ts +71 -14
  24. package/dist/projections/base.js +83 -12
  25. package/dist/projections/decision.js +23 -3
  26. package/dist/projections/handoff.js +39 -16
  27. package/dist/projections/proposal.js +16 -2
  28. package/dist/projections/quorum.js +13 -0
  29. package/dist/projections/task.d.ts +5 -5
  30. package/dist/projections/task.js +35 -13
  31. package/dist/proposal.d.ts +1 -0
  32. package/dist/proposal.js +2 -2
  33. package/dist/proto-registry.d.ts +69 -3
  34. package/dist/proto-registry.js +83 -5
  35. package/dist/quorum.d.ts +1 -0
  36. package/dist/quorum.js +4 -4
  37. package/dist/task.d.ts +1 -0
  38. package/dist/task.js +24 -11
  39. package/dist/types.d.ts +14 -7
  40. package/dist/validation.d.ts +14 -0
  41. package/dist/validation.js +39 -5
  42. package/dist/version.d.ts +1 -1
  43. package/dist/version.js +1 -1
  44. package/package.json +3 -2
package/CHANGELOG.md CHANGED
@@ -4,6 +4,52 @@ All notable changes to `macp-sdk-typescript` are documented here. The format
4
4
  follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the
5
5
  project uses [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [0.13.0](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/compare/v0.12.0...v0.13.0) (2026-09-30)
8
+
9
+
10
+ ### ⚠ BREAKING CHANGES
11
+
12
+ * **projections:** TaskProjection.isComplete(taskId) has been removed; use isCompleted(taskId) instead (identical signature and semantics). The TaskCompletionRecord and TaskFailureRecord type aliases have been removed; use TaskCompleteRecord and TaskFailRecord respectively (identical shape). A TypeScript consumer still importing the old type names gets a compile-time TS2724; a plain-JS (or `as any`-cast) caller still invoking isComplete() gets a runtime TypeError.
13
+
14
+ ### Features
15
+
16
+ * **projections:** remove isComplete/TaskCompletionRecord/TaskFailureRecord shims ([#143](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/143)) ([06ec632](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/commit/06ec632d6b6ffcfea0937083c3614fdc952d06a5))
17
+
18
+ ## [0.12.0](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/compare/v0.11.0...v0.12.0) (2026-09-30)
19
+
20
+
21
+ ### ⚠ BREAKING CHANGES
22
+
23
+ * **auth:** Auth.devAgent sets expectedSender to enforce sender identity client-side ([#133](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/133))
24
+
25
+ ### Features
26
+
27
+ * **parity:** add isCanonicalCommitmentHash + anomaly constant exports ([bbde8d2](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/commit/bbde8d2f72b6fa2d1dc9024bafa587a21243d051))
28
+ * **projections:** rename TaskCompletionRecord/TaskFailureRecord ([#142](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/142)) ([84008e1](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/commit/84008e1700304814fe17093ae35c9046aab553ea))
29
+ * **projections:** rename TaskProjection.isComplete to isCompleted ([#141](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/141)) ([3239ae6](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/commit/3239ae63df50b25064b51bdee93d397af2ed904e))
30
+
31
+
32
+ ### Bug Fixes
33
+
34
+ * **agent:** Participant terminal-on-cancel, teardown on every run() exit, tri-state isStopped, guarded rollback pop ([#113](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/113)) ([e1a7de4](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/commit/e1a7de486c9ce6d23c249811a98ba99a15814a9b))
35
+ * **agent:** retry a transient NOT_FOUND on subscribe ([#100](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/100)) ([#101](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/101)) ([ffb0251](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/commit/ffb02514aaaeb9351bf465c00e808833536d3568))
36
+ * **auth:** Auth.devAgent sets expectedSender to enforce sender identity client-side ([#133](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/133)) ([cc0f344](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/commit/cc0f344f2cbb4cf4d59e17fbe4d043cc6f9eebac))
37
+ * **client:** map stream inline errors and registry gRPC statuses to MacpAckError ([#118](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/118)) ([28d4c52](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/commit/28d4c527c324469848ce90a9199b7318970dd071))
38
+ * **commitment-hash:** supersedes: null now hashes as absent, matching macp-sdk-python ([#114](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/114)) ([6e15b9f](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/commit/6e15b9fbc2fc61699970ed30276fd2e249152f08))
39
+ * **index:** drop _resetLoggingForTests from public surface; correct pagination/ListRoots docs ([#112](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/112)) ([99c2f4a](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/commit/99c2f4a67c5200a60f3d7facfbec0ab0dc08eb85))
40
+ * **parity:** re-vendor contract.json for contract_version 1.1.1 ([#129](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/129)) ([459e795](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/commit/459e7959a5eabaa32008b8750bbf33c972e151a3)), closes [#125](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/125)
41
+ * **parity:** re-vendor contract.json for contract_version 1.2.0 ([#136](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/136)) ([8ef1ab1](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/commit/8ef1ab1861e8e871cf3284ee3e125fceba7e2691)), closes [#135](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/135)
42
+ * **projections:** gate TaskComplete/TaskFail phase transitions on task existence ([#120](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/120)) ([41f9010](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/commit/41f90109df032c8072052232cd4501d2bbda0fed)), closes [#111](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/111)
43
+ * **projections:** gate three more phase transitions on entity existence ([#123](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/123)) ([866490a](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/commit/866490a332353049064596371dd1e05b4598062f)), closes [#119](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/119)
44
+ * **projections:** guard QuorumProjection.setBallot on request existence ([#127](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/127)) ([e3b75aa](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/commit/e3b75aa265e019610cfd7b0d0446a38a93437d01)), closes [#121](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/121)
45
+ * **projections:** unfreeze 5 of 6 ProjectionAnomalyKind sites ([#126](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/126), [#128](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/128)) ([#134](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/134)) ([a99668c](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/commit/a99668cf8977309c91c0390f5c0967ad80c2786b))
46
+ * **proto-registry:** stop coercing legacy-JSON Contribute value ([#124](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/124)) ([#130](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/130)) ([02bbe90](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/commit/02bbe90c48824e82e0436d315dd0582d057ffec2))
47
+ * **proto-registry:** stop misdecoding canonical Contribute payloads as JSON ([#110](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/110)) ([26dddb6](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/commit/26dddb61eb7ff163c23ecbd6e96c2abedfab150c))
48
+ * **sessions,agent,auth:** thread auth through start(), default identities, validate strategy output ([#116](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/116)) ([4c633e0](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/commit/4c633e02f11bc5f6a8995346b9b4cdc0213003c0))
49
+ * **strategies:** exclude REVIEW evaluations from majorityVoter ratio ([#124](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/124)) ([#131](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/131)) ([b52c7f2](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/commit/b52c7f2744bd5841a023461a4009c411a9514b75))
50
+ * **validation:** enforce no-fall-through session-id rule, reject NaN confidence, normalize decision.ts wire case ([#115](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/115)) ([df1c6a2](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/commit/df1c6a2c966f2f5f88937b3b2fcc2a160edbd987))
51
+ * **validation:** stop rejecting empty/omitted intent, instructions, action, summary ([#124](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/124)) ([#132](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/132)) ([aa157ee](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/commit/aa157eeff8d284c1cc727c7cb0d73a86f41ab35b))
52
+
7
53
  ## [0.11.0](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/compare/v0.10.0...v0.11.0) (2026-09-22)
8
54
 
9
55
 
@@ -69,6 +115,22 @@ project uses [Semantic Versioning](https://semver.org/).
69
115
  pass `schemaVersion` explicitly get going forward. Pass
70
116
  `{ schemaVersion: 1 }` or `{ schemaVersion: 2 }` explicitly to keep the
71
117
  old fail-open behavior.
118
+ * **auth:** `Auth.devAgent(agentId)` now also sets `expectedSender: agentId`
119
+ on the returned `AuthConfig` (issue #124 item 4). The runtime's dev
120
+ fallback already authenticates the gRPC connection as the bearer value
121
+ (`agentId`) and unconditionally refuses a mismatched `sender` for dev
122
+ credentials (`macp-runtime/src/server.rs:229-231`, no insecure/dev
123
+ exemption) — this SDK now enforces the same guarantee client-side instead
124
+ of surfacing the runtime's rejection late and confusingly. A bootstrap or
125
+ caller whose `agent_id`/credential diverges from the `sender`/
126
+ `participant_id` it actually sends now fails on the first typed
127
+ handler-driven action (e.g. `acceptTask`, `vote`) with
128
+ `MacpIdentityMismatchError`, instead of only failing server-side. The
129
+ initiator's `SessionStart`/kickoff path is unaffected (it passes no
130
+ explicit `sender`), as is the generic `actions.send` escape hatch (which
131
+ still only fails server-side). A caller that genuinely needs one
132
+ credential reused across multiple senders should use
133
+ `Auth.bearer(token, senderHint)` (the legacy two-arg form) instead.
72
134
 
73
135
  ### Features
74
136
 
@@ -98,6 +160,327 @@ project uses [Semantic Versioning](https://semver.org/).
98
160
  legitimate retry with a corrected payload was silently absorbed as a
99
161
  redelivery and the envelope's effect was lost forever.
100
162
 
163
+ ### Added
164
+
165
+ - **All five mode sessions' `start()` now accept a per-call
166
+ `auth?: AuthConfig`** (issue #108, part 2 of 2 — 4B), matching every other
167
+ action method on these classes. Previously `start()` alone ignored a
168
+ per-call `auth` on `DecisionSession`/`ProposalSession`/`TaskSession`/
169
+ `HandoffSession`/`QuorumSession`, resolving the sender only against the
170
+ session/client-level credential — so a caller passing a conflicting
171
+ per-call `auth.expectedSender` never got the identity-mismatch guard
172
+ (RFC-MACP-0004 §4) that every other method already enforced.
173
+ `BaseSession.start()` already accepted and threaded a per-call `auth`
174
+ before this change; see the `BaseSession.start()` validation entry below
175
+ for what actually changed there in 4B.
176
+ - **`ProjectionAnomalyKind` gains two new, cross-SDK-agreed kinds:
177
+ `duplicate_task_accept` and `settled_handoff`** (issue #126/#128;
178
+ `macp-sdk-python` landed its half in PR #95). `TaskProjection` now records
179
+ a `duplicate_task_accept` anomaly when a `TaskAccept` for a task it has a
180
+ `TaskRequest` on file for is discarded because another task already holds
181
+ the session's one assignee slot (RFC-MACP-0009 §5 rules 3/3a).
182
+ `HandoffProjection` now records a `settled_handoff` anomaly when a
183
+ `HandoffAccept`/`HandoffDecline` targets a `handoff_id` that already
184
+ settled as accepted or declined (RFC-MACP-0010 §5 rule 4). Both new
185
+ constants (`ANOMALY_DUPLICATE_TASK_ACCEPT`/`ANOMALY_SETTLED_HANDOFF`)
186
+ export from the package root alongside the existing pair. A `TaskAccept`/
187
+ `HandoffAccept`/`HandoffDecline` for an *unknown* `task_id`/`handoff_id`
188
+ still records no anomaly — that determination (not caller misuse) is
189
+ unchanged by this release. This is purely additive observability: no
190
+ change to `phase`, `tasks`, or `handoffs` state itself at any of these
191
+ sites. `DecisionProjection`'s late-`Vote`-after-`Commitment` site remains
192
+ the one still-open, undecided case (per #128's own explicit scoping) —
193
+ unaffected by this change.
194
+
195
+ ### Changed
196
+
197
+ - **BREAKING (test-only surface): `_resetLoggingForTests` is no longer part
198
+ of the package's public export surface.** (issue #109) It was reachable
199
+ only via `src/index.ts`'s `export * from './logging'` wildcard; that line
200
+ is now a named re-export listing `logger`, `configureLogging`, `LogLevel`,
201
+ and `LogSink`. Because `package.json`'s `exports` map exposes only `"."`,
202
+ this makes the helper unreachable from the published package entirely (no
203
+ submodule fallback) — accepted, since it is test-only by name and by
204
+ purpose, and `configureLogging` already covers every legitimate caller
205
+ need.
206
+ - **`validateSessionId` now enforces the runtime's actual no-fall-through
207
+ rule** (issue #108, part 1 of 2 — 4A). A UUID-**shaped** string (36 chars,
208
+ hyphens at 8-13-18-23, hex-only) is now validated strictly as a lowercase
209
+ v4/v7 UUID and never reinterpreted as base64url. An uppercase UUID, a
210
+ lowercase v1 UUID, the nil UUID, or a UUID with a bad variant nibble now
211
+ throws `MacpSessionError` client-side — all were already rejected by the
212
+ runtime, so nothing that previously *worked* stops working, but a caller
213
+ relying on the old, looser client-side check now gets the rejection
214
+ earlier. Ports `macp-sdk-python`'s two-regex no-fall-through structure.
215
+ - **`DecisionSession.evaluate`/`raiseObjection`/`vote` now put the
216
+ normalized (uppercased/lowercased) value on the wire, not the caller's
217
+ original case** (issue #108, part 1 of 2 — 4A).
218
+ `validateVote`/`validateRecommendation`/`validateSeverity` each return a
219
+ normalized string; the three call sites in `decision.ts` previously
220
+ discarded it. A consumer that string-compares an un-normalized value
221
+ (e.g. `'approve'` instead of `'APPROVE'`) sees a different value on the
222
+ wire; every in-repo projection already normalizes case itself, so no
223
+ projection assertion is affected by this change.
224
+ - **An empty/missing `bearerToken` now throws `MacpSdkError`, not a bare
225
+ `Error`** (issue #108, part 2 of 2 — 4B). `validateAuth` (and therefore
226
+ `metadataFromAuth`, reached from every authenticated RPC) previously threw
227
+ a plain `Error('bearerToken is required')`, which is not `instanceof
228
+ MacpSdkError` — escaping the documented `catch (e) { if (e instanceof
229
+ MacpSdkError) ... }` pattern from both SDKs' error-handling docs. Message
230
+ text unchanged.
231
+ - **`BaseSession.start()` now runs the same full `validateSessionStart`
232
+ check as the five built-in mode sessions, not a partial check** (issue
233
+ #108, part 2 of 2 — 4B). Previously this documented ext-mode extension
234
+ point validated only participant count and `maxSuspendMs`, so an empty
235
+ `intent`, an empty/duplicate `participants` array, or an out-of-range
236
+ `ttlMs` reached the wire unvalidated from a custom mode built on
237
+ `BaseSession` — the weakest-validated mode in the SDK. All five now
238
+ reject client-side, matching every built-in mode session.
239
+ - **BREAKING (narrows an exception type): `send()` now throws `MacpAckError`,
240
+ not `MacpTransportError`, for a gRPC `ALREADY_EXISTS`/`FAILED_PRECONDITION`;
241
+ same for `registerExtMode`/`unregisterExtMode`/`promoteMode`/
242
+ `registerPolicy`/`unregisterPolicy` on `FAILED_PRECONDITION`** (issue #107).
243
+ These are application-level rejections the runtime couldn't express as a
244
+ normal Ack, not transport faults — `MacpAckError` (`failure.code`
245
+ `'SESSION_ALREADY_EXISTS'` / `'POLICY_DENIED'` / `'FAILED_PRECONDITION'`
246
+ respectively) is the correct, Python-parity model
247
+ (`client.py:442-452`, `_map_registry_mutation_error` at `client.py:347-362`).
248
+ Code today catching `MacpTransportError` around these calls for these
249
+ specific statuses stops matching (`MacpAckError` extends `MacpSdkError`
250
+ directly, not `MacpTransportError`); every other gRPC status on these RPCs,
251
+ and every status on every other RPC, is unaffected. A knock-on effect on
252
+ `retrySend()`: a `FAILED_PRECONDITION` `send()` was previously retried to
253
+ exhaustion (no code filter on `MacpTransportError`) and surfaced as
254
+ `MacpRetryError`; it now surfaces immediately as `MacpAckError`, since
255
+ `POLICY_DENIED` is not in `DEFAULT_RETRY_POLICY.retryableCodes` — strictly
256
+ more correct (neither condition is retryable) and no longer masks the real
257
+ error behind a generic "retries exhausted."
258
+ - **`TaskProjection.isComplete(taskId)` renamed to `isCompleted(taskId)`**
259
+ (issue #138, cross-SDK naming decision at
260
+ multiagentcoordinationprotocol#135). Every other `is*` predicate in this
261
+ SDK is past-participle (`isFailed`, `isAccepted`, `isTerminallyRejected`,
262
+ `isDeclined`); `isComplete` was the outlier, and `macp-sdk-python` has
263
+ always spelled this `is_completed`. The old name was kept as a deprecated
264
+ alias for one minor, then removed — see `### Removed` below.
265
+ - **`TaskCompletionRecord` renamed to `TaskCompleteRecord`, and
266
+ `TaskFailureRecord` renamed to `TaskFailRecord`** (issue #138, cross-SDK
267
+ naming decision at multiagentcoordinationprotocol#135). Both now mirror
268
+ their triggering `TaskComplete`/`TaskFail` message types the way their
269
+ sibling `TaskUpdateRecord` already mirrors `TaskUpdate`, and match
270
+ `macp-sdk-python` and `macp-runtime`'s own names for the same records. The
271
+ old names were kept as deprecated aliases for one minor, then removed —
272
+ see `### Removed` below.
273
+
274
+ ### Removed
275
+
276
+ - **`TaskProjection.isComplete(taskId)`** (issue #138/#140, breaking): the
277
+ deprecated alias (added in `0.12.0`) has been removed. Use
278
+ `isCompleted(taskId)`.
279
+ - **`TaskCompletionRecord`/`TaskFailureRecord`** (issue #138/#140, breaking):
280
+ the deprecated type aliases (added in `0.12.0`) have been removed. Use
281
+ `TaskCompleteRecord`/`TaskFailRecord` respectively.
282
+
283
+ ### Fixed
284
+
285
+ - **`validateConfidence` now rejects `NaN`** (issue #108, part 1 of 2 — 4A).
286
+ `NaN < 0` and `NaN > 1.0` are both `false`, so a bare range check silently
287
+ accepted `NaN` — verified by execution before this fix. `±Infinity` was
288
+ already rejected (incidentally); this also rejects it explicitly via
289
+ `Number.isFinite`, matching this file's own `ttlMs`/`maxSuspendMs` idiom.
290
+ - **`commitmentHash`/`canonicalizeCommitmentPayload`: `supersedes: null` now
291
+ hashes as absent, matching `macp-sdk-python`** (issue #105). `supersedes`
292
+ is a proto3 message-typed field with only present/absent wire states;
293
+ `null` is the idiomatic in-language representation of "absent" (e.g. under
294
+ a `defaults: true` protobufjs/proto-loader decode, or a payload
295
+ round-tripped through JSON). Previously `supersedes: null` took the
296
+ *present* branch and hashed identically to an explicit
297
+ `{ sessionId: '', commitmentHash: '' }`, producing a different digest than
298
+ `macp-sdk-python` computes for the same logical commitment. The predicate
299
+ changed from `p.supersedes !== undefined` to `p.supersedes != null`
300
+ (deliberately loose — catches `null` and `undefined` only, so `0`, `''`,
301
+ `false`, and other falsy-but-not-nullish values still take the present
302
+ branch, unaffected). Not a protocol MINOR bump: the hashing `LABEL` and
303
+ frozen field set are unchanged — this corrects a projection bug, not the
304
+ algorithm. Any digest a caller already computed by feeding `null` was
305
+ already wrong (Python never reproduced it), so there is no
306
+ correct-value regression, but the fix is a security/audit-relevant
307
+ behavior change on this SDK's own output.
308
+ - **`Participant` lifecycle: `SessionCancel` now reaches `onTerminal`, and
309
+ teardown runs on every exit from `run()`, not just an explicit `stop()`**
310
+ (issue #106). Three independent bugs in `src/agent/participant.ts`:
311
+ - A runtime-issued `SessionCancel` never fired `onTerminal` at all — no
312
+ built-in projection maps it to a terminal phase, so the phase-driven
313
+ dispatch path had nothing to trigger on. `Participant` now recognizes
314
+ `SessionCancel` directly as a fallback when the phase path doesn't fire,
315
+ dispatching `{ state: 'Cancelled' }`. Against the current `macp-runtime`,
316
+ this fallback's practical reach is `processEvent()` callers and
317
+ cross-SDK parity with `macp-sdk-python`'s identical fallback — the
318
+ streamed `run()` path never observes a `SessionCancel` at all today,
319
+ since `cancel_session` stores it as `EntryKind::Internal` and
320
+ `get_incoming_after` filters strictly to `EntryKind::Incoming`;
321
+ cancellation surfaces via `session_lifecycle_bus`/`WatchSessions`
322
+ instead.
323
+ - `run()`'s `finally` only cleared an internal flag; the transport
324
+ subscription and the cancel-callback HTTP listener were torn down
325
+ exclusively inside `stop()`. A normal terminal exit, or a transport
326
+ simply running out of messages, leaked both — a long-lived agent
327
+ process running many sessions sequentially leaked one gRPC stream
328
+ subscription and one listening TCP socket per completed session.
329
+ Teardown now runs on every exit from `run()`.
330
+ - `isStopped` computed `!running`, so it read `true` immediately after
331
+ construction, before `run()` had ever been called. Fixed by tracking
332
+ three independent states (`running` / `terminal` / `stopRequested`)
333
+ instead of one flag — deliberately **not** a single latch copied from
334
+ `macp-sdk-python`, because that would make a `Participant` single-use
335
+ after any `stop()`, breaking this SDK's own documented and tested
336
+ restart contract (issue #66): `stop()` mid-stream, then a second
337
+ `run()` on the same instance, must resume rather than refuse. Only a
338
+ genuinely *terminal* outcome is single-use; a `stop()`-only exit is a
339
+ resumable pause.
340
+ - **`BaseProjection`'s rollback pop is now guarded by reference identity**
341
+ (issue #106, item 4): it no longer unconditionally pops the last
342
+ `transcript` entry on a failed decode. `BaseProjection` is documented
343
+ subclassable API; a third-party subclass whose `applyMode` pushes its own
344
+ entry onto `transcript` before throwing would previously have had that
345
+ entry silently removed instead of the envelope actually under
346
+ application — data corruption strictly worse than the bug the rollback
347
+ itself fixes. Mirrors `macp-sdk-python`'s identical guard and its own
348
+ documented trade-off in this exact scenario.
349
+ - **Identity fields on `Task`/`Handoff` actions now default to the resolved
350
+ sender when omitted or empty, matching `macp-sdk-python`** (issue #108,
351
+ part 2 of 2 — 4B): `TaskSession.acceptTask`/`rejectTask`/`completeTask`/
352
+ `failTask`'s `assignee`, and `HandoffSession.acceptHandoff`'s `acceptedBy`/
353
+ `decline`'s `declinedBy`, are now optional and fall back to the caller's
354
+ authenticated identity (`input.field || resolvedSender` — `||`, not `??`,
355
+ since an explicit empty string is also meant to fall back). Previously
356
+ these were required fields passed straight through with no default, unlike
357
+ the Python SDK (`task.py:299,323,391,430`, `handoff.py:247,271`).
358
+ - **`HandoffSession.addContext`'s `contentType` now defaults to
359
+ `'application/octet-stream'` when omitted**, matching `macp-sdk-python`
360
+ (`handoff.py:210`) (issue #108, part 2 of 2 — 4B). Previously `contentType`
361
+ was a required field with no default.
362
+ - **`evaluationHandler`/`votingHandler` now validate a strategy's output
363
+ before dispatching it** (issue #108, part 2 of 2 — 4B). Both previously
364
+ passed a custom `EvaluationStrategy`/`VotingStrategy`'s raw
365
+ `recommendation`/`confidence`/`vote` straight to `ctx.actions.*` with no
366
+ validation, so a buggy strategy (an out-of-enum recommendation, a `NaN`
367
+ confidence, an out-of-enum vote) failed later and less legibly, from
368
+ inside the session rather than at the handler that produced the bad
369
+ value. Both handlers now reuse this SDK's own `validateRecommendation`/
370
+ `validateConfidence`/`validateVote` (throwing the documented
371
+ `MacpSessionError`) and dispatch the validators' normalized return value.
372
+ - **`MacpStream`'s inline-error path was reading a shape a real decode never
373
+ produces, silently dropping every inline application-level error** (issue
374
+ #107). `StreamSessionResponse.response` is a proto3 `oneof`; under this
375
+ file's own `@grpc/proto-loader` options (`oneofs: true`), the decoded
376
+ object exposes the payload directly as `chunk.envelope`/`chunk.error`, with
377
+ `chunk.response` set to the oneof's *arm name as a string* (`'envelope'` or
378
+ `'error'`) — not a nested object. The old code read
379
+ `chunk?.response?.error`, which indexes a string and is always `undefined`
380
+ on every message that has ever crossed this stream; the corresponding unit
381
+ test only ever passed because it emitted the same fictional
382
+ `{ response: { error } }` shape by hand rather than a real decode. Fixed to
383
+ read `chunk.error` directly, and each inline error is now also logged at
384
+ `warn` (it previously produced no log line anywhere).
385
+ - **`TaskProjection.phase` no longer advances to `'Completed'`/`'Failed'` for
386
+ a `TaskComplete`/`TaskFail` naming an unknown `task_id`** (issue #111).
387
+ Both `this.phase` assignments sat outside the `if (task) { ... }` guard
388
+ that already gated every other per-task field mutation in the same
389
+ `applyMode` cases, so a message for a `task_id` this projection never saw
390
+ a `TaskRequest` for still flipped the session-level `phase` to a terminal
391
+ state even though no task actually completed or failed. Moved both
392
+ assignments inside their sibling `if (task)` block, matching the guard
393
+ already correct on `TaskAccept` (issue #71/#70) and on
394
+ `HandoffProjection`'s analogous `phase` transitions.
395
+ - **Three more `phase`-transition-outside-guard sites, same bug shape as
396
+ issue #111, found during that fix's verification** (issue #119):
397
+ - `ProposalProjection`'s `Reject` case moved `this.phase =
398
+ 'TerminalRejected'` inside the sibling `if (proposal) { ... }` guard. A
399
+ terminal `Reject` for an unknown `proposalId` no longer advances `phase`
400
+ — which matters beyond bookkeeping, since `'TerminalRejected'` is a
401
+ `TERMINAL_PHASES` member in `src/agent/participant.ts`, so the bug could
402
+ end a live `Participant`'s `run()` loop for a session that never
403
+ actually terminated.
404
+ - `HandoffProjection`'s `HandoffContext` case moved its `phase` transition
405
+ (`'OfferPending'` → `'ContextSharing'`) inside the sibling `if (handoff)
406
+ { ... }` guard, matching `HandoffAccept`/`HandoffDecline`, which were
407
+ already correct.
408
+ - `DecisionProjection`'s `Vote` case gained a new existence guard (none
409
+ existed before): `if (!this.proposals.has(record.proposalId)) break;`,
410
+ per RFC-MACP-0007 §5 rule 2 ("`Evaluation`, `Objection`, and `Vote` MUST
411
+ reference an existing `proposal_id`"). Previously a `Vote` for an unknown
412
+ `proposalId` fabricated a `votes` `Map` entry and flipped `phase` to
413
+ `'Voting'` — the fabricated entry also inflated `voteTotals()`'s and
414
+ `majorityWinner()`'s denominators, able to flip a real proposal from
415
+ winner to non-winner in a close tally.
416
+
417
+ All three sites' `rejections`/audit-array-style writes remain unconditional
418
+ (append-only logs, matching the runtime), so query methods reading from
419
+ them (`isTerminallyRejected`, `hasTerminalRejection`) can still report
420
+ `true` for an unknown id even though `phase` no longer moves — intentional,
421
+ not a residual gap.
422
+ - **`QuorumProjection.setBallot` no longer fabricates a `ballots` entry for an
423
+ unknown `request_id`** (issue #121, found during issue #119's plan-review
424
+ sweep). RFC-MACP-0011 §5 rule 3 requires a ballot (`Approve`/`Reject`/
425
+ `Abstain`) to reference the Session's accepted `request_id`; there was no
426
+ check against `this.requests` before creating or extending a `ballots` Map
427
+ entry, so an unfiltered/multi-session transcript could make
428
+ `votedSenders()` and `approvalCount()`/`rejectionCount()`/
429
+ `abstentionCount()` report on a `request_id` that was never opened, and a
430
+ second such ballot from the same sender could fire a spurious
431
+ `duplicate_ballot` anomaly against it. Unlike the three sites above, this
432
+ bug never touched `phase` — `QuorumProjection.phase` is set only by
433
+ `ApprovalRequest` and by `Commitment` handling in `BaseProjection` — so the
434
+ fix is a single existence guard at the top of `setBallot`, with no phase
435
+ component.
436
+ - **`ProtoRegistry.decodeMultiRoundContribute` no longer coerces a legacy-JSON
437
+ `Contribute` payload's `value` to a string** (issue #124). Previously
438
+ `String(parsed.value ?? '')` made an absent `value` key indistinguishable
439
+ from a genuine empty string, and stringified any non-string JSON value
440
+ (`123` decoded to `'123'`, `null` decoded to `''`). The legacy-JSON decode
441
+ path now passes `parsed.value` through exactly as `JSON.parse` produced
442
+ it — `undefined` stays `undefined`, `null` stays `null`, a number stays a
443
+ number. The canonical-protobuf decode path is unaffected (its `value` is
444
+ always a `string`, per the proto schema).
445
+ - **`majorityVoter` no longer counts `REVIEW` evaluations in its approval
446
+ ratio** (issue #124). RFC-MACP-0007 §4 states `REVIEW` evaluations "do not
447
+ block or approve a proposal; they serve as informational analysis records
448
+ only" — a `REVIEW` was nonetheless lowering the ratio's denominator, so a
449
+ proposal with one `APPROVE` and one `REVIEW` voted at ratio `0.5` instead
450
+ of `1.0`. The ratio's denominator is now the `decisive` evaluations
451
+ (everything except `REVIEW`, case-insensitive); `shouldVote` now returns
452
+ `false` for an evaluation set of only `REVIEW`s (no decisive evaluation to
453
+ vote on) instead of voting `REJECT` at ratio `0`. `BLOCK` and `REJECT`
454
+ remain in the denominator — they are stances, not informational-only
455
+ records.
456
+ - **`intent`, `instructions`, `action`, and `summary` no longer rejected when
457
+ empty or omitted** (issue #124). `SessionStart.intent`,
458
+ `TaskSession.requestTask`'s `instructions`, and
459
+ `QuorumSession.requestApproval`'s `action`/`summary` are proto3 singular
460
+ `string` fields — implicit presence means an omitted value and an explicit
461
+ `''` serialize to identical bytes, so a client-side "required, non-empty"
462
+ check rejected a payload the runtime would accept and no runtime-side check
463
+ could ever distinguish. `intent` also has an explicit normative rule
464
+ (RFC-MACP-0001 §7.1: a runtime "MUST NOT reject a SessionStart solely
465
+ because intent is empty"). Every other `validateRequiredField` call
466
+ site — `taskId`, `title`, `requestId`, `proposalId`, `option`, `handoffId`,
467
+ `targetParticipant`, `modeVersion`, `configurationVersion` — is unaffected
468
+ and still rejects an empty value.
469
+
470
+ ### Documentation
471
+
472
+ - **Pagination memory caveat** (issue #109): `docs/api/client.md`'s
473
+ `listSessions` and `listSessionsPage` entries now state plainly that while
474
+ `macp-runtime` ≥ 0.7.0 (current: 0.8.3) implements real server-side
475
+ pagination, `listSessions()` itself still accumulates every page into one
476
+ in-memory array regardless of `pageSize` — callers who need bounded
477
+ memory should call `listSessionsPage()` directly.
478
+ - **`ListRoots` wording** (issue #109): `docs/guides/streaming.md` no
479
+ longer says the runtime "serves `ListRoots`," which reads as "returns
480
+ real data" — reworded to state plainly that the runtime always returns an
481
+ empty root list. `docs/api/client.md`'s `listRoots` entry gains the same
482
+ statement, which it previously lacked entirely.
483
+
101
484
  ## [0.10.0](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/compare/v0.9.0...v0.10.0) (2026-09-01)
102
485
 
103
486
 
package/README.md CHANGED
@@ -158,7 +158,7 @@ await session.commit({ action: 'task.completed', authorityScope: 'lead', reason:
158
158
 
159
159
  // Projection queries
160
160
  session.projection.progressOf('t1'); // 1.0
161
- session.projection.isComplete('t1'); // true
161
+ session.projection.isCompleted('t1'); // true
162
162
  session.projection.activeTasks(); // []
163
163
  ```
164
164
 
@@ -4,4 +4,4 @@ export * from './dispatcher';
4
4
  export * from './participant';
5
5
  export * from './strategies';
6
6
  export * from './runner';
7
- export { GrpcTransportAdapter, HttpTransportAdapter, type TransportAdapter, type HttpPollingConfig, } from './transports';
7
+ export { GrpcTransportAdapter, HttpTransportAdapter, DEFAULT_SUBSCRIBE_RETRY_POLICY, type TransportAdapter, type HttpPollingConfig, type SubscribeRetryPolicy, } from './transports';
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.HttpTransportAdapter = exports.GrpcTransportAdapter = void 0;
17
+ exports.DEFAULT_SUBSCRIBE_RETRY_POLICY = exports.HttpTransportAdapter = exports.GrpcTransportAdapter = void 0;
18
18
  __exportStar(require("./types"), exports);
19
19
  __exportStar(require("./cancel-callback"), exports);
20
20
  __exportStar(require("./dispatcher"), exports);
@@ -24,3 +24,4 @@ __exportStar(require("./runner"), exports);
24
24
  var transports_1 = require("./transports");
25
25
  Object.defineProperty(exports, "GrpcTransportAdapter", { enumerable: true, get: function () { return transports_1.GrpcTransportAdapter; } });
26
26
  Object.defineProperty(exports, "HttpTransportAdapter", { enumerable: true, get: function () { return transports_1.HttpTransportAdapter; } });
27
+ Object.defineProperty(exports, "DEFAULT_SUBSCRIBE_RETRY_POLICY", { enumerable: true, get: function () { return transports_1.DEFAULT_SUBSCRIBE_RETRY_POLICY; } });
@@ -39,8 +39,10 @@ export interface ParticipantConfig {
39
39
  /**
40
40
  * Bind a cancel-callback HTTP endpoint (RFC-0001 §7.2 Option A) when
41
41
  * this participant's event loop starts. The server is closed
42
- * automatically when {@link Participant.stop} is called. Parity with
43
- * python-sdk's bootstrap `cancel_callback` field.
42
+ * automatically on any exit from {@link Participant.run} — an explicit
43
+ * {@link Participant.stop}, a terminal outcome, or the transport running
44
+ * out of messages. Parity with python-sdk's bootstrap `cancel_callback`
45
+ * field.
44
46
  */
45
47
  cancelCallback?: {
46
48
  host: string;
@@ -64,7 +66,23 @@ export declare class Participant {
64
66
  private readonly modeVersion?;
65
67
  private readonly configurationVersion?;
66
68
  private readonly policyVersion?;
69
+ /** Whether the event loop is currently inside {@link run}. */
67
70
  private running;
71
+ /**
72
+ * Latching: set once a terminal result is dispatched (a terminal phase,
73
+ * or the {@code SessionCancel} fallback in {@link processMessage}). A
74
+ * terminal `Participant` is single-use — {@link run} refuses to
75
+ * re-enter, because the projection is resolved and the runtime will
76
+ * accept nothing further for this session.
77
+ */
78
+ private terminal;
79
+ /**
80
+ * The cooperative cancel flag {@link stop} sets and {@link run} clears
81
+ * on entry — the loop's break predicate. Distinct from {@link terminal}:
82
+ * a `stop()`-exited, non-terminal `Participant` is a resumable pause
83
+ * (see {@link isStopped}), not single-use.
84
+ */
85
+ private stopRequested;
68
86
  private lastPhase;
69
87
  private cancelCallbackServer?;
70
88
  private readonly cancelCallbackConfig?;
@@ -74,7 +92,12 @@ export declare class Participant {
74
92
  on(messageType: string, handler: MessageHandler): Participant;
75
93
  onPhaseChange(phase: string, handler: PhaseChangeHandler): Participant;
76
94
  onTerminal(handler: TerminalHandler): Participant;
77
- /** Whether {@link stop} has been called (or the loop has exited). */
95
+ /**
96
+ * `true` after a terminal outcome (single-use from here on), or while a
97
+ * `stop()` request is pending/has taken effect on a `Participant` whose
98
+ * loop has not (yet) been resumed by a later {@link run}. A `stop()`
99
+ * exit is resumable — see {@link run} and {@link terminal}'s own doc.
100
+ */
78
101
  get isStopped(): boolean;
79
102
  run(): Promise<void>;
80
103
  /**
@@ -87,10 +110,30 @@ export declare class Participant {
87
110
  private buildHandlerContext;
88
111
  /** Returns true if a terminal state was reached. */
89
112
  private processMessage;
113
+ /**
114
+ * Idempotent teardown of everything {@link run} started: the transport
115
+ * subscription and the cancel-callback HTTP listener, if any. Called
116
+ * from both {@link stop} and every exit from {@link run}'s loop (issue
117
+ * #106.2) — a normal terminal exit or a transport running out of
118
+ * messages used to leak both. A rejecting `transport.stop()` is caught
119
+ * rather than propagated, so a transport fault on teardown cannot mask
120
+ * a clean session outcome as a thrown error from {@link run}.
121
+ */
122
+ private teardown;
123
+ /**
124
+ * Request that {@link run}'s loop stop. This SDK's {@link TransportAdapter}
125
+ * has no `cancel()` — `stop()` on the transport is the only thing that can
126
+ * wake a loop suspended awaiting the next envelope, so teardown (including
127
+ * the transport) runs here unconditionally, not just the flag. Safe to
128
+ * call whether or not `run()` is currently active; does not itself flip
129
+ * {@link running} — only `run()`'s own loop exit does that, since it is
130
+ * the only place that actually knows the loop has left.
131
+ */
90
132
  stop(): Promise<void>;
91
133
  /**
92
134
  * Attach a running cancel-callback HTTP server to this participant.
93
- * The server is closed automatically when {@link stop} is called.
135
+ * The server is closed automatically on any exit from {@link run} — see
136
+ * {@link teardown}.
94
137
  * Parity with python-sdk's `Participant.attach_cancel_callback_server`.
95
138
  */
96
139
  attachCancelCallbackServer(server: CancelCallbackServer): void;