macp-sdk-typescript 0.4.1 → 0.5.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,83 @@ 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.5.0] - 2026-07-06
8
+
9
+ Absorbs `macp-runtime` 0.5.0 and `@multiagentcoordinationprotocol/proto` 0.1.6
10
+ (resolves to ≥ 0.1.6; content identical through 0.1.8). Plan:
11
+ `plans/absorb-runtime-v0.5.0.md`.
12
+
13
+ **Requires `macp-runtime` ≥ 0.5.0** for: protobuf `Contribute` encoding, the
14
+ empty-`policy_version` commitment echo, the external task orchestrator, and the
15
+ bearer-only dev-auth flow (the runtime no longer reads `x-macp-agent-id`, and
16
+ refuses to start with no auth unless `MACP_ALLOW_INSECURE=1`). Pin `0.4.x` to
17
+ talk to an older runtime.
18
+
19
+ ### Added
20
+
21
+ - **`SessionStartPayload.maxSuspendMs`** (proto ≥ 0.1.5) — per-session cap on
22
+ cumulative suspended time before a `SUSPENDED` session `EXPIRE`s (RFC-MACP-0001
23
+ §7.5). Threaded through `buildSessionStartPayload`, every mode session's
24
+ `start()`, the `Participant` initiator config, and the bootstrap runner
25
+ (`session_start.max_suspend_ms`). `0`/absent = runtime default (7 days);
26
+ negatives are rejected client-side.
27
+ - **`HandoffAcceptPayload.implicit` decode support** (proto ≥ 0.1.6) — surfaced
28
+ read-only on `HandoffRecord.implicit` and `HandoffProjection.isImplicitlyAccepted()`
29
+ for the runtime-emitted synthetic accept (RFC-MACP-0010 §5.1). Client-submitted
30
+ `implicit` is stripped before encoding so it can never reach the wire.
31
+ - **`ListSessions` pagination** (proto ≥ 0.1.6) — new
32
+ `MacpClient.listSessionsPage({ pageSize, pageToken })` returning
33
+ `{ sessions, nextPageToken }`; `listSessions()` now transparently auto-paginates
34
+ so it still returns the complete listing.
35
+ - **`ext.multi_round.v1` `Contribute` uses canonical protobuf** — encodes as
36
+ `ContributePayload`; decode tries legacy JSON (`{"value":"..."}`) first for
37
+ byte-identical replay of pre-proto histories.
38
+ - **`MacpTransportError.code`** — carries the gRPC status name
39
+ (`RESOURCE_EXHAUSTED`, `FAILED_PRECONDITION`, `UNAUTHENTICATED`, …) surfaced on
40
+ unary calls, `MacpStream`, and watch streams, so consumers can distinguish
41
+ watch-stream lag (reconnect) from auth failure (don't) and passive-subscribe
42
+ resume below a compacted base.
43
+ - **`GrpcTransportAdapter.lastSequence`** — the 1-based accepted-envelope ordinal
44
+ of the last delivered envelope, a resume cursor for `sendSubscribe`.
45
+
46
+ ### Changed
47
+
48
+ - **Dev auth is bearer-only.** `Auth.devAgent(agentId)` now sends
49
+ `Authorization: Bearer <agentId>` (was the `x-macp-agent-id` header). The
50
+ runtime's dev fallback authenticates any bearer value as its sender, so
51
+ behavior is transparent for existing callers — but the wire header changed.
52
+ Integration/runbook: drop `MACP_ALLOW_DEV_SENDER_HEADER=1`, keep the now-mandatory
53
+ `MACP_ALLOW_INSECURE=1`.
54
+ - **`MacpClient.watchSignals` requires auth** (runtime 0.5.0) — routed through
55
+ `requireAuth`, so a missing credential fails fast client-side instead of as a
56
+ stream `UNAUTHENTICATED`. Breaking for anyone watching signals without auth.
57
+ - **`buildQuorumPolicy` validates percentage thresholds.** A `percentage`
58
+ `threshold.value` must be an integer 0–100 (the approval bar =
59
+ `ceil(value/100 × participants)`); a fractional value like `0.75` (previously a
60
+ silent ~1% bar) now throws `MacpSessionError`. Bug-surfacing by design.
61
+ - **`registerExtMode` pre-validates descriptors** — throws if
62
+ `terminalMessageTypes` lacks `'Commitment'` (mirrors the runtime, which rejects
63
+ such descriptors).
64
+ - **Canonical conformance fixtures + harness.** Vendored fixtures byte-synced to
65
+ the spec canonical pack (fully-qualified `payload_type`, `expected_error_code`,
66
+ `schema.json`); the harness parses FQ names only, with a format guard rejecting
67
+ legacy shorthand.
68
+ - **`SessionStartPayload` bodies now emit `maxSuspendMs`** (`0` when unset).
69
+
70
+ ### Removed
71
+
72
+ - **`AuthConfig.agentId`** and the `x-macp-agent-id` metadata path — no supported
73
+ runtime reads the header. Use `Auth.devAgent` / `Auth.bearer`.
74
+ - **`SessionLifecycleWatcher.events()` / `nextEvent()`** — deprecated aliases;
75
+ use `changes()` / `nextChange()`.
76
+ - **`MacpClient._watchModeRegistry` / `_watchRoots` / `_watchSignals` /
77
+ `_watchPolicies`** — deprecated underscored aliases; use the un-prefixed
78
+ `watch*` methods.
79
+
80
+ ### Fixed
81
+
82
+ - `MacpClient.clientVersion` default was a stale `'0.3.0'`; now `'0.5.0'`.
83
+
7
84
  ## [0.4.0] - 2026-06-22
8
85
 
9
86
  Adopts `@multiagentcoordinationprotocol/proto` 0.1.3 — session
package/README.md CHANGED
@@ -297,7 +297,8 @@ See [`docs/api/cancel-callback.md`](docs/api/cancel-callback.md) and
297
297
  ## Authentication
298
298
 
299
299
  ```typescript
300
- // Development (uses x-macp-agent-id header)
300
+ // Development (bearer-only; sends Authorization: Bearer my-agent).
301
+ // Requires the runtime to run with MACP_ALLOW_INSECURE=1.
301
302
  const auth = Auth.devAgent('my-agent');
302
303
 
303
304
  // Production (Bearer token with authenticated identity — RFC-MACP-0004 §4).
@@ -417,7 +418,7 @@ Run the full SDK against a live MACP runtime:
417
418
  docker build -t macp-runtime ../macp-runtime/
418
419
  docker run -d --name macp-runtime-test -p 50051:50051 \
419
420
  -e MACP_BIND_ADDR=0.0.0.0:50051 -e MACP_ALLOW_INSECURE=1 \
420
- -e MACP_ALLOW_DEV_SENDER_HEADER=1 -e MACP_MEMORY_ONLY=1 macp-runtime
421
+ -e MACP_MEMORY_ONLY=1 macp-runtime
421
422
 
422
423
  # Run tests
423
424
  npm run test:integration
@@ -9,6 +9,8 @@ export interface InitiatorConfig {
9
9
  intent: string;
10
10
  participants: string[];
11
11
  ttlMs: number;
12
+ /** Per-session max-suspend cap (ms, proto ≥ 0.1.5). 0/absent = runtime default. */
13
+ maxSuspendMs?: number;
12
14
  contextId?: string;
13
15
  extensions?: Record<string, Buffer>;
14
16
  context?: Record<string, unknown>;
@@ -298,6 +298,7 @@ class Participant {
298
298
  intent: ss.intent,
299
299
  participants: ss.participants,
300
300
  ttlMs: ss.ttlMs,
301
+ maxSuspendMs: ss.maxSuspendMs,
301
302
  contextId: ss.contextId,
302
303
  extensions: ss.extensions,
303
304
  roots: ss.roots,
@@ -18,6 +18,7 @@ export interface BootstrapPayload {
18
18
  intent: string;
19
19
  participants: string[];
20
20
  ttl_ms: number;
21
+ max_suspend_ms?: number;
21
22
  mode_version?: string;
22
23
  configuration_version?: string;
23
24
  policy_version?: string;
@@ -77,6 +77,7 @@ function fromBootstrap(bootstrapPath) {
77
77
  intent: ss.intent,
78
78
  participants: ss.participants,
79
79
  ttlMs: ss.ttl_ms,
80
+ maxSuspendMs: ss.max_suspend_ms,
80
81
  contextId: ss.context_id,
81
82
  extensions: encodeExtensions(ss.extensions),
82
83
  roots: ss.roots,
@@ -11,7 +11,17 @@ export declare class GrpcTransportAdapter implements TransportAdapter {
11
11
  private readonly auth?;
12
12
  private stream;
13
13
  private seq;
14
+ private delivered;
14
15
  constructor(client: MacpClient, sessionId: string, auth?: AuthConfig | undefined);
16
+ /**
17
+ * 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.
23
+ */
24
+ get lastSequence(): number;
15
25
  start(): AsyncIterable<IncomingMessage>;
16
26
  stop(): Promise<void>;
17
27
  }
@@ -18,11 +18,23 @@ class GrpcTransportAdapter {
18
18
  auth;
19
19
  stream = null;
20
20
  seq = 0;
21
+ delivered = 0;
21
22
  constructor(client, sessionId, auth) {
22
23
  this.client = client;
23
24
  this.sessionId = sessionId;
24
25
  this.auth = auth;
25
26
  }
27
+ /**
28
+ * 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.
34
+ */
35
+ get lastSequence() {
36
+ return this.delivered;
37
+ }
26
38
  async *start() {
27
39
  this.stream = this.client.openStream({ auth: this.auth });
28
40
  // RFC-MACP-0006-A1: Subscribe to the session with history replay.
@@ -33,6 +45,7 @@ class GrpcTransportAdapter {
33
45
  for await (const envelope of this.stream.responses()) {
34
46
  if (envelope.sessionId !== this.sessionId)
35
47
  continue;
48
+ this.delivered++;
36
49
  yield normalizeEnvelope(envelope, (mode, mt, p) => this.client.protoRegistry.decodeKnownPayload(mode, mt, p), this.seq++);
37
50
  }
38
51
  }
package/dist/auth.d.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import * as grpc from '@grpc/grpc-js';
2
2
  export interface AuthConfig {
3
3
  bearerToken?: string;
4
- agentId?: string;
5
4
  senderHint?: string;
6
5
  /**
7
6
  * The authenticated sender this credential represents. When present, the SDK
@@ -22,10 +21,16 @@ export type BearerAuthOptions = string | {
22
21
  };
23
22
  export declare const Auth: {
24
23
  /**
25
- * Dev-mode agent identity. Uses the `x-macp-agent-id` header — only accepted
26
- * by runtimes started with `MACP_ALLOW_DEV_SENDER_HEADER=1`. Not for
27
- * production. Does not set {@link AuthConfig.expectedSender}; dev flows stay
28
- * permissive so tests can reuse a single credential across senders.
24
+ * Dev-mode agent identity (bearer-only, runtime ≥ 0.5.0). Sends
25
+ * `Authorization: Bearer <agentId>`; the runtime's dev fallback authenticates
26
+ * any bearer value as the sender of that value, so `Auth.devAgent('alice')`
27
+ * authenticates as sender `alice`. Requires the runtime to be started with
28
+ * `MACP_ALLOW_INSECURE=1` (it refuses to start with no auth configured
29
+ * otherwise). Not for production — use {@link Auth.bearer}.
30
+ *
31
+ * The legacy `x-macp-agent-id` header was removed in 0.5.0: no supported
32
+ * runtime reads it. Does not set {@link AuthConfig.expectedSender}; dev flows
33
+ * stay permissive so tests can reuse a single credential across senders.
29
34
  */
30
35
  devAgent(agentId: string): AuthConfig;
31
36
  /**
package/dist/auth.js CHANGED
@@ -42,13 +42,19 @@ const grpc = __importStar(require("@grpc/grpc-js"));
42
42
  const errors_1 = require("./errors");
43
43
  exports.Auth = {
44
44
  /**
45
- * Dev-mode agent identity. Uses the `x-macp-agent-id` header — only accepted
46
- * by runtimes started with `MACP_ALLOW_DEV_SENDER_HEADER=1`. Not for
47
- * production. Does not set {@link AuthConfig.expectedSender}; dev flows stay
48
- * permissive so tests can reuse a single credential across senders.
45
+ * Dev-mode agent identity (bearer-only, runtime ≥ 0.5.0). Sends
46
+ * `Authorization: Bearer <agentId>`; the runtime's dev fallback authenticates
47
+ * any bearer value as the sender of that value, so `Auth.devAgent('alice')`
48
+ * authenticates as sender `alice`. Requires the runtime to be started with
49
+ * `MACP_ALLOW_INSECURE=1` (it refuses to start with no auth configured
50
+ * otherwise). Not for production — use {@link Auth.bearer}.
51
+ *
52
+ * The legacy `x-macp-agent-id` header was removed in 0.5.0: no supported
53
+ * runtime reads it. Does not set {@link AuthConfig.expectedSender}; dev flows
54
+ * stay permissive so tests can reuse a single credential across senders.
49
55
  */
50
56
  devAgent(agentId) {
51
- return { agentId, senderHint: agentId };
57
+ return { bearerToken: agentId, senderHint: agentId };
52
58
  },
53
59
  /**
54
60
  * Production bearer-token credential. Pass `{ expectedSender }` to have the
@@ -75,17 +81,14 @@ exports.Auth = {
75
81
  },
76
82
  };
77
83
  function validateAuth(auth) {
78
- if (!auth.bearerToken && !auth.agentId) {
79
- throw new Error('either bearerToken or agentId is required');
80
- }
81
- if (auth.bearerToken && auth.agentId) {
82
- throw new Error('choose either bearerToken or agentId, not both');
84
+ if (!auth.bearerToken) {
85
+ throw new Error('bearerToken is required');
83
86
  }
84
87
  }
85
88
  function authSender(auth) {
86
89
  if (!auth)
87
90
  return undefined;
88
- return auth.expectedSender ?? auth.senderHint ?? auth.agentId;
91
+ return auth.expectedSender ?? auth.senderHint;
89
92
  }
90
93
  /**
91
94
  * Throw {@link MacpIdentityMismatchError} when a caller-supplied `sender`
@@ -104,9 +107,6 @@ function assertSenderMatchesIdentity(auth, sender) {
104
107
  function metadataFromAuth(auth) {
105
108
  validateAuth(auth);
106
109
  const metadata = new grpc.Metadata();
107
- if (auth.bearerToken)
108
- metadata.set('authorization', `Bearer ${auth.bearerToken}`);
109
- if (auth.agentId)
110
- metadata.set('x-macp-agent-id', auth.agentId);
110
+ metadata.set('authorization', `Bearer ${auth.bearerToken}`);
111
111
  return metadata;
112
112
  }
@@ -40,6 +40,7 @@ export declare abstract class BaseSession<P extends BaseProjection> {
40
40
  intent: string;
41
41
  participants: string[];
42
42
  ttlMs: number;
43
+ maxSuspendMs?: number;
43
44
  contextId?: string;
44
45
  extensions?: Record<string, Buffer>;
45
46
  roots?: Root[];
@@ -58,10 +58,13 @@ class BaseSession {
58
58
  /** Send SessionStart and begin tracking via the projection. */
59
59
  async start(input) {
60
60
  (0, validation_1.validateParticipantCount)(input.participants.length);
61
+ if (input.maxSuspendMs !== undefined)
62
+ (0, validation_1.validateMaxSuspendMs)(input.maxSuspendMs);
61
63
  const payload = (0, envelope_1.buildSessionStartPayload)({
62
64
  intent: input.intent,
63
65
  participants: input.participants,
64
66
  ttlMs: input.ttlMs,
67
+ maxSuspendMs: input.maxSuspendMs,
65
68
  contextId: input.contextId,
66
69
  extensions: input.extensions,
67
70
  roots: input.roots,
package/dist/client.d.ts CHANGED
@@ -23,6 +23,12 @@ interface MacpClientOptions {
23
23
  clientVersion?: string;
24
24
  protoDir?: string;
25
25
  }
26
+ /**
27
+ * Map a gRPC `ServiceError.code` (numeric `grpc.status`) to its status name
28
+ * (e.g. `8` → `'RESOURCE_EXHAUSTED'`). Returns `undefined` for non-numeric or
29
+ * unknown codes. Used to populate {@link MacpTransportError.code}.
30
+ */
31
+ export declare function grpcStatusName(code: unknown): string | undefined;
26
32
  export type InlineErrorCallback = (error: {
27
33
  code?: string;
28
34
  message?: string;
@@ -36,9 +42,19 @@ export declare class MacpStream {
36
42
  onInlineError(callback: InlineErrorCallback): void;
37
43
  send(envelope: Envelope): Promise<void>;
38
44
  /**
39
- * RFC-MACP-0006-A1: Send a subscribe-only frame to receive session history
40
- * + live broadcast. The runtime replays accepted envelopes from
41
- * `afterSequence` onwards, then continues with live broadcast.
45
+ * RFC-MACP-0006 §3.2: Send a subscribe-only frame to receive session history
46
+ * + live broadcast. The runtime replays accepted envelopes strictly after
47
+ * `afterSequence`, then continues with live broadcast.
48
+ *
49
+ * `afterSequence` is the **1-based accepted-envelope ordinal**, exclusive:
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.
54
+ * Ordinals are stable across log compaction and runtime restart. Resuming
55
+ * below a compacted base returns `FAILED_PRECONDITION` (inspectable via
56
+ * `MacpTransportError.code`) rather than silently skipping history (runtime
57
+ * ≥ 0.5.0; older runtimes compared inclusively against a raw log index).
42
58
  */
43
59
  sendSubscribe(sessionId: string, afterSequence?: number): Promise<void>;
44
60
  responses(): AsyncGenerator<Envelope, void, void>;
@@ -135,6 +151,20 @@ export declare class MacpClient {
135
151
  listRoots(deadlineMs?: number): Promise<{
136
152
  roots: Root[];
137
153
  }>;
154
+ /**
155
+ * Register an extension-mode descriptor (runtime 0.5.0 guardrails,
156
+ * change-review A5):
157
+ * - the descriptor MUST declare `Commitment` among `terminalMessageTypes` —
158
+ * an ext mode without a terminal `Commitment` can never resolve, so the
159
+ * runtime rejects it. This method fails fast client-side to match.
160
+ * - a later {@link promoteMode} into the reserved `macp.mode.*` namespace is
161
+ * rejected by the runtime.
162
+ * - a SessionStart with an empty `mode_version` binds the registered
163
+ * descriptor's `modeVersion`; a subsequent Commitment must echo that bound
164
+ * version (echoing `""` no longer matches vacuously). `BaseSession`
165
+ * defaults `modeVersion` to `'1.0.0'`, so set the descriptor's version to
166
+ * match, or override the session's `modeVersion`.
167
+ */
138
168
  registerExtMode(descriptor: ModeDescriptor, options?: {
139
169
  auth?: AuthConfig;
140
170
  deadlineMs?: number;
@@ -179,30 +209,46 @@ export declare class MacpClient {
179
209
  auth?: AuthConfig;
180
210
  deadlineMs?: number;
181
211
  }): Promise<PolicyDescriptor[]>;
212
+ /**
213
+ * Fetch a single page of sessions (proto ≥ 0.1.6). `pageSize: 0` (or absent)
214
+ * lets the server choose the page size; the server MAY cap it. Callers MUST
215
+ * NOT assume the listing is complete unless `nextPageToken` is empty. Pass a
216
+ * returned non-empty `nextPageToken` back as `pageToken` to fetch the next
217
+ * page; a stale token yields `INVALID_ARGUMENT`.
218
+ */
219
+ listSessionsPage(options?: {
220
+ pageSize?: number;
221
+ pageToken?: string;
222
+ auth?: AuthConfig;
223
+ deadlineMs?: number;
224
+ }): Promise<{
225
+ sessions: SessionMetadata[];
226
+ nextPageToken: string;
227
+ }>;
228
+ /**
229
+ * Enumerate ALL sessions, transparently walking pages until the runtime
230
+ * returns an empty `nextPageToken`. Preserves the "complete list" semantics
231
+ * this method has always documented, even against runtimes that cap page
232
+ * sizes (proto ≥ 0.1.6). For manual page control, use {@link listSessionsPage}.
233
+ */
182
234
  listSessions(options?: {
183
235
  auth?: AuthConfig;
184
236
  deadlineMs?: number;
237
+ pageSize?: number;
185
238
  }): Promise<SessionMetadata[]>;
186
239
  watchSessions(auth?: AuthConfig): grpc.ClientReadableStream<any>;
187
240
  watchPolicies(auth?: AuthConfig): grpc.ClientReadableStream<any>;
188
- /** @deprecated Use {@link watchPolicies}. Scheduled for removal in 0.4.0. */
189
- _watchPolicies(auth?: AuthConfig): grpc.ClientReadableStream<any>;
190
241
  openStream(options?: {
191
242
  auth?: AuthConfig;
192
243
  }): MacpStream;
193
244
  watchModeRegistry(auth?: AuthConfig): grpc.ClientReadableStream<any>;
194
245
  watchRoots(auth?: AuthConfig): grpc.ClientReadableStream<any>;
195
- watchSignals(auth?: AuthConfig): grpc.ClientReadableStream<any>;
196
246
  /**
197
- * @deprecated Use {@link watchModeRegistry}, {@link watchRoots},
198
- * {@link watchSignals}. Scheduled for removal in 0.4.0 — the underscored
199
- * aliases were kept while `src/watchers.ts` migrated.
247
+ * Subscribe to the ambient signal plane. Requires auth as of runtime 0.5.0 —
248
+ * routed through {@link requireAuth} so a missing credential fails fast with a
249
+ * clear client-side error instead of a stream `UNAUTHENTICATED`.
200
250
  */
201
- _watchModeRegistry(auth?: AuthConfig): grpc.ClientReadableStream<any>;
202
- /** @deprecated Use {@link watchRoots}. Scheduled for removal in 0.4.0. */
203
- _watchRoots(auth?: AuthConfig): grpc.ClientReadableStream<any>;
204
- /** @deprecated Use {@link watchSignals}. Scheduled for removal in 0.4.0. */
205
- _watchSignals(auth?: AuthConfig): grpc.ClientReadableStream<any>;
251
+ watchSignals(auth?: AuthConfig): grpc.ClientReadableStream<any>;
206
252
  sendSignal(options: {
207
253
  signalType: string;
208
254
  data?: Buffer;
package/dist/client.js CHANGED
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.MacpClient = exports.MacpStream = void 0;
37
+ exports.grpcStatusName = grpcStatusName;
37
38
  /* eslint-disable @typescript-eslint/no-explicit-any --
38
39
  * The gRPC service client returned by `protoLoader.loadSync()` is typed as
39
40
  * `any` by `@grpc/proto-loader`; method signatures (Send, StreamSession,
@@ -98,6 +99,17 @@ class AsyncQueue {
98
99
  });
99
100
  }
100
101
  }
102
+ /**
103
+ * Map a gRPC `ServiceError.code` (numeric `grpc.status`) to its status name
104
+ * (e.g. `8` → `'RESOURCE_EXHAUSTED'`). Returns `undefined` for non-numeric or
105
+ * unknown codes. Used to populate {@link MacpTransportError.code}.
106
+ */
107
+ function grpcStatusName(code) {
108
+ if (typeof code !== 'number')
109
+ return undefined;
110
+ const name = grpc.status[code];
111
+ return typeof name === 'string' ? name : undefined;
112
+ }
101
113
  const TIMEOUT = Symbol('stream-read-timeout');
102
114
  const STREAM_END = Symbol('stream-end');
103
115
  class MacpStream {
@@ -120,7 +132,7 @@ class MacpStream {
120
132
  }
121
133
  });
122
134
  call.on('error', (error) => {
123
- this.queue.push(new errors_1.MacpTransportError(error.details || error.message));
135
+ this.queue.push(new errors_1.MacpTransportError(error.details || error.message, grpcStatusName(error.code)));
124
136
  });
125
137
  call.on('end', () => {
126
138
  this.queue.push(STREAM_END);
@@ -142,9 +154,19 @@ class MacpStream {
142
154
  });
143
155
  }
144
156
  /**
145
- * RFC-MACP-0006-A1: Send a subscribe-only frame to receive session history
146
- * + live broadcast. The runtime replays accepted envelopes from
147
- * `afterSequence` onwards, then continues with live broadcast.
157
+ * RFC-MACP-0006 §3.2: Send a subscribe-only frame to receive session history
158
+ * + live broadcast. The runtime replays accepted envelopes strictly after
159
+ * `afterSequence`, then continues with live broadcast.
160
+ *
161
+ * `afterSequence` is the **1-based accepted-envelope ordinal**, exclusive:
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.
166
+ * Ordinals are stable across log compaction and runtime restart. Resuming
167
+ * below a compacted base returns `FAILED_PRECONDITION` (inspectable via
168
+ * `MacpTransportError.code`) rather than silently skipping history (runtime
169
+ * ≥ 0.5.0; older runtimes compared inclusively against a raw log index).
148
170
  */
149
171
  sendSubscribe(sessionId, afterSequence = 0) {
150
172
  if (this.closed)
@@ -223,7 +245,7 @@ class MacpClient {
223
245
  }
224
246
  this.defaultDeadlineMs = options.defaultDeadlineMs;
225
247
  this.clientName = options.clientName ?? 'macp-sdk-typescript';
226
- this.clientVersion = options.clientVersion ?? '0.3.0';
248
+ this.clientVersion = options.clientVersion ?? '0.5.0';
227
249
  // eslint-disable-next-line @typescript-eslint/no-require-imports
228
250
  const { protoDir: defaultProtoDir } = require('@multiagentcoordinationprotocol/proto');
229
251
  const protoDir = options.protoDir ?? defaultProtoDir;
@@ -266,7 +288,7 @@ class MacpClient {
266
288
  return new Promise((resolve, reject) => {
267
289
  const callback = (error, response) => {
268
290
  if (error)
269
- reject(new errors_1.MacpTransportError(error.details || error.message));
291
+ reject(new errors_1.MacpTransportError(error.details || error.message, grpcStatusName(error.code)));
270
292
  else
271
293
  resolve(response);
272
294
  };
@@ -382,7 +404,25 @@ class MacpClient {
382
404
  async listRoots(deadlineMs) {
383
405
  return this.unary('ListRoots', {}, undefined, deadlineMs);
384
406
  }
407
+ /**
408
+ * Register an extension-mode descriptor (runtime 0.5.0 guardrails,
409
+ * change-review A5):
410
+ * - the descriptor MUST declare `Commitment` among `terminalMessageTypes` —
411
+ * an ext mode without a terminal `Commitment` can never resolve, so the
412
+ * runtime rejects it. This method fails fast client-side to match.
413
+ * - a later {@link promoteMode} into the reserved `macp.mode.*` namespace is
414
+ * rejected by the runtime.
415
+ * - a SessionStart with an empty `mode_version` binds the registered
416
+ * descriptor's `modeVersion`; a subsequent Commitment must echo that bound
417
+ * version (echoing `""` no longer matches vacuously). `BaseSession`
418
+ * defaults `modeVersion` to `'1.0.0'`, so set the descriptor's version to
419
+ * match, or override the session's `modeVersion`.
420
+ */
385
421
  async registerExtMode(descriptor, options) {
422
+ if (!descriptor.terminalMessageTypes?.includes('Commitment')) {
423
+ throw new errors_1.MacpSessionError(`ext-mode descriptor '${descriptor.mode}' must declare 'Commitment' in terminalMessageTypes; ` +
424
+ 'a mode without a terminal Commitment can never resolve and the runtime rejects it.');
425
+ }
386
426
  const auth = this.requireAuth(options?.auth);
387
427
  return this.unary('RegisterExtMode', { modeDescriptor: descriptor }, auth, options?.deadlineMs);
388
428
  }
@@ -412,10 +452,42 @@ class MacpClient {
412
452
  const res = await this.unary('ListPolicies', { mode: mode || '' }, auth, options?.deadlineMs);
413
453
  return res.descriptors || [];
414
454
  }
415
- async listSessions(options) {
455
+ /**
456
+ * Fetch a single page of sessions (proto ≥ 0.1.6). `pageSize: 0` (or absent)
457
+ * lets the server choose the page size; the server MAY cap it. Callers MUST
458
+ * NOT assume the listing is complete unless `nextPageToken` is empty. Pass a
459
+ * returned non-empty `nextPageToken` back as `pageToken` to fetch the next
460
+ * page; a stale token yields `INVALID_ARGUMENT`.
461
+ */
462
+ async listSessionsPage(options) {
416
463
  const auth = this.requireAuth(options?.auth);
417
- const res = await this.unary('ListSessions', {}, auth, options?.deadlineMs);
418
- return res.sessions || [];
464
+ const res = await this.unary('ListSessions', { pageSize: options?.pageSize ?? 0, pageToken: options?.pageToken ?? '' }, auth, options?.deadlineMs);
465
+ return { sessions: res.sessions ?? [], nextPageToken: res.nextPageToken ?? '' };
466
+ }
467
+ /**
468
+ * Enumerate ALL sessions, transparently walking pages until the runtime
469
+ * returns an empty `nextPageToken`. Preserves the "complete list" semantics
470
+ * this method has always documented, even against runtimes that cap page
471
+ * sizes (proto ≥ 0.1.6). For manual page control, use {@link listSessionsPage}.
472
+ */
473
+ async listSessions(options) {
474
+ const all = [];
475
+ let pageToken = '';
476
+ // Bound the walk defensively so a misbehaving runtime that never returns an
477
+ // empty token cannot spin forever.
478
+ for (let page = 0; page < 100_000; page++) {
479
+ const res = await this.listSessionsPage({
480
+ pageSize: options?.pageSize,
481
+ pageToken,
482
+ auth: options?.auth,
483
+ deadlineMs: options?.deadlineMs,
484
+ });
485
+ all.push(...res.sessions);
486
+ if (!res.nextPageToken)
487
+ return all;
488
+ pageToken = res.nextPageToken;
489
+ }
490
+ return all;
419
491
  }
420
492
  watchSessions(auth) {
421
493
  const metadata = this.metadata(auth);
@@ -425,10 +497,6 @@ class MacpClient {
425
497
  const metadata = this.metadata(auth);
426
498
  return metadata ? this.client.WatchPolicies({}, metadata) : this.client.WatchPolicies({});
427
499
  }
428
- /** @deprecated Use {@link watchPolicies}. Scheduled for removal in 0.4.0. */
429
- _watchPolicies(auth) {
430
- return this.watchPolicies(auth);
431
- }
432
500
  openStream(options) {
433
501
  const auth = this.requireAuth(options?.auth);
434
502
  const metadata = this.metadata(auth);
@@ -443,25 +511,15 @@ class MacpClient {
443
511
  const metadata = this.metadata(auth);
444
512
  return metadata ? this.client.WatchRoots({}, metadata) : this.client.WatchRoots({});
445
513
  }
446
- watchSignals(auth) {
447
- const metadata = this.metadata(auth);
448
- return metadata ? this.client.WatchSignals({}, metadata) : this.client.WatchSignals({});
449
- }
450
514
  /**
451
- * @deprecated Use {@link watchModeRegistry}, {@link watchRoots},
452
- * {@link watchSignals}. Scheduled for removal in 0.4.0 — the underscored
453
- * aliases were kept while `src/watchers.ts` migrated.
515
+ * Subscribe to the ambient signal plane. Requires auth as of runtime 0.5.0 —
516
+ * routed through {@link requireAuth} so a missing credential fails fast with a
517
+ * clear client-side error instead of a stream `UNAUTHENTICATED`.
454
518
  */
455
- _watchModeRegistry(auth) {
456
- return this.watchModeRegistry(auth);
457
- }
458
- /** @deprecated Use {@link watchRoots}. Scheduled for removal in 0.4.0. */
459
- _watchRoots(auth) {
460
- return this.watchRoots(auth);
461
- }
462
- /** @deprecated Use {@link watchSignals}. Scheduled for removal in 0.4.0. */
463
- _watchSignals(auth) {
464
- return this.watchSignals(auth);
519
+ watchSignals(auth) {
520
+ const selected = this.requireAuth(auth);
521
+ const metadata = (0, auth_1.metadataFromAuth)(selected);
522
+ return this.client.WatchSignals({}, metadata);
465
523
  }
466
524
  async sendSignal(options) {
467
525
  (0, validation_1.validateSignalType)(options.signalType, options.data);
@@ -30,6 +30,7 @@ export declare class DecisionSession {
30
30
  uri: string;
31
31
  name?: string;
32
32
  }[];
33
+ maxSuspendMs?: number;
33
34
  sender?: string;
34
35
  }): Promise<Ack>;
35
36
  propose(input: DecisionProposalPayload & {
package/dist/decision.js CHANGED
@@ -40,6 +40,7 @@ class DecisionSession {
40
40
  intent: input.intent,
41
41
  participants: input.participants,
42
42
  ttlMs: input.ttlMs,
43
+ maxSuspendMs: input.maxSuspendMs,
43
44
  modeVersion: this.modeVersion,
44
45
  configurationVersion: this.configurationVersion,
45
46
  });
@@ -47,6 +48,7 @@ class DecisionSession {
47
48
  intent: input.intent,
48
49
  participants: input.participants,
49
50
  ttlMs: input.ttlMs,
51
+ maxSuspendMs: input.maxSuspendMs,
50
52
  contextId: input.contextId,
51
53
  extensions: input.extensions,
52
54
  roots: input.roots,