macp-sdk-typescript 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,102 @@ 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.10.0](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/compare/v0.9.0...v0.10.0) (2026-09-01)
8
+
9
+
10
+ ### ⚠ BREAKING CHANGES
11
+
12
+ * `client.sendProgress()` now throws `MacpSessionError` when exactly one of `sessionId`/`mode` is supplied. Such calls used to succeed against a runtime that had the symmetric gap; as of macp-runtime PR #137 they are rejected with `INVALID_ENVELOPE`. Pass both fields for a session-scoped Progress, or neither for an ambient one.
13
+ * **projections:** `TaskProjection` assignee semantics changed twice. (1) The first-accept-wins guard is now per-session, not per-`task_id`: on a transcript with two `TaskRequest`s, only the first `TaskAccept` assigns — the second task keeps `assignee === undefined` and `status === 'requested'`, where it previously got its own assignee. A `TaskAccept` the guard discards also no longer sets `phase = 'InProgress'`. (2) `TaskAccept` is no longer sticky forever: a `TaskReject` from the active assignee clears `assignee` back to `undefined`, so code reading `getTask(id).assignee` after a reject now sees `undefined` (or the reassigned participant) rather than the participant who rejected.
14
+
15
+ ### Bug Fixes
16
+
17
+ * **projections:** scope the Task assignee slot to the session and free it on reject ([#75](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/75)) ([f7841a9](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/commit/f7841a96d120b3fc55effdfabf63a2d72f431081)), closes [#59](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/59) [#60](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/60) [#70](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/70) [#71](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/71)
18
+ * reject mixed ambient/session-scoped Progress envelopes ([#76](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/76)) ([a9e00e3](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/commit/a9e00e370b7a4bf08d09190e8b75a12d221319f8))
19
+
20
+ ## [0.9.0](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/compare/v0.8.0...v0.9.0) (2026-09-01)
21
+
22
+
23
+ ### ⚠ BREAKING CHANGES
24
+
25
+ * **projections:** ProposalProjection.isAccepted()/acceptedProposal(), TaskProjection's `assignee` field, and HandoffProjection's `status` field can now derive different values than before for a transcript containing the message sequences described above. Conforming transcripts (no superseded accept, no second TaskAccept/contradictory handoff accept-decline) are unaffected.
26
+
27
+ ### Bug Fixes
28
+
29
+ * **agent:** don't count an envelope into the resume cursor until it's asked for again ([#72](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/72)) ([f1b7015](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/commit/f1b70155cb3293bc7f36c0389cbf880359a01e58)), closes [#66](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/66)
30
+ * **agent:** resume GrpcTransportAdapter from its own cursor, and count distinct envelopes ([#65](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/65)) ([7535763](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/commit/75357632351addde3a32a61b1200e823f2ef70da))
31
+ * **projections:** enforce cardinality rules issues [#59](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/59)/[#60](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/60) said didn't exist ([#68](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/68)) ([956d881](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/commit/956d881e19591bcd9a04c6496bff07213d1ee6d3))
32
+
33
+ ## [Unreleased]
34
+
35
+ ### Fixed
36
+
37
+ - **BREAKING: four projection sites now enforce the cardinality rule their
38
+ governing RFC section actually states, instead of last-write-wins.**
39
+ Issues #59 and #60 argued that no cardinality/ordering site in the
40
+ built-in projections had a normative rule behind it; a full re-read of the
41
+ four mode RFCs plus RFC-MACP-0001 found governing rules for five of the
42
+ ten sites they raised, three of which were shipped conformance
43
+ violations:
44
+ - `ProposalProjection` (breaking): a later `Accept` from a participant now
45
+ supersedes their earlier one (RFC-MACP-0008 §5 rule 5, `:70`), so
46
+ `isAccepted` and `acceptedProposal()` read the live acceptance set
47
+ instead of raw accept history. Previously `acceptedProposal()` could
48
+ return `undefined` after a legal re-accept, where the RFC's
49
+ determinism clause (§7, `:89`) makes the accepted proposal unambiguous.
50
+ - `TaskProjection` (breaking): the first accepted `TaskAccept` now wins —
51
+ a later `TaskAccept` for the same task no longer overwrites `assignee`
52
+ (RFC-MACP-0009 §5 rules 3/3a, `:69-71`). The policy-gated reassignment
53
+ path (rule 3c) is intentionally not modelled yet; `TaskProjection` has
54
+ no session-policy input.
55
+ - `HandoffProjection` (breaking): a `handoff_id` now settles (`offered` →
56
+ `accepted`/`declined`) exactly once — a later contradictory
57
+ `HandoffAccept`/`HandoffDecline` for an already-settled `handoff_id`,
58
+ or for a `handoff_id` that was never offered, is ignored in both
59
+ directions and can no longer mutate `phase` (RFC-MACP-0010 §5 rule 2
60
+ `:65`, rule 4 `:68`, §5.1(4) `:113-116`).
61
+ - `DecisionProjection` (not breaking for conforming callers): `phase` no
62
+ longer regresses out of `'Committed'` if a `Vote` is replayed after a
63
+ `Commitment` — such a message cannot legally exist in accepted history
64
+ under a conforming runtime (RFC-MACP-0001 §7.2 `:218`, §7.3
65
+ `:240`/`:249`), but a caller violating the accepted-only contract no
66
+ longer corrupts `phase` if it happens.
67
+
68
+ `ProposalProjection.isAccepted()`/`acceptedProposal()`, `TaskProjection`'s
69
+ `assignee` field, and `HandoffProjection`'s `status`/`phase` can now
70
+ derive different values than before for a transcript containing the
71
+ message sequences described above — existing callers reading those
72
+ surfaces should treat this as a breaking change. Conforming transcripts
73
+ (no superseded accept, no second `TaskAccept`, no contradictory or
74
+ unknown-`handoff_id` handoff accept/decline) are unaffected.
75
+
76
+ Three other sites raised by #59 (Decision `Evaluation`/`Objection`
77
+ cardinality, Task `TaskUpdate` accumulation) were confirmed to be silent
78
+ *coherently* and are unchanged. `TaskComplete`/`TaskFail` cardinality is
79
+ genuinely ambiguous in RFC-MACP-0009 and is tracked as an upstream spec
80
+ question rather than an SDK change.
81
+
82
+ ### Changed
83
+
84
+ - **`GrpcTransportAdapter` resumes instead of replaying on a second `start()`.**
85
+ The adapter now calls `sendSubscribe(sessionId, this.lastSequence)` instead of
86
+ always subscribing from `0`, so a caller that `stop()`s and re-`start()`s a
87
+ reused adapter — the only reachable "reconnect" in this SDK; there is no
88
+ built-in retry loop — no longer replays the entire session history. Requires
89
+ `macp-runtime` ≥ 0.5.0 for the `after_sequence` ordinal contract
90
+ (RFC-MACP-0006 §3.2); older runtimes compared it inclusively against a raw
91
+ log index. `lastSequence` itself is corrected in the same change to count
92
+ **distinct** accepted envelopes (keyed on `message_id`) instead of raw
93
+ delivery events — passing the old, uncorrected counter as a resume cursor
94
+ would have silently skipped history on resume (RFC-MACP-0006 §3.2
95
+ Redelivery). Anyone relying on a reused adapter's second `start()` to rebuild
96
+ a *fresh* projection from full history will now see only the envelopes
97
+ accepted since the first pass ended; open a new `GrpcTransportAdapter` (or a
98
+ raw `MacpStream` with `sendSubscribe(sessionId, 0)`) for a full replay.
99
+ - `MacpStream.sendSubscribe`'s docblock no longer claims `IncomingMessage.seq`
100
+ is the RFC-MACP-0006 ordinal — `seq` is a 0-based delivery index that
101
+ advances on redelivery, unlike the ordinal.
102
+
7
103
  ## [0.8.0](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/compare/v0.7.1...v0.8.0) (2026-08-31)
8
104
 
9
105
 
package/README.md CHANGED
@@ -367,7 +367,7 @@ await stream.sendSubscribe(sessionId); // full replay then live
367
367
  await stream.sendSubscribe(sessionId, lastSeq); // resume from cursor
368
368
  ```
369
369
 
370
- The agent framework's `GrpcTransportAdapter` calls this for you on start, so non-initiator agents see `SessionStart` regardless of join order.
370
+ The agent framework's `GrpcTransportAdapter` calls this for you on start, so non-initiator agents see `SessionStart` regardless of join order — and it passes its own resume cursor on a second `start()` (after a `stop()`), so that call resumes rather than replaying the whole session again.
371
371
 
372
372
  ### Registry & Root Watchers
373
373
 
@@ -12,14 +12,39 @@ export declare class GrpcTransportAdapter implements TransportAdapter {
12
12
  private stream;
13
13
  private seq;
14
14
  private delivered;
15
+ /**
16
+ * `message_id`s already counted into `delivered`, so the resume cursor
17
+ * advances once per distinct accepted envelope rather than once per raw
18
+ * delivery (RFC-MACP-0006 §3.2 Redelivery). Deliberately unbounded, for the
19
+ * same reason `BaseProjection` leaves its own copy of this set unbounded
20
+ * (`src/projections/base.ts:118-129`): this set is strictly dominated by
21
+ * memory this SDK already commits to per session — a `Participant` using
22
+ * this adapter also holds a `BaseProjection` with an identical
23
+ * `message_id` set *plus* a full `transcript` of every envelope, payload
24
+ * bytes included. The runtime keeps an unbounded per-session dedup set of
25
+ * its own regardless of what any client does
26
+ * (`macp-runtime/crates/macp-modes/src/step.rs:48/:89`, field declared at
27
+ * `macp-runtime/crates/macp-core/src/session.rs:69`), and sessions are
28
+ * TTL-bounded by protocol.
29
+ */
30
+ private readonly seenMessageIds;
15
31
  constructor(client: MacpClient, sessionId: string, auth?: AuthConfig | undefined);
16
32
  /**
17
33
  * The server passive-subscribe ordinal of the last delivered envelope (the
18
- * 1-based count of accepted envelopes delivered on this stream, RFC-MACP-0006
19
- * §3.2). `0` before anything is delivered. Pass this as `afterSequence` to
20
- * `MacpStream.sendSubscribe` on a reconnect to resume exactly-once — ordinals
21
- * are stable across compaction/restart. Distinct from `IncomingMessage.seq`,
22
- * which is a client-local 0-based delivery index.
34
+ * 1-based count of *distinct* accepted envelopes delivered on this stream,
35
+ * RFC-MACP-0006 §3.2). `0` before anything is delivered. `start()` passes
36
+ * this value as `afterSequence` to `MacpStream.sendSubscribe` itself, so a
37
+ * caller that `stop()`s and calls `start()` again (the only reachable
38
+ * "reconnect" in this SDK — there is no built-in retry loop) resumes
39
+ * rather than replays the whole session. Ordinals are stable across
40
+ * compaction/restart. Counts distinct `message_id`s, never raw delivery
41
+ * events: RFC-MACP-0006 §3.2 Redelivery requires that "a redelivery MUST
42
+ * NOT advance the client's sequence position; only a distinct accepted
43
+ * envelope does," and a client that counts raw deliveries "arrives at a
44
+ * position ahead of the true one, and its next resume silently skips
45
+ * history." Distinct from `IncomingMessage.seq`, which is a client-local
46
+ * 0-based delivery index that advances on every delivery, redeliveries
47
+ * included.
23
48
  */
24
49
  get lastSequence(): number;
25
50
  start(): AsyncIterable<IncomingMessage>;
@@ -19,6 +19,22 @@ class GrpcTransportAdapter {
19
19
  stream = null;
20
20
  seq = 0;
21
21
  delivered = 0;
22
+ /**
23
+ * `message_id`s already counted into `delivered`, so the resume cursor
24
+ * advances once per distinct accepted envelope rather than once per raw
25
+ * delivery (RFC-MACP-0006 §3.2 Redelivery). Deliberately unbounded, for the
26
+ * same reason `BaseProjection` leaves its own copy of this set unbounded
27
+ * (`src/projections/base.ts:118-129`): this set is strictly dominated by
28
+ * memory this SDK already commits to per session — a `Participant` using
29
+ * this adapter also holds a `BaseProjection` with an identical
30
+ * `message_id` set *plus* a full `transcript` of every envelope, payload
31
+ * bytes included. The runtime keeps an unbounded per-session dedup set of
32
+ * its own regardless of what any client does
33
+ * (`macp-runtime/crates/macp-modes/src/step.rs:48/:89`, field declared at
34
+ * `macp-runtime/crates/macp-core/src/session.rs:69`), and sessions are
35
+ * TTL-bounded by protocol.
36
+ */
37
+ seenMessageIds = new Set();
22
38
  constructor(client, sessionId, auth) {
23
39
  this.client = client;
24
40
  this.sessionId = sessionId;
@@ -26,27 +42,92 @@ class GrpcTransportAdapter {
26
42
  }
27
43
  /**
28
44
  * The server passive-subscribe ordinal of the last delivered envelope (the
29
- * 1-based count of accepted envelopes delivered on this stream, RFC-MACP-0006
30
- * §3.2). `0` before anything is delivered. Pass this as `afterSequence` to
31
- * `MacpStream.sendSubscribe` on a reconnect to resume exactly-once — ordinals
32
- * are stable across compaction/restart. Distinct from `IncomingMessage.seq`,
33
- * which is a client-local 0-based delivery index.
45
+ * 1-based count of *distinct* accepted envelopes delivered on this stream,
46
+ * RFC-MACP-0006 §3.2). `0` before anything is delivered. `start()` passes
47
+ * this value as `afterSequence` to `MacpStream.sendSubscribe` itself, so a
48
+ * caller that `stop()`s and calls `start()` again (the only reachable
49
+ * "reconnect" in this SDK — there is no built-in retry loop) resumes
50
+ * rather than replays the whole session. Ordinals are stable across
51
+ * compaction/restart. Counts distinct `message_id`s, never raw delivery
52
+ * events: RFC-MACP-0006 §3.2 Redelivery requires that "a redelivery MUST
53
+ * NOT advance the client's sequence position; only a distinct accepted
54
+ * envelope does," and a client that counts raw deliveries "arrives at a
55
+ * position ahead of the true one, and its next resume silently skips
56
+ * history." Distinct from `IncomingMessage.seq`, which is a client-local
57
+ * 0-based delivery index that advances on every delivery, redeliveries
58
+ * included.
34
59
  */
35
60
  get lastSequence() {
36
61
  return this.delivered;
37
62
  }
38
63
  async *start() {
64
+ // A prior stream from an earlier start() (without an intervening stop())
65
+ // must not keep feeding this adapter's counter — two live generators
66
+ // would both advance `delivered` from two differently-positioned
67
+ // replays. `MacpStream.close()` is `this.call.end()` (`src/client.ts:223-227`),
68
+ // a write-side half-close — it does not itself force the server to stop
69
+ // pushing. It is enough here because macp-runtime's StreamSession loop
70
+ // reads the resulting end-of-request-stream as `StreamAction::ClientDone`,
71
+ // whose arm drains any already-buffered envelopes once and then breaks,
72
+ // ending that stream's response side too
73
+ // (macp-runtime/src/server.rs:654-666). Close it before opening the new
74
+ // one so re-entrant start() and stop()-then-start() behave identically.
75
+ if (this.stream) {
76
+ this.stream.close();
77
+ this.stream = null;
78
+ }
39
79
  this.stream = this.client.openStream({ auth: this.auth });
40
- // RFC-MACP-0006-A1: Subscribe to the session with history replay.
41
- // The runtime replays accepted envelopes then switches to live broadcast.
42
- // This ensures non-initiator agents receive the SessionStart + Proposal
43
- // regardless of spawn order or connection timing.
44
- await this.stream.sendSubscribe(this.sessionId);
80
+ // RFC-MACP-0006 §3.2: subscribe to the session, resuming from this
81
+ // adapter's own cursor. `this.delivered` is 0 before anything has been
82
+ // delivered, by construction, and `afterSequence = 0` is normatively
83
+ // "replay from the session's first accepted envelope" — so the first
84
+ // subscribe and every later one (e.g. after stop()+start()) are the same
85
+ // expression. There is no separate first-subscribe/reconnect branch to
86
+ // keep in sync with the counter.
87
+ await this.stream.sendSubscribe(this.sessionId, this.delivered);
45
88
  for await (const envelope of this.stream.responses()) {
46
89
  if (envelope.sessionId !== this.sessionId)
47
90
  continue;
48
- this.delivered++;
49
- yield normalizeEnvelope(envelope, (mode, mt, p) => this.client.protoRegistry.decodeKnownPayload(mode, mt, p), this.seq++);
91
+ // Count this envelope into the resume cursor only *after* the
92
+ // consumer has come back for more, not when it is handed off. `yield`
93
+ // suspends here until the consumer's `for await` either calls
94
+ // `.next()` again (it took this envelope and kept iterating — the
95
+ // code below runs) or closes the iterator via `break`/`return`/throw
96
+ // (e.g. `Participant.run()` observing `!this.running` and breaking
97
+ // out of its loop without processing this envelope). In the latter
98
+ // case `.return()` on this generator resumes at this `yield` as if a
99
+ // `return` had been written here, so nothing below it runs: the
100
+ // cursor is never advanced past an envelope the consumer never
101
+ // actually took past the loop boundary, and the next `start()`
102
+ // redelivers it instead of silently skipping it. See #66 — counting
103
+ // before yielding let a stop-mid-stream permanently drop an envelope
104
+ // once #65 made the resume cursor authoritative.
105
+ const message = normalizeEnvelope(envelope, (mode, mt, p) => this.client.protoRegistry.decodeKnownPayload(mode, mt, p), this.seq++);
106
+ yield message;
107
+ // RFC-MACP-0006 §3.2 Redelivery: a redelivery MUST NOT advance the
108
+ // resume cursor, and a consumer that accumulates state per envelope
109
+ // MUST be idempotent w.r.t. `message_id`. An empty/absent messageId
110
+ // has no identity to dedup on and increments unconditionally — the
111
+ // same carve-out the projection guard documents at
112
+ // `src/projections/base.ts:224-227`, so the two dedup sites read the
113
+ // same way. In practice the `else` below can never run against a
114
+ // conformant runtime: `validate_envelope_shape` rejects any envelope
115
+ // with an empty `message_id` as `InvalidEnvelope` before it can be
116
+ // accepted (`macp-runtime/src/server.rs:118`), and RFC-MACP-0001 §8.2
117
+ // makes `message_id` the runtime's dedup identity, so no accepted
118
+ // envelope in any session history can carry one. The branch stays
119
+ // anyway — it costs nothing and errs toward still counting the
120
+ // envelope instead of silently dropping it if a non-conformant server
121
+ // ever sends one.
122
+ if (envelope.messageId) {
123
+ if (!this.seenMessageIds.has(envelope.messageId)) {
124
+ this.seenMessageIds.add(envelope.messageId);
125
+ this.delivered++;
126
+ }
127
+ }
128
+ else {
129
+ this.delivered++;
130
+ }
50
131
  }
51
132
  }
52
133
  async stop() {
package/dist/client.d.ts CHANGED
@@ -48,9 +48,13 @@ export declare class MacpStream {
48
48
  *
49
49
  * `afterSequence` is the **1-based accepted-envelope ordinal**, exclusive:
50
50
  * `0` (default) replays from the very start; `k` replays envelopes with
51
- * ordinal `> k`. Clients derive the ordinal by counting delivered envelopes
52
- * (the Nth accepted envelope has ordinal N) — see
53
- * `IncomingMessage.seq`, which is exactly this ordinal under the new contract.
51
+ * ordinal `> k`. Clients derive the ordinal by counting **distinct**
52
+ * accepted envelopes delivered, keyed on `message_id` — RFC-MACP-0006 §3.2
53
+ * Redelivery requires that a repeat of an already-observed envelope MUST
54
+ * NOT advance the count. This is **not** the same as `IncomingMessage.seq`,
55
+ * which is a client-local 0-based delivery index that advances on every
56
+ * delivery, redeliveries included; see `GrpcTransportAdapter.lastSequence`
57
+ * for a cursor that tracks the ordinal correctly.
54
58
  * Ordinals are stable across log compaction and runtime restart. Resuming
55
59
  * below a compacted base returns `FAILED_PRECONDITION` (inspectable via
56
60
  * `MacpTransportError.code`) rather than silently skipping history (runtime
@@ -258,6 +262,15 @@ export declare class MacpClient {
258
262
  auth?: AuthConfig;
259
263
  deadlineMs?: number;
260
264
  }): Promise<Ack>;
265
+ /**
266
+ * Send a `Progress` message.
267
+ *
268
+ * Per RFC-MACP-0001 §6, `Progress` is legal in exactly two shapes: *ambient*
269
+ * (`sessionId` and `mode` both omitted/empty) or *session-scoped* (both
270
+ * non-empty). Supplying exactly one of the two throws `MacpSessionError` —
271
+ * the runtime rejects that mixed envelope with `INVALID_ENVELOPE`, so this
272
+ * fails fast with a message naming the mismatched field.
273
+ */
261
274
  sendProgress(options: {
262
275
  sessionId?: string;
263
276
  mode?: string;
package/dist/client.js CHANGED
@@ -160,9 +160,13 @@ class MacpStream {
160
160
  *
161
161
  * `afterSequence` is the **1-based accepted-envelope ordinal**, exclusive:
162
162
  * `0` (default) replays from the very start; `k` replays envelopes with
163
- * ordinal `> k`. Clients derive the ordinal by counting delivered envelopes
164
- * (the Nth accepted envelope has ordinal N) — see
165
- * `IncomingMessage.seq`, which is exactly this ordinal under the new contract.
163
+ * ordinal `> k`. Clients derive the ordinal by counting **distinct**
164
+ * accepted envelopes delivered, keyed on `message_id` — RFC-MACP-0006 §3.2
165
+ * Redelivery requires that a repeat of an already-observed envelope MUST
166
+ * NOT advance the count. This is **not** the same as `IncomingMessage.seq`,
167
+ * which is a client-local 0-based delivery index that advances on every
168
+ * delivery, redeliveries included; see `GrpcTransportAdapter.lastSequence`
169
+ * for a cursor that tracks the ordinal correctly.
166
170
  * Ordinals are stable across log compaction and runtime restart. Resuming
167
171
  * below a compacted base returns `FAILED_PRECONDITION` (inspectable via
168
172
  * `MacpTransportError.code`) rather than silently skipping history (runtime
@@ -541,7 +545,17 @@ class MacpClient {
541
545
  });
542
546
  return this.send(envelope, { auth, deadlineMs: options.deadlineMs });
543
547
  }
548
+ /**
549
+ * Send a `Progress` message.
550
+ *
551
+ * Per RFC-MACP-0001 §6, `Progress` is legal in exactly two shapes: *ambient*
552
+ * (`sessionId` and `mode` both omitted/empty) or *session-scoped* (both
553
+ * non-empty). Supplying exactly one of the two throws `MacpSessionError` —
554
+ * the runtime rejects that mixed envelope with `INVALID_ENVELOPE`, so this
555
+ * fails fast with a message naming the mismatched field.
556
+ */
544
557
  async sendProgress(options) {
558
+ (0, validation_1.validateProgressScope)(options.sessionId ?? '', options.mode ?? '');
545
559
  const auth = this.requireAuth(options.auth);
546
560
  (0, auth_1.assertSenderMatchesIdentity)(auth, options.sender);
547
561
  const progressPayload = (0, envelope_1.buildProgressPayload)({
@@ -109,7 +109,23 @@ class DecisionProjection {
109
109
  }
110
110
  bySender.set(envelope.sender, { ...record, sender: envelope.sender });
111
111
  this.votes.set(record.proposalId, bySender);
112
- this.phase = 'Voting';
112
+ // RFC-MACP-0001 §7.2 (`:218`): RESOLVED is terminal and sessions
113
+ // MUST transition monotonically w.r.t. termination — never back to
114
+ // OPEN/SUSPENDED. §7.3 (`:240`, restated `:249`) says a conforming
115
+ // runtime rejects any session-scoped message once the session is
116
+ // non-OPEN, so a `Vote` cannot legally follow a `Commitment` in
117
+ // accepted history. If one reaches here anyway (the caller violated
118
+ // the accepted-only contract — see `applyEnvelope`'s docblock), do
119
+ // not regress `phase` out of `'Committed'`. Note "phase" itself is
120
+ // not a normative MACP term (it appears once, in passing, at
121
+ // RFC-MACP-0012 `:211`); this guard is justified by session
122
+ // terminality, not by a phase specification. An anomaly would be
123
+ // recorded here too, but `ProjectionAnomalyKind` (`base.ts:8-9`) is
124
+ // deliberately frozen pending cross-SDK agreement with
125
+ // macp-sdk-python.
126
+ if (this.phase !== 'Committed') {
127
+ this.phase = 'Voting';
128
+ }
113
129
  break;
114
130
  }
115
131
  case 'Commitment': {
@@ -91,25 +91,61 @@ class HandoffProjection {
91
91
  case 'HandoffAccept': {
92
92
  const record = payload;
93
93
  const handoff = this.handoffs.get(record.handoffId);
94
- if (handoff) {
94
+ if (!handoff) {
95
+ // RFC-MACP-0010 §5 rule 2 (`:65`): "HandoffContext, HandoffAccept,
96
+ // and HandoffDecline MUST reference an existing handoff_id." An
97
+ // accept for an unknown/never-offered handoff_id is invalid input
98
+ // (e.g. an unfiltered transcript) and MUST NOT mutate `phase` or
99
+ // fabricate a handoff record. An anomaly would be recorded here,
100
+ // but `ProjectionAnomalyKind` (`base.ts:8-9`) is deliberately
101
+ // frozen pending cross-SDK agreement with macp-sdk-python.
102
+ break;
103
+ }
104
+ // RFC-MACP-0010 §5 rule 4 (`:68`): "Once an offer has been
105
+ // accepted, no competing accept for that same `handoff_id` is
106
+ // valid." §5.1(4) (`:113-116`) settles the decline-after-accept
107
+ // direction too: a `handoff_id` transitions
108
+ // offered -> accepted | declined exactly once. Only settle if this
109
+ // handoff hasn't already resolved — the same shape as the
110
+ // HandoffContext guard just above. An anomaly would be recorded when
111
+ // this guard discards a competing accept, but `ProjectionAnomalyKind`
112
+ // (`base.ts:8-9`) is deliberately frozen pending cross-SDK agreement
113
+ // with macp-sdk-python.
114
+ if (handoff.status === 'offered' || handoff.status === 'context_sent') {
95
115
  handoff.status = 'accepted';
96
116
  handoff.acceptedBy = record.acceptedBy;
97
117
  // proto3 bool defaults are materialized to `false` on decode
98
118
  // (proto-registry), so this is always a real boolean once proto 0.1.6
99
119
  // is loaded — `true` marks a runtime synthetic implicit accept.
100
120
  handoff.implicit = record.implicit ?? false;
121
+ this.phase = 'Accepted';
101
122
  }
102
- this.phase = 'Accepted';
103
123
  break;
104
124
  }
105
125
  case 'HandoffDecline': {
106
126
  const record = payload;
107
127
  const handoff = this.handoffs.get(record.handoffId);
108
- if (handoff) {
128
+ if (!handoff) {
129
+ // RFC-MACP-0010 §5 rule 2 (`:65`): "HandoffContext, HandoffAccept,
130
+ // and HandoffDecline MUST reference an existing handoff_id." A
131
+ // decline for an unknown/never-offered handoff_id is invalid input
132
+ // (e.g. an unfiltered transcript) and MUST NOT mutate `phase` or
133
+ // fabricate a handoff record. An anomaly would be recorded here,
134
+ // but `ProjectionAnomalyKind` (`base.ts:8-9`) is deliberately
135
+ // frozen pending cross-SDK agreement with macp-sdk-python.
136
+ break;
137
+ }
138
+ // RFC-MACP-0010 §5 rule 4 (`:68`) + §5.1(4) (`:113-116`): a
139
+ // `handoff_id` settles once. A decline after the handoff already
140
+ // settled (accepted or declined) is invalid and ignored. An anomaly
141
+ // would be recorded here too, but `ProjectionAnomalyKind`
142
+ // (`base.ts:8-9`) is deliberately frozen pending cross-SDK agreement
143
+ // with macp-sdk-python.
144
+ if (handoff.status === 'offered' || handoff.status === 'context_sent') {
109
145
  handoff.status = 'declined';
110
146
  handoff.declinedBy = record.declinedBy;
147
+ this.phase = 'Declined';
111
148
  }
112
- this.phase = 'Declined';
113
149
  break;
114
150
  }
115
151
  case 'Commitment': {
@@ -25,6 +25,20 @@ export declare class ProposalProjection {
25
25
  readonly proposals: Map<string, ProposalRecord>;
26
26
  readonly accepts: ProposalAcceptRecord[];
27
27
  readonly rejections: ProposalRejectRecord[];
28
+ /**
29
+ * Each sender's current (unsuperseded) `Accept`, keyed by sender.
30
+ * RFC-MACP-0008 §5 rule 5 (`:70`): "A participant MAY change its
31
+ * acceptance target by sending a later `Accept` for a different live
32
+ * proposal. The latest accepted `Accept` from a participant supersedes
33
+ * earlier accepts from the same participant." `accepts` above retains the
34
+ * full append-only history (including superseded accepts) for audit
35
+ * purposes; `isAccepted` and `acceptedProposal` read from this map instead
36
+ * so they reflect the live acceptance set — required for determinism by
37
+ * §7 (`:89`): "Given the same accepted history and the same version-bound
38
+ * rules, implementations MUST derive the same live proposal set, the same
39
+ * acceptance set, and the same commitment eligibility."
40
+ */
41
+ private readonly latestAcceptBySender;
28
42
  /**
29
43
  * The session's accepted history, one envelope per unique `message_id`. See
30
44
  * `BaseProjection.transcript` (`src/projections/base.ts`) for the full
@@ -74,9 +88,25 @@ export declare class ProposalProjection {
74
88
  get isPositiveOutcome(): boolean | undefined;
75
89
  activeProposals(): ProposalRecord[];
76
90
  latestProposal(): ProposalRecord | undefined;
91
+ /**
92
+ * True if some participant's *current* (unsuperseded) `Accept` targets
93
+ * `proposalId` — RFC-MACP-0008 §5 rule 5 (`:70`). Note this reads from the
94
+ * live acceptance set, not from `accepts`' full history: a participant
95
+ * whose only accept for `proposalId` was later superseded by a re-accept
96
+ * of a different proposal is not counted.
97
+ */
77
98
  isAccepted(proposalId: string): boolean;
78
99
  isTerminallyRejected(proposalId: string): boolean;
79
100
  liveProposals(): Map<string, ProposalRecord>;
101
+ /**
102
+ * The single proposal every participant's *current* accept targets, or
103
+ * `undefined` if no one has an outstanding accept or outstanding accepts
104
+ * are split across more than one proposal. Computed from the live
105
+ * acceptance set (see `isAccepted`), not from `accepts`' full history — a
106
+ * participant who accepts `p1` and later re-accepts `p2` (RFC-MACP-0008 §5
107
+ * rule 5, `:70`) contributes only `p2`, so this correctly returns `p2`
108
+ * rather than `undefined`.
109
+ */
80
110
  acceptedProposal(): string | undefined;
81
111
  hasTerminalRejection(): boolean;
82
112
  }
@@ -7,6 +7,20 @@ class ProposalProjection {
7
7
  proposals = new Map();
8
8
  accepts = [];
9
9
  rejections = [];
10
+ /**
11
+ * Each sender's current (unsuperseded) `Accept`, keyed by sender.
12
+ * RFC-MACP-0008 §5 rule 5 (`:70`): "A participant MAY change its
13
+ * acceptance target by sending a later `Accept` for a different live
14
+ * proposal. The latest accepted `Accept` from a participant supersedes
15
+ * earlier accepts from the same participant." `accepts` above retains the
16
+ * full append-only history (including superseded accepts) for audit
17
+ * purposes; `isAccepted` and `acceptedProposal` read from this map instead
18
+ * so they reflect the live acceptance set — required for determinism by
19
+ * §7 (`:89`): "Given the same accepted history and the same version-bound
20
+ * rules, implementations MUST derive the same live proposal set, the same
21
+ * acceptance set, and the same commitment eligibility."
22
+ */
23
+ latestAcceptBySender = new Map();
10
24
  /**
11
25
  * The session's accepted history, one envelope per unique `message_id`. See
12
26
  * `BaseProjection.transcript` (`src/projections/base.ts`) for the full
@@ -88,7 +102,11 @@ class ProposalProjection {
88
102
  }
89
103
  case 'Accept': {
90
104
  const record = payload;
91
- this.accepts.push({ ...record, sender: envelope.sender });
105
+ const accept = { ...record, sender: envelope.sender };
106
+ this.accepts.push(accept);
107
+ // RFC-MACP-0008 §5 rule 5 (`:70`): this Accept supersedes any earlier
108
+ // one from the same sender for the live acceptance set.
109
+ this.latestAcceptBySender.set(envelope.sender, accept);
92
110
  break;
93
111
  }
94
112
  case 'Reject': {
@@ -149,8 +167,19 @@ class ProposalProjection {
149
167
  const all = [...this.proposals.values()];
150
168
  return all[all.length - 1];
151
169
  }
170
+ /**
171
+ * True if some participant's *current* (unsuperseded) `Accept` targets
172
+ * `proposalId` — RFC-MACP-0008 §5 rule 5 (`:70`). Note this reads from the
173
+ * live acceptance set, not from `accepts`' full history: a participant
174
+ * whose only accept for `proposalId` was later superseded by a re-accept
175
+ * of a different proposal is not counted.
176
+ */
152
177
  isAccepted(proposalId) {
153
- return this.accepts.some((a) => a.proposalId === proposalId);
178
+ for (const accept of this.latestAcceptBySender.values()) {
179
+ if (accept.proposalId === proposalId)
180
+ return true;
181
+ }
182
+ return false;
154
183
  }
155
184
  isTerminallyRejected(proposalId) {
156
185
  return this.rejections.some((r) => r.proposalId === proposalId && r.terminal);
@@ -163,10 +192,19 @@ class ProposalProjection {
163
192
  }
164
193
  return result;
165
194
  }
195
+ /**
196
+ * The single proposal every participant's *current* accept targets, or
197
+ * `undefined` if no one has an outstanding accept or outstanding accepts
198
+ * are split across more than one proposal. Computed from the live
199
+ * acceptance set (see `isAccepted`), not from `accepts`' full history — a
200
+ * participant who accepts `p1` and later re-accepts `p2` (RFC-MACP-0008 §5
201
+ * rule 5, `:70`) contributes only `p2`, so this correctly returns `p2`
202
+ * rather than `undefined`.
203
+ */
166
204
  acceptedProposal() {
167
- if (this.accepts.length === 0)
205
+ if (this.latestAcceptBySender.size === 0)
168
206
  return undefined;
169
- const ids = new Set(this.accepts.map((a) => a.proposalId));
207
+ const ids = new Set([...this.latestAcceptBySender.values()].map((a) => a.proposalId));
170
208
  if (ids.size === 1)
171
209
  return ids.values().next().value;
172
210
  return undefined;
@@ -61,6 +61,26 @@ export declare class TaskProjection {
61
61
  * only as a one-line pointer.
62
62
  */
63
63
  private readonly seenMessageIds;
64
+ /**
65
+ * The **session-level** active assignment: who currently holds the session's
66
+ * single assignee slot, and which `task_id` they took it on.
67
+ *
68
+ * RFC-MACP-0009 §5 rule 3 (`:69`): "Only one assignee may become active for
69
+ * the Session in base v1." The constraint is scoped to the *Session*, not to
70
+ * a `task_id` — rule 1 (`:67`) separately caps a v1 session at one
71
+ * `TaskRequest`, so in a conforming transcript the two scopes coincide, but
72
+ * an unfiltered transcript carrying two `TaskRequest`s must not be able to
73
+ * hand out two active assignees. That is why this is one field on the
74
+ * projection rather than a per-`TaskRecord` flag (issue #71).
75
+ *
76
+ * `sender` is the envelope sender, not `TaskAcceptPayload.assignee`, to
77
+ * mirror the reference runtime: `macp-runtime`
78
+ * `crates/macp-modes/src/mode/task.rs:211` sets `state.active_assignee =
79
+ * Some(env.sender)` after rejecting any payload whose `assignee` disagrees
80
+ * with the sender (`:205-207`). `taskId` is retained so a reject can clear
81
+ * the right `TaskRecord.assignee` even on a multi-task transcript.
82
+ */
83
+ private activeAssignment?;
64
84
  /**
65
85
  * Apply one envelope to this projection's in-process state.
66
86
  *
@@ -31,6 +31,26 @@ class TaskProjection {
31
31
  * only as a one-line pointer.
32
32
  */
33
33
  seenMessageIds = new Set();
34
+ /**
35
+ * The **session-level** active assignment: who currently holds the session's
36
+ * single assignee slot, and which `task_id` they took it on.
37
+ *
38
+ * RFC-MACP-0009 §5 rule 3 (`:69`): "Only one assignee may become active for
39
+ * the Session in base v1." The constraint is scoped to the *Session*, not to
40
+ * a `task_id` — rule 1 (`:67`) separately caps a v1 session at one
41
+ * `TaskRequest`, so in a conforming transcript the two scopes coincide, but
42
+ * an unfiltered transcript carrying two `TaskRequest`s must not be able to
43
+ * hand out two active assignees. That is why this is one field on the
44
+ * projection rather than a per-`TaskRecord` flag (issue #71).
45
+ *
46
+ * `sender` is the envelope sender, not `TaskAcceptPayload.assignee`, to
47
+ * mirror the reference runtime: `macp-runtime`
48
+ * `crates/macp-modes/src/mode/task.rs:211` sets `state.active_assignee =
49
+ * Some(env.sender)` after rejecting any payload whose `assignee` disagrees
50
+ * with the sender (`:205-207`). `taskId` is retained so a reject can clear
51
+ * the right `TaskRecord.assignee` even on a multi-task transcript.
52
+ */
53
+ activeAssignment;
34
54
  /**
35
55
  * Apply one envelope to this projection's in-process state.
36
56
  *
@@ -81,11 +101,38 @@ class TaskProjection {
81
101
  case 'TaskAccept': {
82
102
  const record = payload;
83
103
  const task = this.tasks.get(record.taskId);
84
- if (task) {
104
+ // RFC-MACP-0009 §5 rules 3/3a (`:69-70`): "Only one assignee may
105
+ // become active for the Session in base v1. The first accepted
106
+ // `TaskAccept` from any eligible participant designates that
107
+ // participant as the active assignee. Subsequent `TaskAccept`
108
+ // messages for the same session MUST be rejected if an active
109
+ // assignee is already designated." A conforming runtime rejects the
110
+ // second `TaskAccept` before it reaches accepted history
111
+ // (`macp-runtime` `crates/macp-modes/src/mode/task.rs:184-186`); this
112
+ // guard makes the projection first-accept-wins too, so a rogue second
113
+ // one (e.g. an unfiltered transcript) cannot silently reassign.
114
+ //
115
+ // The guard is keyed on the SESSION slot (`activeAssignment`), not on
116
+ // `task.assignee`, because rule 3's scope is the Session (issue #71).
117
+ // Two `TaskRequest`s in one transcript therefore share one assignee
118
+ // slot, matching the runtime, whose `TaskState` holds a single
119
+ // `active_assignee` (`task.rs:70`) alongside a single `task`.
120
+ //
121
+ // Rule 3c's policy-gated reassignment (`allow_reassignment_on_reject`,
122
+ // RFC-MACP-0012 `:135`) is reachable here via the `TaskReject` case
123
+ // below, which frees the slot — see its comment for why that needs no
124
+ // policy input.
125
+ //
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) {
85
131
  task.assignee = record.assignee;
86
132
  task.status = 'accepted';
133
+ this.activeAssignment = { sender: envelope.sender, taskId: record.taskId };
134
+ this.phase = 'InProgress';
87
135
  }
88
- this.phase = 'InProgress';
89
136
  break;
90
137
  }
91
138
  case 'TaskReject': {
@@ -93,6 +140,33 @@ class TaskProjection {
93
140
  const task = this.tasks.get(record.taskId);
94
141
  if (task)
95
142
  task.status = 'rejected';
143
+ // RFC-MACP-0009 §5 rule 3c (`:72`): "When policy sets
144
+ // `allow_reassignment_on_reject: true` and the active assignee sends
145
+ // `TaskReject`, the session returns to the pre-assignment state. Other
146
+ // eligible participants MAY then send `TaskAccept` for the same
147
+ // `task_id`." Mirrors `macp-runtime`
148
+ // `crates/macp-modes/src/mode/task.rs:257-260`, which clears
149
+ // `active_assignee` only when the ACTIVE assignee is the rejecter —
150
+ // a reject from anyone else never frees the slot (`task.rs:241-244`
151
+ // rejects it as `InvalidPayload`).
152
+ //
153
+ // Deliberately NOT gated on the session policy, and it does not need
154
+ // to be. The runtime gates the reassignment twice — it denies the
155
+ // active assignee's `TaskReject` (`task.rs:223-239`) and the follow-up
156
+ // `TaskAccept` (`task.rs:187-204`) with `PolicyDenied` when
157
+ // `allow_reassignment_on_reject` is false — and a projection is a view
158
+ // of history the runtime ALREADY ACCEPTED (see this method's
159
+ // accepted-only input contract). A reassignment the runtime denied
160
+ // never reaches the transcript, so tracking the reject unconditionally
161
+ // cannot diverge from the runtime on any real transcript, and it needs
162
+ // no new API surface (threading a `PolicyDefinition` into the
163
+ // constructor would). Issue #70, option 2.
164
+ if (this.activeAssignment?.sender === envelope.sender) {
165
+ const held = this.tasks.get(this.activeAssignment.taskId);
166
+ if (held)
167
+ held.assignee = undefined;
168
+ this.activeAssignment = undefined;
169
+ }
96
170
  break;
97
171
  }
98
172
  case 'TaskUpdate': {
@@ -5,6 +5,26 @@ export declare function validateConfidence(value: number): void;
5
5
  export declare function validateSeverity(value: string): string;
6
6
  export declare function validateParticipantCount(count: number): void;
7
7
  export declare function validateSignalType(signalType: string, data?: Buffer | Uint8Array): void;
8
+ /**
9
+ * Validate the `Progress` scope pairing (RFC-MACP-0001 §6).
10
+ *
11
+ * `Progress` is legal in exactly two shapes — ambient (`sessionId` and `mode`
12
+ * both empty) or session-scoped (both non-empty). An envelope with exactly one
13
+ * of the two empty is a mixed shape that the runtime rejects with
14
+ * `INVALID_ENVELOPE`; raising here names the mismatched field instead.
15
+ *
16
+ * Unlike Signals, `Progress` is *not* required to be ambient — this is a
17
+ * tri-state rule, so neither field may be inferred from the other.
18
+ *
19
+ * The emptiness test deliberately mirrors `macp-runtime`'s
20
+ * `validate_envelope_shape` **exactly**, including its asymmetry: `sessionId`
21
+ * is compared raw while `mode` is trimmed first. Mirroring rather than
22
+ * normalising is the only choice under which the SDK neither accepts a shape
23
+ * the runtime rejects nor rejects one it accepts. Concretely, a
24
+ * whitespace-only `mode` alongside an empty `sessionId` is ambient to the
25
+ * runtime, so it stays ambient here.
26
+ */
27
+ export declare function validateProgressScope(sessionId: string, mode: string): void;
8
28
  export declare function validateTtlMs(ttlMs: number): void;
9
29
  export declare function validateMaxSuspendMs(maxSuspendMs: number): void;
10
30
  export declare function validateParticipants(participants: string[]): void;
@@ -7,6 +7,7 @@ exports.validateConfidence = validateConfidence;
7
7
  exports.validateSeverity = validateSeverity;
8
8
  exports.validateParticipantCount = validateParticipantCount;
9
9
  exports.validateSignalType = validateSignalType;
10
+ exports.validateProgressScope = validateProgressScope;
10
11
  exports.validateTtlMs = validateTtlMs;
11
12
  exports.validateMaxSuspendMs = validateMaxSuspendMs;
12
13
  exports.validateParticipants = validateParticipants;
@@ -61,6 +62,37 @@ function validateSignalType(signalType, data) {
61
62
  throw new errors_1.MacpSessionError('signalType must be non-empty when data is present');
62
63
  }
63
64
  }
65
+ /**
66
+ * Validate the `Progress` scope pairing (RFC-MACP-0001 §6).
67
+ *
68
+ * `Progress` is legal in exactly two shapes — ambient (`sessionId` and `mode`
69
+ * both empty) or session-scoped (both non-empty). An envelope with exactly one
70
+ * of the two empty is a mixed shape that the runtime rejects with
71
+ * `INVALID_ENVELOPE`; raising here names the mismatched field instead.
72
+ *
73
+ * Unlike Signals, `Progress` is *not* required to be ambient — this is a
74
+ * tri-state rule, so neither field may be inferred from the other.
75
+ *
76
+ * The emptiness test deliberately mirrors `macp-runtime`'s
77
+ * `validate_envelope_shape` **exactly**, including its asymmetry: `sessionId`
78
+ * is compared raw while `mode` is trimmed first. Mirroring rather than
79
+ * normalising is the only choice under which the SDK neither accepts a shape
80
+ * the runtime rejects nor rejects one it accepts. Concretely, a
81
+ * whitespace-only `mode` alongside an empty `sessionId` is ambient to the
82
+ * runtime, so it stays ambient here.
83
+ */
84
+ function validateProgressScope(sessionId, mode) {
85
+ const sessionIdEmpty = sessionId === '';
86
+ const modeEmpty = mode.trim() === '';
87
+ if (sessionIdEmpty === modeEmpty)
88
+ return;
89
+ const detail = sessionIdEmpty
90
+ ? `mode is ${JSON.stringify(mode)} but sessionId is empty`
91
+ : `sessionId is ${JSON.stringify(sessionId)} but mode is empty`;
92
+ throw new errors_1.MacpSessionError(`Progress must be either ambient (sessionId and mode both empty) or session-scoped ` +
93
+ `(both non-empty), but ${detail} (RFC-MACP-0001 §6). ` +
94
+ 'Pass both fields for a session-scoped Progress, or neither for an ambient one.');
95
+ }
64
96
  const MAX_TTL_MS = 86_400_000; // 24 hours
65
97
  function validateTtlMs(ttlMs) {
66
98
  if (!Number.isFinite(ttlMs) || ttlMs < 1 || ttlMs > MAX_TTL_MS) {
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.8.0";
7
+ export declare const VERSION = "0.10.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.8.0'; // x-release-please-version
10
+ exports.VERSION = '0.10.0'; // x-release-please-version
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "macp-sdk-typescript",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "TypeScript SDK for the MACP Rust runtime",
5
5
  "license": "Apache-2.0",
6
6
  "type": "commonjs",