macp-sdk-typescript 0.9.0 → 0.11.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.
@@ -1,4 +1,4 @@
1
- import type { ProjectionAnomaly } from './base';
1
+ import { BaseProjection } from './base';
2
2
  import type { Envelope } from '../types';
3
3
  import type { ProtoRegistry } from '../proto-registry';
4
4
  export interface TaskRecord {
@@ -33,58 +33,35 @@ export interface TaskFailureRecord {
33
33
  retryable: boolean;
34
34
  sender: string;
35
35
  }
36
- export declare class TaskProjection {
36
+ export declare class TaskProjection extends BaseProjection {
37
+ protected readonly mode = "macp.mode.task.v1";
37
38
  readonly tasks: Map<string, TaskRecord>;
38
39
  readonly updates: TaskUpdateRecord[];
39
40
  readonly completions: TaskCompletionRecord[];
40
41
  readonly failures: TaskFailureRecord[];
41
- /**
42
- * The session's accepted history, one envelope per unique `message_id`. See
43
- * `BaseProjection.transcript` (`src/projections/base.ts`) for the full
44
- * redelivery-idempotence contract (RFC-MACP-0006 §3.2); duplicated here
45
- * only as a one-line pointer.
46
- */
47
- readonly transcript: Envelope[];
48
- /**
49
- * Cardinality anomalies recorded while replaying this projection's accepted
50
- * transcript. See `BaseProjection.anomalies` (`src/projections/base.ts`)
51
- * for the canonical description; duplicated here only as a one-line
52
- * pointer. No built-in detection populates this for Task mode.
53
- */
54
- readonly anomalies: ProjectionAnomaly[];
55
42
  phase: 'Pending' | 'Requested' | 'InProgress' | 'Completed' | 'Failed' | 'Committed';
56
- commitment?: Record<string, unknown>;
57
43
  /**
58
- * `message_id`s already applied to this projection. See
59
- * `BaseProjection.seenMessageIds` (`src/projections/base.ts`) for the full
60
- * redelivery-idempotence rationale (RFC-MACP-0006 §3.2); duplicated here
61
- * only as a one-line pointer.
62
- */
63
- private readonly seenMessageIds;
64
- /**
65
- * Apply one envelope to this projection's in-process state.
44
+ * The **session-level** active assignment: who currently holds the session's
45
+ * single assignee slot, and which `task_id` they took it on.
66
46
  *
67
- * Input contract: **accepted-only**, caller-maintained (`Envelope` carries
68
- * no acceptance marker). Canonical source: `schemas/conformance/README.md`
69
- * "Notes:", RFC-MACP-0007 §5.3, RFC-MACP-0011 §5. Full rationale and failure
70
- * mode are documented once, on `BaseProjection.applyEnvelope`
71
- * (`src/projections/base.ts`), and duplicated here only as a one-line
72
- * pointer so six independent copies of the same prose cannot drift.
47
+ * RFC-MACP-0009 §5 rule 3 (`:69`): "Only one assignee may become active for
48
+ * the Session in base v1." The constraint is scoped to the *Session*, not to
49
+ * a `task_id` — rule 1 (`:67`) separately caps a v1 session at one
50
+ * `TaskRequest`, so in a conforming transcript the two scopes coincide, but
51
+ * an unfiltered transcript carrying two `TaskRequest`s must not be able to
52
+ * hand out two active assignees. That is why this is one field on the
53
+ * projection rather than a per-`TaskRecord` flag (issue #71).
73
54
  *
74
- * Redelivery idempotence: a redelivered envelope (same `message_id`) is a
75
- * non-event — not appended to `transcript`, not passed to the `switch`.
76
- * See `BaseProjection.applyEnvelope` for the full RFC-MACP-0006 §3.2
77
- * citation; duplicated here only as a one-line pointer.
78
- */
79
- applyEnvelope(envelope: Envelope, protoRegistry: ProtoRegistry): void;
80
- /**
81
- * True once at least one `ProjectionAnomaly` has been recorded. See
82
- * `BaseProjection.hasAnomalies` (`src/projections/base.ts`) for the
83
- * canonical description; duplicated here only as a one-line pointer.
55
+ * `sender` is the envelope sender, not `TaskAcceptPayload.assignee`, to
56
+ * mirror the reference runtime: `macp-runtime`
57
+ * `crates/macp-modes/src/mode/task.rs:211` sets `state.active_assignee =
58
+ * Some(env.sender)` after rejecting any payload whose `assignee` disagrees
59
+ * with the sender (`:205-207`). `taskId` is retained so a reject can clear
60
+ * the right `TaskRecord.assignee` even on a multi-task transcript.
84
61
  */
85
- get hasAnomalies(): boolean;
86
- get isCommitted(): boolean;
87
- get isPositiveOutcome(): boolean | undefined;
62
+ private activeAssignment?;
63
+ /** Handle a Task-mode envelope. See `BaseProjection.applyEnvelope` for the accepted-only input contract and redelivery/rollback semantics shared by all built-in modes. */
64
+ protected applyMode(envelope: Envelope, protoRegistry: ProtoRegistry): void;
88
65
  getTask(taskId: string): TaskRecord | undefined;
89
66
  isComplete(taskId: string): boolean;
90
67
  isFailed(taskId: string): boolean;
@@ -2,65 +2,36 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.TaskProjection = void 0;
4
4
  const constants_1 = require("../constants");
5
- const logging_1 = require("../logging");
6
- class TaskProjection {
5
+ const base_1 = require("./base");
6
+ class TaskProjection extends base_1.BaseProjection {
7
+ mode = constants_1.MODE_TASK;
7
8
  tasks = new Map();
8
9
  updates = [];
9
10
  completions = [];
10
11
  failures = [];
11
- /**
12
- * The session's accepted history, one envelope per unique `message_id`. See
13
- * `BaseProjection.transcript` (`src/projections/base.ts`) for the full
14
- * redelivery-idempotence contract (RFC-MACP-0006 §3.2); duplicated here
15
- * only as a one-line pointer.
16
- */
17
- transcript = [];
18
- /**
19
- * Cardinality anomalies recorded while replaying this projection's accepted
20
- * transcript. See `BaseProjection.anomalies` (`src/projections/base.ts`)
21
- * for the canonical description; duplicated here only as a one-line
22
- * pointer. No built-in detection populates this for Task mode.
23
- */
24
- anomalies = [];
25
12
  phase = 'Pending';
26
- commitment;
27
- /**
28
- * `message_id`s already applied to this projection. See
29
- * `BaseProjection.seenMessageIds` (`src/projections/base.ts`) for the full
30
- * redelivery-idempotence rationale (RFC-MACP-0006 §3.2); duplicated here
31
- * only as a one-line pointer.
32
- */
33
- seenMessageIds = new Set();
34
13
  /**
35
- * Apply one envelope to this projection's in-process state.
14
+ * The **session-level** active assignment: who currently holds the session's
15
+ * single assignee slot, and which `task_id` they took it on.
36
16
  *
37
- * Input contract: **accepted-only**, caller-maintained (`Envelope` carries
38
- * no acceptance marker). Canonical source: `schemas/conformance/README.md`
39
- * "Notes:", RFC-MACP-0007 §5.3, RFC-MACP-0011 §5. Full rationale and failure
40
- * mode are documented once, on `BaseProjection.applyEnvelope`
41
- * (`src/projections/base.ts`), and duplicated here only as a one-line
42
- * pointer so six independent copies of the same prose cannot drift.
17
+ * RFC-MACP-0009 §5 rule 3 (`:69`): "Only one assignee may become active for
18
+ * the Session in base v1." The constraint is scoped to the *Session*, not to
19
+ * a `task_id` — rule 1 (`:67`) separately caps a v1 session at one
20
+ * `TaskRequest`, so in a conforming transcript the two scopes coincide, but
21
+ * an unfiltered transcript carrying two `TaskRequest`s must not be able to
22
+ * hand out two active assignees. That is why this is one field on the
23
+ * projection rather than a per-`TaskRecord` flag (issue #71).
43
24
  *
44
- * Redelivery idempotence: a redelivered envelope (same `message_id`) is a
45
- * non-event — not appended to `transcript`, not passed to the `switch`.
46
- * See `BaseProjection.applyEnvelope` for the full RFC-MACP-0006 §3.2
47
- * citation; duplicated here only as a one-line pointer.
25
+ * `sender` is the envelope sender, not `TaskAcceptPayload.assignee`, to
26
+ * mirror the reference runtime: `macp-runtime`
27
+ * `crates/macp-modes/src/mode/task.rs:211` sets `state.active_assignee =
28
+ * Some(env.sender)` after rejecting any payload whose `assignee` disagrees
29
+ * with the sender (`:205-207`). `taskId` is retained so a reject can clear
30
+ * the right `TaskRecord.assignee` even on a multi-task transcript.
48
31
  */
49
- applyEnvelope(envelope, protoRegistry) {
50
- if (envelope.mode !== constants_1.MODE_TASK)
51
- return;
52
- if (envelope.messageId) {
53
- if (this.seenMessageIds.has(envelope.messageId)) {
54
- logging_1.logger.debug('projection redelivery ignored', {
55
- messageId: envelope.messageId,
56
- mode: envelope.mode,
57
- messageType: envelope.messageType,
58
- });
59
- return;
60
- }
61
- this.seenMessageIds.add(envelope.messageId);
62
- }
63
- this.transcript.push(envelope);
32
+ activeAssignment;
33
+ /** Handle a Task-mode envelope. See `BaseProjection.applyEnvelope` for the accepted-only input contract and redelivery/rollback semantics shared by all built-in modes. */
34
+ applyMode(envelope, protoRegistry) {
64
35
  const payload = protoRegistry.decodeKnownPayload(envelope.mode, envelope.messageType, envelope.payload);
65
36
  switch (envelope.messageType) {
66
37
  case 'TaskRequest': {
@@ -81,46 +52,72 @@ class TaskProjection {
81
52
  case 'TaskAccept': {
82
53
  const record = payload;
83
54
  const task = this.tasks.get(record.taskId);
84
- // RFC-MACP-0009 §5 rules 3/3a (`:69-71`): "Only one assignee may
55
+ // RFC-MACP-0009 §5 rules 3/3a (`:69-70`): "Only one assignee may
85
56
  // become active for the Session in base v1. The first accepted
86
57
  // `TaskAccept` from any eligible participant designates that
87
58
  // participant as the active assignee. Subsequent `TaskAccept`
88
59
  // messages for the same session MUST be rejected if an active
89
60
  // assignee is already designated." A conforming runtime rejects the
90
- // second `TaskAccept` before it reaches accepted history; this guard
91
- // makes the projection first-accept-wins too, so a rogue second one
92
- // (e.g. an unfiltered transcript) cannot silently reassign the task.
61
+ // second `TaskAccept` before it reaches accepted history
62
+ // (`macp-runtime` `crates/macp-modes/src/mode/task.rs:184-186`); this
63
+ // guard makes the projection first-accept-wins too, so a rogue second
64
+ // one (e.g. an unfiltered transcript) cannot silently reassign.
65
+ //
66
+ // The guard is keyed on the SESSION slot (`activeAssignment`), not on
67
+ // `task.assignee`, because rule 3's scope is the Session (issue #71).
68
+ // Two `TaskRequest`s in one transcript therefore share one assignee
69
+ // slot, matching the runtime, whose `TaskState` holds a single
70
+ // `active_assignee` (`task.rs:70`) alongside a single `task`.
93
71
  //
94
- // Rule 3c's policy-gated reassignment path (`allow_reassignment_on_reject`,
95
- // RFC-MACP-0012 `:135`) is deliberately NOT modelled here:
96
- // `TaskProjection` has no session-policy input today, and rule 3b
97
- // makes an unconditional reassignment wrong by default ("`TaskAccept`
98
- // is irrevocable unless policy explicitly permits reassignment").
99
- // Plumbing policy into this projection needs its own decision (see
100
- // plans/cross-repo/macp-sdk-typescript-conformance-and-transport.md,
101
- // Phase 3 open question 4) before that path can be implemented.
72
+ // Rule 3c's policy-gated reassignment (`allow_reassignment_on_reject`,
73
+ // RFC-MACP-0012 `:135`) is reachable here via the `TaskReject` case
74
+ // below, which frees the slot — see its comment for why that needs no
75
+ // policy input.
102
76
  //
103
77
  // An anomaly would be recorded when this guard discards a second
104
78
  // `TaskAccept`, but `ProjectionAnomalyKind` (`base.ts:8-9`) is
105
79
  // deliberately frozen pending cross-SDK agreement with
106
80
  // macp-sdk-python.
107
- if (task && task.assignee === undefined) {
81
+ if (task && this.activeAssignment === undefined) {
108
82
  task.assignee = record.assignee;
109
83
  task.status = 'accepted';
84
+ this.activeAssignment = { sender: envelope.sender, taskId: record.taskId };
85
+ this.phase = 'InProgress';
110
86
  }
111
- this.phase = 'InProgress';
112
87
  break;
113
88
  }
114
89
  case 'TaskReject': {
115
90
  const record = payload;
116
91
  const task = this.tasks.get(record.taskId);
117
- // Deliberately does not clear `task.assignee`: rule 3c's
118
- // policy-gated reassignment path is not modelled here (see the
119
- // `TaskAccept` case above), so `assignee` is treated as sticky once
120
- // set, per rule 3b's default ("irrevocable unless policy explicitly
121
- // permits reassignment").
122
92
  if (task)
123
93
  task.status = 'rejected';
94
+ // RFC-MACP-0009 §5 rule 3c (`:72`): "When policy sets
95
+ // `allow_reassignment_on_reject: true` and the active assignee sends
96
+ // `TaskReject`, the session returns to the pre-assignment state. Other
97
+ // eligible participants MAY then send `TaskAccept` for the same
98
+ // `task_id`." Mirrors `macp-runtime`
99
+ // `crates/macp-modes/src/mode/task.rs:257-260`, which clears
100
+ // `active_assignee` only when the ACTIVE assignee is the rejecter —
101
+ // a reject from anyone else never frees the slot (`task.rs:241-244`
102
+ // rejects it as `InvalidPayload`).
103
+ //
104
+ // Deliberately NOT gated on the session policy, and it does not need
105
+ // to be. The runtime gates the reassignment twice — it denies the
106
+ // active assignee's `TaskReject` (`task.rs:223-239`) and the follow-up
107
+ // `TaskAccept` (`task.rs:187-204`) with `PolicyDenied` when
108
+ // `allow_reassignment_on_reject` is false — and a projection is a view
109
+ // of history the runtime ALREADY ACCEPTED (see this method's
110
+ // accepted-only input contract). A reassignment the runtime denied
111
+ // never reaches the transcript, so tracking the reject unconditionally
112
+ // cannot diverge from the runtime on any real transcript, and it needs
113
+ // no new API surface (threading a `PolicyDefinition` into the
114
+ // constructor would). Issue #70, option 2.
115
+ if (this.activeAssignment?.sender === envelope.sender) {
116
+ const held = this.tasks.get(this.activeAssignment.taskId);
117
+ if (held)
118
+ held.assignee = undefined;
119
+ this.activeAssignment = undefined;
120
+ }
124
121
  break;
125
122
  }
126
123
  case 'TaskUpdate': {
@@ -153,33 +150,10 @@ class TaskProjection {
153
150
  this.phase = 'Failed';
154
151
  break;
155
152
  }
156
- case 'Commitment': {
157
- this.commitment = payload;
158
- this.phase = 'Committed';
159
- break;
160
- }
161
153
  default:
162
154
  break;
163
155
  }
164
156
  }
165
- /**
166
- * True once at least one `ProjectionAnomaly` has been recorded. See
167
- * `BaseProjection.hasAnomalies` (`src/projections/base.ts`) for the
168
- * canonical description; duplicated here only as a one-line pointer.
169
- */
170
- get hasAnomalies() {
171
- return this.anomalies.length > 0;
172
- }
173
- get isCommitted() {
174
- return this.commitment !== undefined;
175
- }
176
- get isPositiveOutcome() {
177
- if (!this.commitment)
178
- return undefined;
179
- const val = this.commitment.outcomePositive ??
180
- this.commitment.outcome_positive;
181
- return val !== undefined ? Boolean(val) : true;
182
- }
183
157
  getTask(taskId) {
184
158
  return this.tasks.get(taskId);
185
159
  }
@@ -10,11 +10,25 @@ export declare class ProtoRegistry {
10
10
  /**
11
11
  * Decode an `ext.multi_round.v1` `Contribute` payload, accepting both wire
12
12
  * formats: legacy JSON (`{"value":"..."}`, still replayed verbatim from
13
- * pre-proto histories) and canonical protobuf (`ContributePayload`). JSON is
14
- * tried first — mirroring the runtime's permanent decode order — and the two
15
- * encodings are disjoint on their first byte (`{` = 0x7b vs proto field-1
16
- * tag 0x0A), so a canonical proto payload never mis-parses as JSON and vice
17
- * versa. Both normalize to `{ value: string }`.
13
+ * pre-proto histories) and canonical protobuf (`ContributePayload`). Both
14
+ * normalize to `{ value: string }`.
15
+ *
16
+ * Parse-then-fallback (issue #93), not a first-byte shortcut: JSON is always
17
+ * attempted first, and only a `JSON.parse` failure falls through to
18
+ * protobuf. A first-byte check (`{` = 0x7b vs proto field-1 tag 0x0A) does
19
+ * not generalize — e.g. legacy JSON with leading whitespace has neither byte
20
+ * first, and used to reach `decodeMessage` and throw. Parity with
21
+ * `macp-sdk-python`'s `_decode_json_first_then_proto`, which has always
22
+ * worked this way. Safe because a canonical proto payload's bytes are
23
+ * essentially never also syntactically valid JSON.
24
+ *
25
+ * An empty payload falls through the same path: `JSON.parse('')` throws, so
26
+ * it reaches `decodeMessage` on zero bytes, which yields `{}` (proto3
27
+ * defaults) rather than a decode error. This SDK's decode layer is
28
+ * observational, not an acceptance gate — whether an empty `Contribute`
29
+ * should be rejected is a runtime-acceptance question (tracked cross-repo,
30
+ * `contribute_acceptance.empty_payload` in `schemas/parity/contract.json`,
31
+ * currently `macp-runtime`-only), not something this method decides.
18
32
  */
19
33
  private decodeMultiRoundContribute;
20
34
  private lookupType;
@@ -169,26 +169,34 @@ class ProtoRegistry {
169
169
  /**
170
170
  * Decode an `ext.multi_round.v1` `Contribute` payload, accepting both wire
171
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 }`.
172
+ * pre-proto histories) and canonical protobuf (`ContributePayload`). Both
173
+ * normalize to `{ value: string }`.
174
+ *
175
+ * Parse-then-fallback (issue #93), not a first-byte shortcut: JSON is always
176
+ * attempted first, and only a `JSON.parse` failure falls through to
177
+ * protobuf. A first-byte check (`{` = 0x7b vs proto field-1 tag 0x0A) does
178
+ * not generalize — e.g. legacy JSON with leading whitespace has neither byte
179
+ * first, and used to reach `decodeMessage` and throw. Parity with
180
+ * `macp-sdk-python`'s `_decode_json_first_then_proto`, which has always
181
+ * worked this way. Safe because a canonical proto payload's bytes are
182
+ * essentially never also syntactically valid JSON.
183
+ *
184
+ * An empty payload falls through the same path: `JSON.parse('')` throws, so
185
+ * it reaches `decodeMessage` on zero bytes, which yields `{}` (proto3
186
+ * defaults) rather than a decode error. This SDK's decode layer is
187
+ * observational, not an acceptance gate — whether an empty `Contribute`
188
+ * should be rejected is a runtime-acceptance question (tracked cross-repo,
189
+ * `contribute_acceptance.empty_payload` in `schemas/parity/contract.json`,
190
+ * currently `macp-runtime`-only), not something this method decides.
177
191
  */
178
192
  decodeMultiRoundContribute(payload) {
179
- if (!payload.length)
193
+ try {
194
+ const parsed = JSON.parse(payload.toString('utf8'));
195
+ return { value: typeof parsed.value === 'string' ? parsed.value : String(parsed.value ?? '') };
196
+ }
197
+ catch {
180
198
  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
199
  }
191
- return this.decodeMessage(MULTI_ROUND_CONTRIBUTE, payload);
192
200
  }
193
201
  lookupType(typeName) {
194
202
  const lookedUp = this.root.lookupType(typeName);
@@ -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.9.0";
7
+ export declare const VERSION = "0.11.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.9.0'; // x-release-please-version
10
+ exports.VERSION = '0.11.0'; // x-release-please-version
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "macp-sdk-typescript",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "TypeScript SDK for the MACP Rust runtime",
5
5
  "license": "Apache-2.0",
6
6
  "type": "commonjs",
@@ -55,18 +55,18 @@
55
55
  "dependencies": {
56
56
  "@grpc/grpc-js": "^1.14.3",
57
57
  "@grpc/proto-loader": "^0.8.0",
58
- "@multiagentcoordinationprotocol/proto": "^0.1.9",
58
+ "@multiagentcoordinationprotocol/proto": "^0.1.10",
59
59
  "protobufjs": "^8.6.5"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@types/node": "^26.4.0",
63
63
  "@typescript-eslint/eslint-plugin": "^8.68.0",
64
64
  "@typescript-eslint/parser": "^8.68.0",
65
- "@vitest/coverage-v8": "^4.1.11",
65
+ "@vitest/coverage-v8": "^5.0.0",
66
66
  "eslint": "^10.9.1",
67
67
  "prettier": "^3.0.0",
68
68
  "tsx": "^4.21.0",
69
69
  "typescript": "^6.0.3",
70
- "vitest": "^4.1.11"
70
+ "vitest": "^5.0.0"
71
71
  }
72
72
  }