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.
@@ -7,6 +7,12 @@ export declare function buildSessionStartPayload(input: {
7
7
  intent: string;
8
8
  participants: string[];
9
9
  ttlMs: number;
10
+ /**
11
+ * Per-session max-suspend cap in ms (proto ≥ 0.1.5). `0`/absent selects the
12
+ * runtime default (7 days). Emitted as `0` when omitted, matching `ttlMs`'s
13
+ * "0 = default" convention.
14
+ */
15
+ maxSuspendMs?: number;
10
16
  modeVersion?: string;
11
17
  configurationVersion?: string;
12
18
  policyVersion?: string;
@@ -24,6 +30,20 @@ export declare function buildCommitmentRef(input: {
24
30
  sessionId: string;
25
31
  commitmentHash: string;
26
32
  }): CommitmentRef;
33
+ /**
34
+ * Build a `CommitmentPayload`. Defaults `policyVersion` to
35
+ * {@link DEFAULT_POLICY_VERSION} (`'policy.default'`) when omitted.
36
+ *
37
+ * Policy-version echo (runtime ≥ 0.5.0): a Commitment with an **empty**
38
+ * `policyVersion` matches the session's bound policy — a session started with an
39
+ * empty policy_version no longer has to echo `'policy.default'`. A **non-empty**
40
+ * value must equal the session's resolved policy id exactly. The mode session
41
+ * helpers (`DecisionSession.commit`, etc.) echo the session-bound value, which
42
+ * is correct against both old and new runtimes; this standalone builder keeps
43
+ * the `'policy.default'` default for backward compatibility (older runtimes
44
+ * rejected an empty echo). Pass `policyVersion: ''` explicitly to opt into the
45
+ * new empty-matches-bound behavior — `''` is not coalesced by the default.
46
+ */
27
47
  export declare function buildCommitmentPayload(input: {
28
48
  action: string;
29
49
  authorityScope: string;
package/dist/envelope.js CHANGED
@@ -36,6 +36,7 @@ function buildSessionStartPayload(input) {
36
36
  configurationVersion: input.configurationVersion ?? constants_1.DEFAULT_CONFIGURATION_VERSION,
37
37
  policyVersion: input.policyVersion ?? constants_1.DEFAULT_POLICY_VERSION,
38
38
  ttlMs: input.ttlMs,
39
+ maxSuspendMs: input.maxSuspendMs ?? 0,
39
40
  contextId: input.contextId ?? '',
40
41
  extensions: input.extensions ?? {},
41
42
  roots: input.roots ?? [],
@@ -56,6 +57,20 @@ function inferOutcomePositive(action) {
56
57
  function buildCommitmentRef(input) {
57
58
  return { sessionId: input.sessionId, commitmentHash: input.commitmentHash };
58
59
  }
60
+ /**
61
+ * Build a `CommitmentPayload`. Defaults `policyVersion` to
62
+ * {@link DEFAULT_POLICY_VERSION} (`'policy.default'`) when omitted.
63
+ *
64
+ * Policy-version echo (runtime ≥ 0.5.0): a Commitment with an **empty**
65
+ * `policyVersion` matches the session's bound policy — a session started with an
66
+ * empty policy_version no longer has to echo `'policy.default'`. A **non-empty**
67
+ * value must equal the session's resolved policy id exactly. The mode session
68
+ * helpers (`DecisionSession.commit`, etc.) echo the session-bound value, which
69
+ * is correct against both old and new runtimes; this standalone builder keeps
70
+ * the `'policy.default'` default for backward compatibility (older runtimes
71
+ * rejected an empty echo). Pass `policyVersion: ''` explicitly to opt into the
72
+ * new empty-matches-bound behavior — `''` is not coalesced by the default.
73
+ */
59
74
  function buildCommitmentPayload(input) {
60
75
  const payload = {
61
76
  commitmentId: input.commitmentId ?? newCommitmentId(),
package/dist/errors.d.ts CHANGED
@@ -3,7 +3,16 @@ export declare class MacpSdkError extends Error {
3
3
  constructor(message: string);
4
4
  }
5
5
  export declare class MacpTransportError extends MacpSdkError {
6
- constructor(message: string);
6
+ /**
7
+ * gRPC status name (e.g. `RESOURCE_EXHAUSTED`, `FAILED_PRECONDITION`,
8
+ * `UNAUTHENTICATED`) when the underlying transport failure carried one.
9
+ * Lets consumers distinguish, for example, watch-stream consumer lag
10
+ * (`RESOURCE_EXHAUSTED` → reconnect) from an auth failure (`UNAUTHENTICATED`
11
+ * → do not reconnect), or a passive-subscribe resume below a compacted base
12
+ * (`FAILED_PRECONDITION`). Absent for locally-raised transport errors.
13
+ */
14
+ readonly code?: string;
15
+ constructor(message: string, code?: string);
7
16
  }
8
17
  /**
9
18
  * Structured NACK record. Mirrors python-sdk's `AckFailure` dataclass so
package/dist/errors.js CHANGED
@@ -9,9 +9,19 @@ class MacpSdkError extends Error {
9
9
  }
10
10
  exports.MacpSdkError = MacpSdkError;
11
11
  class MacpTransportError extends MacpSdkError {
12
- constructor(message) {
12
+ /**
13
+ * gRPC status name (e.g. `RESOURCE_EXHAUSTED`, `FAILED_PRECONDITION`,
14
+ * `UNAUTHENTICATED`) when the underlying transport failure carried one.
15
+ * Lets consumers distinguish, for example, watch-stream consumer lag
16
+ * (`RESOURCE_EXHAUSTED` → reconnect) from an auth failure (`UNAUTHENTICATED`
17
+ * → do not reconnect), or a passive-subscribe resume below a compacted base
18
+ * (`FAILED_PRECONDITION`). Absent for locally-raised transport errors.
19
+ */
20
+ code;
21
+ constructor(message, code) {
13
22
  super(message);
14
23
  this.name = 'MacpTransportError';
24
+ this.code = code;
15
25
  }
16
26
  }
17
27
  exports.MacpTransportError = MacpTransportError;
package/dist/handoff.d.ts CHANGED
@@ -30,6 +30,7 @@ export declare class HandoffSession {
30
30
  uri: string;
31
31
  name?: string;
32
32
  }[];
33
+ maxSuspendMs?: number;
33
34
  sender?: string;
34
35
  }): Promise<Ack>;
35
36
  offer(input: HandoffOfferPayload & {
package/dist/handoff.js CHANGED
@@ -40,6 +40,7 @@ class HandoffSession {
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 HandoffSession {
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,
@@ -89,12 +91,18 @@ class HandoffSession {
89
91
  }
90
92
  async acceptHandoff(input) {
91
93
  (0, validation_1.validateRequiredField)('handoffId', input.handoffId);
94
+ // `implicit` is a runtime-emitted-only flag (RFC-MACP-0010 §5.1); the
95
+ // runtime rejects client-submitted accepts with implicit=true. Strip it so
96
+ // a caller can never produce a rejected envelope, regardless of what they
97
+ // pass — belt and braces with the JSDoc "read-only" contract on the type.
98
+ const rest = { ...input };
99
+ delete rest.implicit;
92
100
  const envelope = (0, envelope_1.buildEnvelope)({
93
101
  mode: constants_1.MODE_HANDOFF,
94
102
  messageType: 'HandoffAccept',
95
103
  sessionId: this.sessionId,
96
104
  sender: this.senderFor(input.sender, input.auth),
97
- payload: this.client.protoRegistry.encodeKnownPayload(constants_1.MODE_HANDOFF, 'HandoffAccept', (0, envelope_1.toProtoPayload)(input)),
105
+ payload: this.client.protoRegistry.encodeKnownPayload(constants_1.MODE_HANDOFF, 'HandoffAccept', rest),
98
106
  });
99
107
  return this.sendAndTrack(envelope, input.auth);
100
108
  }
package/dist/policy.d.ts CHANGED
@@ -15,7 +15,18 @@ export interface CommitmentRules {
15
15
  }
16
16
  export interface VotingRules {
17
17
  algorithm?: 'none' | 'majority' | 'supermajority' | 'unanimous' | 'weighted' | 'plurality';
18
+ /**
19
+ * Vote-share fraction on a **0–1 scale** (e.g. `0.5` = simple majority). This
20
+ * is a DIFFERENT field from the quorum `percentage` below — do not confuse
21
+ * the scales.
22
+ */
18
23
  threshold?: number;
24
+ /**
25
+ * Participation quorum. For `type: 'percentage'`, `value` is an **integer
26
+ * 0–100** (the runtime evaluates it on the same 0–100 scale as the quorum
27
+ * mode's threshold), NOT a 0–1 fraction. For `type: 'count'`, an absolute
28
+ * participant count.
29
+ */
19
30
  quorum?: {
20
31
  type: 'count' | 'percentage';
21
32
  value: number;
@@ -39,6 +50,15 @@ export interface EvaluationRules {
39
50
  }
40
51
  export interface QuorumThreshold {
41
52
  type: 'n_of_m' | 'percentage' | 'weighted';
53
+ /**
54
+ * The approval bar (RFC-MACP-0012 §4.2) — how many APPROVE commitments the
55
+ * request needs, NOT a participation quorum. Scale depends on `type`:
56
+ * - `n_of_m` / `weighted`: an absolute count (or weight sum).
57
+ * - `percentage`: an **integer 0–100**; the runtime computes the bar as
58
+ * `ceil(value / 100 × participants)`. So `75` means "≥ 75% of participants
59
+ * must approve", NOT `0.75`. `buildQuorumPolicy` validates this range and
60
+ * throws on a non-integer or out-of-[0,100] value.
61
+ */
42
62
  value: number;
43
63
  }
44
64
  export interface AbstentionRules {
package/dist/policy.js CHANGED
@@ -5,6 +5,7 @@ exports.buildQuorumPolicy = buildQuorumPolicy;
5
5
  exports.buildProposalPolicy = buildProposalPolicy;
6
6
  exports.buildTaskPolicy = buildTaskPolicy;
7
7
  exports.buildHandoffPolicy = buildHandoffPolicy;
8
+ const errors_1 = require("./errors");
8
9
  // ── Builder helpers ─────────────────────────────────────────────────
9
10
  function serializeCommitment(commitment) {
10
11
  return {
@@ -49,6 +50,14 @@ function buildDecisionPolicy(policyId, description, rules) {
49
50
  };
50
51
  }
51
52
  function buildQuorumPolicy(policyId, description, rules) {
53
+ if (rules.threshold?.type === 'percentage') {
54
+ const v = rules.threshold.value;
55
+ if (!Number.isInteger(v) || v < 0 || v > 100) {
56
+ throw new errors_1.MacpSessionError(`quorum percentage threshold must be an integer in [0, 100] (e.g. 75 for 75%), got ${v}. ` +
57
+ 'The runtime computes the approval bar as ceil(value/100 × participants); a fractional value ' +
58
+ 'like 0.75 would round to a ~1% bar.');
59
+ }
60
+ }
52
61
  const rulesJson = {
53
62
  threshold: {
54
63
  type: rules.threshold?.type ?? 'n_of_m',
@@ -10,6 +10,12 @@ export interface HandoffRecord {
10
10
  contextContentType?: string;
11
11
  acceptedBy?: string;
12
12
  declinedBy?: string;
13
+ /**
14
+ * `true` when the accept that resolved this handoff was a runtime-emitted
15
+ * synthetic implicit accept (RFC-MACP-0010 §5.1, proto ≥ 0.1.6). `false` for
16
+ * an explicit target accept. Only meaningful once `status === 'accepted'`.
17
+ */
18
+ implicit?: boolean;
13
19
  }
14
20
  export declare class HandoffProjection {
15
21
  readonly handoffs: Map<string, HandoffRecord>;
@@ -22,6 +28,12 @@ export declare class HandoffProjection {
22
28
  getHandoff(handoffId: string): HandoffRecord | undefined;
23
29
  isAccepted(handoffId: string): boolean;
24
30
  isDeclined(handoffId: string): boolean;
31
+ /**
32
+ * `true` when the handoff was resolved by a runtime-emitted synthetic implicit
33
+ * accept (RFC-MACP-0010 §5.1) rather than an explicit target accept. Returns
34
+ * `false` for explicitly-accepted or not-yet-accepted handoffs.
35
+ */
36
+ isImplicitlyAccepted(handoffId: string): boolean;
25
37
  pendingHandoffs(): HandoffRecord[];
26
38
  hasAcceptedOffer(handoffId?: string): boolean;
27
39
  activeOffer(): HandoffRecord | undefined;
@@ -47,6 +47,10 @@ class HandoffProjection {
47
47
  if (handoff) {
48
48
  handoff.status = 'accepted';
49
49
  handoff.acceptedBy = record.acceptedBy;
50
+ // proto3 bool defaults are materialized to `false` on decode
51
+ // (proto-registry), so this is always a real boolean once proto 0.1.6
52
+ // is loaded — `true` marks a runtime synthetic implicit accept.
53
+ handoff.implicit = record.implicit ?? false;
50
54
  }
51
55
  this.phase = 'Accepted';
52
56
  break;
@@ -89,6 +93,15 @@ class HandoffProjection {
89
93
  isDeclined(handoffId) {
90
94
  return this.handoffs.get(handoffId)?.status === 'declined';
91
95
  }
96
+ /**
97
+ * `true` when the handoff was resolved by a runtime-emitted synthetic implicit
98
+ * accept (RFC-MACP-0010 §5.1) rather than an explicit target accept. Returns
99
+ * `false` for explicitly-accepted or not-yet-accepted handoffs.
100
+ */
101
+ isImplicitlyAccepted(handoffId) {
102
+ const handoff = this.handoffs.get(handoffId);
103
+ return handoff?.status === 'accepted' && handoff.implicit === true;
104
+ }
92
105
  pendingHandoffs() {
93
106
  return [...this.handoffs.values()].filter((h) => h.status === 'offered' || h.status === 'context_sent');
94
107
  }
@@ -30,6 +30,7 @@ export declare class ProposalSession {
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: ProposalModeProposalPayload & {
package/dist/proposal.js CHANGED
@@ -40,6 +40,7 @@ class ProposalSession {
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 ProposalSession {
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,
@@ -7,6 +7,16 @@ export declare class ProtoRegistry {
7
7
  decodeMessage(typeName: string, payload: Buffer): Record<string, unknown>;
8
8
  encodeKnownPayload(mode: string, messageType: string, value: Record<string, unknown>): Buffer;
9
9
  decodeKnownPayload(mode: string, messageType: string, payload: Buffer): Record<string, unknown> | undefined;
10
+ /**
11
+ * Decode an `ext.multi_round.v1` `Contribute` payload, accepting both wire
12
+ * formats: legacy JSON (`{"value":"..."}`, still replayed verbatim from
13
+ * pre-proto histories) and canonical protobuf (`ContributePayload`). JSON is
14
+ * tried first — mirroring the runtime's permanent decode order — and the two
15
+ * encodings are disjoint on their first byte (`{` = 0x7b vs proto field-1
16
+ * tag 0x0A), so a canonical proto payload never mis-parses as JSON and vice
17
+ * versa. Both normalize to `{ value: string }`.
18
+ */
19
+ private decodeMultiRoundContribute;
10
20
  private lookupType;
11
21
  private tryDecodeUtf8;
12
22
  }
@@ -46,6 +46,9 @@ const CORE_MAP = {
46
46
  Signal: 'macp.v1.SignalPayload',
47
47
  Progress: 'macp.v1.ProgressPayload',
48
48
  };
49
+ // The one mapping whose historical payloads may be legacy JSON. Runtime replays
50
+ // pre-proto histories byte-identically, so decode must accept both encodings.
51
+ const MULTI_ROUND_CONTRIBUTE = 'macp.modes.multi_round.v1.ContributePayload';
49
52
  const MODE_MAP = {
50
53
  [constants_1.MODE_DECISION]: {
51
54
  Proposal: 'macp.modes.decision.v1.ProposalPayload',
@@ -81,7 +84,10 @@ const MODE_MAP = {
81
84
  Abstain: 'macp.modes.quorum.v1.AbstainPayload',
82
85
  },
83
86
  [constants_1.MODE_MULTI_ROUND]: {
84
- Contribute: '__json__',
87
+ // Canonical protobuf as of proto 0.1.4 / runtime 0.5.0. Encode is always
88
+ // protobuf; decode tries legacy JSON (`{"value":"..."}`) first, then
89
+ // protobuf — see `MULTI_ROUND_CONTRIBUTE` handling in decodeKnownPayload.
90
+ Contribute: MULTI_ROUND_CONTRIBUTE,
85
91
  },
86
92
  };
87
93
  const PROTO_FILES = [
@@ -93,6 +99,7 @@ const PROTO_FILES = [
93
99
  'macp/modes/task/v1/task.proto',
94
100
  'macp/modes/handoff/v1/handoff.proto',
95
101
  'macp/modes/quorum/v1/quorum.proto',
102
+ 'macp/modes/multi_round/v1/multi_round.proto',
96
103
  ];
97
104
  class ProtoRegistry {
98
105
  protoDir;
@@ -155,8 +162,34 @@ class ProtoRegistry {
155
162
  return this.tryDecodeUtf8(payload);
156
163
  if (typeName === '__json__')
157
164
  return this.tryDecodeUtf8(payload);
165
+ if (typeName === MULTI_ROUND_CONTRIBUTE)
166
+ return this.decodeMultiRoundContribute(payload);
158
167
  return this.decodeMessage(typeName, payload);
159
168
  }
169
+ /**
170
+ * Decode an `ext.multi_round.v1` `Contribute` payload, accepting both wire
171
+ * formats: legacy JSON (`{"value":"..."}`, still replayed verbatim from
172
+ * pre-proto histories) and canonical protobuf (`ContributePayload`). JSON is
173
+ * tried first — mirroring the runtime's permanent decode order — and the two
174
+ * encodings are disjoint on their first byte (`{` = 0x7b vs proto field-1
175
+ * tag 0x0A), so a canonical proto payload never mis-parses as JSON and vice
176
+ * versa. Both normalize to `{ value: string }`.
177
+ */
178
+ decodeMultiRoundContribute(payload) {
179
+ if (!payload.length)
180
+ return this.decodeMessage(MULTI_ROUND_CONTRIBUTE, payload);
181
+ if (payload[0] === 0x7b) {
182
+ // Leading `{` — legacy JSON. Parse and normalize to { value }.
183
+ try {
184
+ const parsed = JSON.parse(payload.toString('utf8'));
185
+ return { value: typeof parsed.value === 'string' ? parsed.value : String(parsed.value ?? '') };
186
+ }
187
+ catch {
188
+ // Not valid JSON despite the leading brace — fall through to protobuf.
189
+ }
190
+ }
191
+ return this.decodeMessage(MULTI_ROUND_CONTRIBUTE, payload);
192
+ }
160
193
  lookupType(typeName) {
161
194
  const lookedUp = this.root.lookupType(typeName);
162
195
  if (!(lookedUp instanceof protobufjs_1.default.Type))
package/dist/quorum.d.ts CHANGED
@@ -30,6 +30,7 @@ export declare class QuorumSession {
30
30
  uri: string;
31
31
  name?: string;
32
32
  }[];
33
+ maxSuspendMs?: number;
33
34
  sender?: string;
34
35
  }): Promise<Ack>;
35
36
  requestApproval(input: ApprovalRequestPayload & {
package/dist/quorum.js CHANGED
@@ -40,6 +40,7 @@ class QuorumSession {
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 QuorumSession {
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,
package/dist/retry.d.ts CHANGED
@@ -5,6 +5,13 @@ export interface RetryPolicy {
5
5
  maxRetries: number;
6
6
  backoffBase: number;
7
7
  backoffMax: number;
8
+ /**
9
+ * Retryable **ack** (NACK) codes — MACP application-level error codes on a
10
+ * `Send` response (e.g. `RATE_LIMITED`, `INTERNAL_ERROR`). This is distinct
11
+ * from a gRPC **stream** status such as `RESOURCE_EXHAUSTED` (watch-stream
12
+ * consumer lag), which is not an ack code and is not handled here — reconnect
13
+ * the stream instead (see the watcher classes' reconnect JSDoc).
14
+ */
8
15
  retryableCodes: Set<string>;
9
16
  }
10
17
  export declare const DEFAULT_RETRY_POLICY: RetryPolicy;
package/dist/task.d.ts CHANGED
@@ -30,6 +30,7 @@ export declare class TaskSession {
30
30
  uri: string;
31
31
  name?: string;
32
32
  }[];
33
+ maxSuspendMs?: number;
33
34
  sender?: string;
34
35
  }): Promise<Ack>;
35
36
  requestTask(input: TaskRequestPayload & {
package/dist/task.js CHANGED
@@ -40,6 +40,7 @@ class TaskSession {
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 TaskSession {
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,
package/dist/types.d.ts CHANGED
@@ -106,6 +106,15 @@ export interface SessionStartPayload {
106
106
  configurationVersion: string;
107
107
  policyVersion?: string;
108
108
  ttlMs: number;
109
+ /**
110
+ * Maximum cumulative suspended duration (ms) before a SUSPENDED session
111
+ * transitions to EXPIRED (RFC-MACP-0001 §7.5, proto ≥ 0.1.5). Bound at
112
+ * session start like {@link ttlMs}: `0` (or absent) selects the runtime's
113
+ * configured default (7 days). The runtime records the resolved cap so
114
+ * replay is deterministic. Negative values are rejected at SessionStart.
115
+ * Decoded as a string (int64) but accepted as `number` on input.
116
+ */
117
+ maxSuspendMs?: number | string;
109
118
  contextId?: string;
110
119
  extensions?: Record<string, Buffer>;
111
120
  roots?: Root[];
@@ -251,6 +260,15 @@ export interface HandoffAcceptPayload {
251
260
  handoffId: string;
252
261
  acceptedBy: string;
253
262
  reason?: string;
263
+ /**
264
+ * Runtime-emitted synthetic accepts ONLY (RFC-MACP-0010 §5.1): `true` on the
265
+ * accept the runtime produces when a handoff's `implicit_accept_timeout_ms`
266
+ * elapses (`message_id` = `implicit-accept:<handoff_id>`, proto ≥ 0.1.6).
267
+ * Read-only — clients MUST NOT submit an accept with `implicit = true`; the
268
+ * runtime rejects it. `HandoffSession.acceptHandoff` strips this field before
269
+ * encoding so a caller can never produce a rejected envelope.
270
+ */
271
+ implicit?: boolean;
254
272
  }
255
273
  export interface HandoffDeclinePayload {
256
274
  handoffId: string;
@@ -6,12 +6,14 @@ 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
8
  export declare function validateTtlMs(ttlMs: number): void;
9
+ export declare function validateMaxSuspendMs(maxSuspendMs: number): void;
9
10
  export declare function validateParticipants(participants: string[]): void;
10
11
  export declare function validateRequiredField(fieldName: string, value: string): void;
11
12
  export declare function validateSessionStart(input: {
12
13
  intent: string;
13
14
  participants: string[];
14
15
  ttlMs: number;
16
+ maxSuspendMs?: number;
15
17
  modeVersion: string;
16
18
  configurationVersion: string;
17
19
  }): void;
@@ -8,6 +8,7 @@ exports.validateSeverity = validateSeverity;
8
8
  exports.validateParticipantCount = validateParticipantCount;
9
9
  exports.validateSignalType = validateSignalType;
10
10
  exports.validateTtlMs = validateTtlMs;
11
+ exports.validateMaxSuspendMs = validateMaxSuspendMs;
11
12
  exports.validateParticipants = validateParticipants;
12
13
  exports.validateRequiredField = validateRequiredField;
13
14
  exports.validateSessionStart = validateSessionStart;
@@ -65,6 +66,13 @@ function validateTtlMs(ttlMs) {
65
66
  throw new errors_1.MacpSessionError(`ttl_ms must be in [1, ${MAX_TTL_MS}], got ${ttlMs}`);
66
67
  }
67
68
  }
69
+ function validateMaxSuspendMs(maxSuspendMs) {
70
+ // 0 = "use runtime default"; only negatives are rejected (matches the
71
+ // runtime, which rejects negative max_suspend_ms at SessionStart).
72
+ if (!Number.isFinite(maxSuspendMs) || maxSuspendMs < 0) {
73
+ throw new errors_1.MacpSessionError(`max_suspend_ms must be >= 0 (0 = runtime default), got ${maxSuspendMs}`);
74
+ }
75
+ }
68
76
  function validateParticipants(participants) {
69
77
  if (!participants.length) {
70
78
  throw new errors_1.MacpSessionError('participants must be non-empty');
@@ -87,6 +95,8 @@ function validateSessionStart(input) {
87
95
  validateRequiredField('intent', input.intent);
88
96
  validateParticipants(input.participants);
89
97
  validateTtlMs(input.ttlMs);
98
+ if (input.maxSuspendMs !== undefined)
99
+ validateMaxSuspendMs(input.maxSuspendMs);
90
100
  validateRequiredField('modeVersion', input.modeVersion);
91
101
  validateRequiredField('configurationVersion', input.configurationVersion);
92
102
  }
@@ -1,5 +1,5 @@
1
1
  import type { AuthConfig } from './auth';
2
- import type { MacpClient } from './client';
2
+ import { type MacpClient } from './client';
3
3
  import type { Envelope, PolicyDescriptor, RegistryChanged, RootsChanged, SessionLifecycleEvent, SessionLifecycleEventType } from './types';
4
4
  /**
5
5
  * The lifecycle event types after which a session emits no further events.
@@ -43,6 +43,16 @@ export declare class RootsWatcher {
43
43
  watch(handler: (change: RootsChanged) => void | Promise<void>): Promise<void>;
44
44
  nextChange(): Promise<RootsChanged>;
45
45
  }
46
+ /**
47
+ * Watches the ambient signal plane. **Requires auth** as of runtime 0.5.0: if
48
+ * no credential is configured on the watcher or the client, iterating
49
+ * {@link SignalWatcher.signals} throws immediately (via `client.watchSignals`).
50
+ *
51
+ * Consumer lag terminates the stream with a coded `MacpTransportError`
52
+ * (`code === 'RESOURCE_EXHAUSTED'`); the correct response is to reconnect.
53
+ * A missing/invalid credential surfaces as `code === 'UNAUTHENTICATED'` — fix
54
+ * auth, do not reconnect.
55
+ */
46
56
  export declare class SignalWatcher {
47
57
  private readonly client;
48
58
  private readonly auth?;
@@ -67,26 +77,20 @@ export declare class PolicyWatcher {
67
77
  watch(handler: (change: PolicyChange) => void | Promise<void>): Promise<void>;
68
78
  nextChange(): Promise<PolicyChange>;
69
79
  }
80
+ /**
81
+ * Watches session lifecycle events (`WatchSessions`). Consumer lag terminates
82
+ * the stream with a coded `MacpTransportError` (`code === 'RESOURCE_EXHAUSTED'`);
83
+ * the correct response is to reconnect and re-sync current state via
84
+ * {@link MacpClient.listSessions}, since events emitted during the gap are lost.
85
+ */
70
86
  export declare class SessionLifecycleWatcher {
71
87
  private readonly client;
72
88
  private readonly auth?;
73
- private deprecatedEventsWarned;
74
- private deprecatedNextEventWarned;
75
89
  constructor(client: MacpClient, options?: {
76
90
  auth?: AuthConfig;
77
91
  });
78
92
  changes(signal?: AbortSignal): AsyncGenerator<SessionLifecycleEvent, void, void>;
79
93
  watch(handler: (event: SessionLifecycleEvent) => void | Promise<void>): Promise<void>;
80
94
  nextChange(): Promise<SessionLifecycleEvent>;
81
- /**
82
- * @deprecated Use {@link changes} — renamed for parity with python-sdk's
83
- * `SessionLifecycleWatcher.changes()` and the uniform `changes()` name
84
- * used by all other watchers in both SDKs. Will be removed in 0.5.0.
85
- */
86
- events(signal?: AbortSignal): AsyncGenerator<SessionLifecycleEvent, void, void>;
87
- /**
88
- * @deprecated Use {@link nextChange}. Will be removed in 0.5.0.
89
- */
90
- nextEvent(): Promise<SessionLifecycleEvent>;
91
95
  }
92
96
  export {};
package/dist/watchers.js CHANGED
@@ -1,7 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SessionLifecycleWatcher = exports.PolicyWatcher = exports.SignalWatcher = exports.RootsWatcher = exports.ModeRegistryWatcher = exports.isTerminalSessionLifecycleEvent = exports.isSessionResumed = exports.isSessionSuspended = exports.isSessionCancelled = exports.isSessionExpired = exports.isSessionResolved = exports.isSessionCreated = exports.TERMINAL_SESSION_LIFECYCLE_EVENT_TYPES = void 0;
4
- const logging_1 = require("./logging");
4
+ const client_1 = require("./client");
5
+ const errors_1 = require("./errors");
5
6
  /**
6
7
  * The lifecycle event types after which a session emits no further events.
7
8
  * `EVENT_TYPE_CANCELLED` (proto 0.1.3) is terminal alongside `RESOLVED` and
@@ -48,7 +49,14 @@ function serverStreamToAsyncGenerator(stream) {
48
49
  }
49
50
  });
50
51
  stream.on('error', (error) => {
51
- queue.push({ error });
52
+ // Wrap the raw gRPC ServiceError into a coded MacpTransportError so
53
+ // consumers can branch on `.code`: consumer lag terminates a watch stream
54
+ // with RESOURCE_EXHAUSTED (reconnect + re-sync), an unauthenticated
55
+ // WatchSignals with UNAUTHENTICATED (fix auth, do NOT reconnect).
56
+ const code = (0, client_1.grpcStatusName)(error.code);
57
+ const serviceErr = error;
58
+ const wrapped = new errors_1.MacpTransportError(serviceErr.details || error.message, code);
59
+ queue.push({ error: wrapped });
52
60
  if (resolve) {
53
61
  resolve();
54
62
  resolve = null;
@@ -148,6 +156,16 @@ class RootsWatcher {
148
156
  }
149
157
  }
150
158
  exports.RootsWatcher = RootsWatcher;
159
+ /**
160
+ * Watches the ambient signal plane. **Requires auth** as of runtime 0.5.0: if
161
+ * no credential is configured on the watcher or the client, iterating
162
+ * {@link SignalWatcher.signals} throws immediately (via `client.watchSignals`).
163
+ *
164
+ * Consumer lag terminates the stream with a coded `MacpTransportError`
165
+ * (`code === 'RESOURCE_EXHAUSTED'`); the correct response is to reconnect.
166
+ * A missing/invalid credential surfaces as `code === 'UNAUTHENTICATED'` — fix
167
+ * auth, do not reconnect.
168
+ */
151
169
  class SignalWatcher {
152
170
  client;
153
171
  auth;
@@ -210,11 +228,15 @@ class PolicyWatcher {
210
228
  }
211
229
  }
212
230
  exports.PolicyWatcher = PolicyWatcher;
231
+ /**
232
+ * Watches session lifecycle events (`WatchSessions`). Consumer lag terminates
233
+ * the stream with a coded `MacpTransportError` (`code === 'RESOURCE_EXHAUSTED'`);
234
+ * the correct response is to reconnect and re-sync current state via
235
+ * {@link MacpClient.listSessions}, since events emitted during the gap are lost.
236
+ */
213
237
  class SessionLifecycleWatcher {
214
238
  client;
215
239
  auth;
216
- deprecatedEventsWarned = false;
217
- deprecatedNextEventWarned = false;
218
240
  constructor(client, options) {
219
241
  this.client = client;
220
242
  this.auth = options?.auth;
@@ -243,27 +265,5 @@ class SessionLifecycleWatcher {
243
265
  throw new Error('stream ended before receiving a session lifecycle event');
244
266
  return result.value;
245
267
  }
246
- /**
247
- * @deprecated Use {@link changes} — renamed for parity with python-sdk's
248
- * `SessionLifecycleWatcher.changes()` and the uniform `changes()` name
249
- * used by all other watchers in both SDKs. Will be removed in 0.5.0.
250
- */
251
- async *events(signal) {
252
- if (!this.deprecatedEventsWarned) {
253
- this.deprecatedEventsWarned = true;
254
- logging_1.logger.warn('[deprecated] SessionLifecycleWatcher.events() — use changes() instead');
255
- }
256
- yield* this.changes(signal);
257
- }
258
- /**
259
- * @deprecated Use {@link nextChange}. Will be removed in 0.5.0.
260
- */
261
- async nextEvent() {
262
- if (!this.deprecatedNextEventWarned) {
263
- this.deprecatedNextEventWarned = true;
264
- logging_1.logger.warn('[deprecated] SessionLifecycleWatcher.nextEvent() — use nextChange() instead');
265
- }
266
- return this.nextChange();
267
- }
268
268
  }
269
269
  exports.SessionLifecycleWatcher = SessionLifecycleWatcher;