macp-sdk-typescript 0.13.0 → 0.14.1
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 +117 -0
- package/dist/agent/runner.d.ts +23 -1
- package/dist/agent/runner.js +57 -8
- package/dist/policy.js +7 -3
- package/dist/projections/base.d.ts +20 -3
- package/dist/projections/base.js +18 -0
- package/dist/projections/decision.js +22 -15
- package/dist/projections/handoff.js +4 -4
- package/dist/projections/proposal.d.ts +31 -1
- package/dist/projections/proposal.js +19 -2
- package/dist/projections/quorum.js +19 -16
- package/dist/projections/task.d.ts +7 -0
- package/dist/projections/task.js +15 -4
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/watchers.js +5 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,88 @@ 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.14.1](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/compare/v0.14.0...v0.14.1) (2026-10-03)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Bug Fixes
|
|
11
|
+
|
|
12
|
+
* **parity:** re-vendor contract.json for contract_version 1.3.0 ([#156](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/156)) ([#157](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/157)) ([497949d](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/commit/497949d2b29031d340f5d8cc2ef17ae34c946c42))
|
|
13
|
+
|
|
14
|
+
## [Unreleased]
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **`TaskProjection.rejections: TaskRejectRecord[]`** (issue #151; a
|
|
19
|
+
cross-SDK parity audit against `macp-sdk-python`'s `TaskRejectRecord`,
|
|
20
|
+
`src/macp_sdk/task.py:53-58`). `TaskReject` now appends an unconditional
|
|
21
|
+
audit record — `taskId`, `assignee`, `reason?`, `sender` (from the
|
|
22
|
+
envelope, not the payload) — matching the convention already established
|
|
23
|
+
for `updates`/`completions`/`failures`. A `TaskReject` for an unknown
|
|
24
|
+
`taskId` still records a `rejections` entry (no crash, no silent drop) but
|
|
25
|
+
creates no `tasks` entry, unchanged from today. Deliberately does not fall
|
|
26
|
+
back `assignee` to the envelope sender the way Python's `p.assignee or
|
|
27
|
+
envelope.sender` does — this SDK's own sibling `TaskComplete`/`TaskFail`
|
|
28
|
+
handlers spread the payload's `assignee` as-is with no such fallback, and
|
|
29
|
+
intra-SDK consistency with that established pattern takes precedence here.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- **`BaseProjection` phase no longer regresses once `Committed`, and
|
|
34
|
+
`DecisionProjection` no longer lets a replayed `Proposal` rewind phase
|
|
35
|
+
from `Voting` back to `Evaluation` after voting has begun** (issue #150,
|
|
36
|
+
issue #153; found by a cross-SDK parity audit against `macp-sdk-python`'s
|
|
37
|
+
`_set_phase()`, `src/macp_sdk/base_projection.py:289-313`). All five
|
|
38
|
+
built-in mode projections now route every phase write through a new
|
|
39
|
+
shared, protected `setPhase()` choke point on `BaseProjection` that
|
|
40
|
+
no-ops once `phase === 'Committed'`, instead of assigning `this.phase`
|
|
41
|
+
directly at each of the (previously) 12 call sites across
|
|
42
|
+
`decision.ts`/`proposal.ts`/`task.ts`/`quorum.ts`/`handoff.ts` — only
|
|
43
|
+
`decision.ts`'s own Vote-branch guard protected this before. RFC-MACP-0007
|
|
44
|
+
§5 rule 6 separately requires a runtime to reject any
|
|
45
|
+
`Proposal`/`Evaluation`/`Objection` after the first accepted `Vote`;
|
|
46
|
+
`DecisionProjection`'s `Proposal` handler now guards on the pre-
|
|
47
|
+
`Evaluation` phase value (`if (this.phase === 'Proposal')
|
|
48
|
+
this.setPhase('Evaluation')`) so a replayed/retried `Proposal` (same
|
|
49
|
+
`proposalId`, a distinct `messageId` that still passes
|
|
50
|
+
`applyEnvelope`'s `message_id` dedup gate) can no longer rewind `phase`
|
|
51
|
+
back to `'Evaluation'` once voting has begun. `BaseProjection.
|
|
52
|
+
applyEnvelope`'s own direct `this.phase = 'Committed'` assignment for a
|
|
53
|
+
`Commitment` envelope is deliberately left untouched — exact parity with
|
|
54
|
+
Python's own `_set_phase()` docstring, which documents the same bypass.
|
|
55
|
+
- **`buildDecisionPolicy` now rejects a `+Infinity` voting weight** (issue
|
|
56
|
+
#152; found by a cross-SDK parity audit against `macp-sdk-python`'s
|
|
57
|
+
`math.isfinite` guard, `src/macp_sdk/policy.py:186`). The per-weight
|
|
58
|
+
validation guard changed from `weight <= 0 || Number.isNaN(weight)` to
|
|
59
|
+
`weight <= 0 || !Number.isFinite(weight)` — `!Number.isFinite` is `true`
|
|
60
|
+
for `NaN`, `+Infinity`, and `-Infinity` alike, closing the `+Infinity` gap
|
|
61
|
+
the old `Number.isNaN`-only check missed (a `+Infinity` weight serializes
|
|
62
|
+
to JSON `null`, which is schema-invalid against
|
|
63
|
+
`decision-rules.schema.json`). `-Infinity`/`NaN` continue to throw,
|
|
64
|
+
unchanged.
|
|
65
|
+
- **`watchers.ts`'s 5 "stream ended before receiving a ..." sites now throw
|
|
66
|
+
`MacpTransportError` instead of a bare `Error`** (issue #154; parity with
|
|
67
|
+
`macp-sdk-python` PR #138). `ModeRegistryWatcher.nextChange()`,
|
|
68
|
+
`RootsWatcher.nextChange()`, `SignalWatcher.nextSignal()`,
|
|
69
|
+
`PolicyWatcher.nextChange()`, and `SessionLifecycleWatcher.nextChange()`
|
|
70
|
+
each threw a bare `Error` when their stream ended before yielding a value;
|
|
71
|
+
a caller's single `catch (e) { if (e instanceof MacpSdkError) ... }` now
|
|
72
|
+
covers a cleanly-ended watch stream alongside every other SDK failure.
|
|
73
|
+
Message strings are unchanged byte-for-byte; `error.code` remains
|
|
74
|
+
`undefined` on all 5 (locally raised, no gRPC status to attach).
|
|
75
|
+
|
|
76
|
+
## [0.14.0](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/compare/v0.13.0...v0.14.0) (2026-10-01)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
### ⚠ BREAKING CHANGES
|
|
80
|
+
|
|
81
|
+
* **agent:** encodeExtensions() (src/agent/runner.ts) no longer JSON-stringifies a session_start.extensions value. A string value is now base64-decoded (falling back to raw UTF-8 if it isn't valid base64); a number, boolean, plain object, array, or null value now throws instead of being silently JSON-encoded, as does a present-but-non-object extensions map itself. Buffer/Uint8Array values are unaffected. This reverses the UTF-8-JSON-encoding behavior documented in the 0.2.3 (SDK-TS-1) CHANGELOG entry, which silently put wire bytes on the connection that a conforming peer cannot decode.
|
|
82
|
+
* **projections:** ProposalRecord.status's type no longer accepts 'accepted'. A TypeScript consumer comparing status === 'accepted' or switching on it now gets a compile-time TS2367/TS2678/TS2322 instead of silently-dead code; a plain-JS (or `as any`-cast) caller sees no behavior change, since the comparison always evaluated false.
|
|
83
|
+
|
|
84
|
+
### Bug Fixes
|
|
85
|
+
|
|
86
|
+
* **agent:** encodeExtensions() base64-decodes extensions values ([#149](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/149)) ([72c6ea4](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/commit/72c6ea484e1dd5e75dbfc0bd12cbed787eeeacbd))
|
|
87
|
+
* **projections:** ProposalRecord.status no longer accepts 'accepted' ([#147](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/issues/147)) ([c5142d7](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/commit/c5142d7c1ccd37b128549e512e9d5c6436899e30))
|
|
88
|
+
|
|
7
89
|
## [0.13.0](https://github.com/multiagentcoordinationprotocol/macp-sdk-typescript/compare/v0.12.0...v0.13.0) (2026-09-30)
|
|
8
90
|
|
|
9
91
|
|
|
@@ -131,6 +213,30 @@ project uses [Semantic Versioning](https://semver.org/).
|
|
|
131
213
|
still only fails server-side). A caller that genuinely needs one
|
|
132
214
|
credential reused across multiple senders should use
|
|
133
215
|
`Auth.bearer(token, senderHint)` (the legacy two-arg form) instead.
|
|
216
|
+
* **projections:** `ProposalRecord.status`'s type no longer accepts
|
|
217
|
+
`'accepted'` (issue #146). No code path has ever assigned it — acceptance
|
|
218
|
+
is tracked separately via `accepts`/`isAccepted()`, since it's a
|
|
219
|
+
per-sender, supersedable relation (RFC-MACP-0008 §5 rule 5), not a
|
|
220
|
+
per-proposal fact a scalar field could hold. A TypeScript consumer
|
|
221
|
+
comparing `status === 'accepted'` or switching on it now gets a
|
|
222
|
+
compile-time `TS2367`/`TS2678`/`TS2322` instead of silently-dead code; a
|
|
223
|
+
plain-JS (or `as any`-cast) caller sees no behavior change, since the
|
|
224
|
+
comparison always evaluated `false`.
|
|
225
|
+
* **agent:** `fromBootstrap()`'s `encodeExtensions()` now base64-decodes a
|
|
226
|
+
`session_start.extensions` string value (falling back to raw UTF-8 for a
|
|
227
|
+
non-base64 string), instead of `JSON.stringify`-ing it, per RFC-MACP-0001
|
|
228
|
+
§10.3 (a protobuf `bytes` field's canonical JSON representation is
|
|
229
|
+
base64) — the wire-level `extensions` map is `map<string, bytes>` (issue
|
|
230
|
+
#139). This reverses the UTF-8-JSON-encoding behavior documented at the
|
|
231
|
+
`0.2.3` entry above (SDK-TS-1): that release's choice silently put wire
|
|
232
|
+
bytes on the connection that a conforming peer (including this SDK's own
|
|
233
|
+
`macp-sdk-python` counterpart) cannot decode, even for the canonical
|
|
234
|
+
example value. A number, boolean, plain object, array, or `null`
|
|
235
|
+
extension value — previously silently JSON-encoded — now throws at
|
|
236
|
+
bootstrap construction instead, naming the offending key; so does a
|
|
237
|
+
present-but-non-object `extensions` map itself. `Buffer`/`Uint8Array`
|
|
238
|
+
values are unaffected (same passthrough as before, for TS-only
|
|
239
|
+
in-process callers — never reachable via a JSON bootstrap file).
|
|
134
240
|
|
|
135
241
|
### Features
|
|
136
242
|
|
|
@@ -191,6 +297,12 @@ project uses [Semantic Versioning](https://semver.org/).
|
|
|
191
297
|
sites. `DecisionProjection`'s late-`Vote`-after-`Commitment` site remains
|
|
192
298
|
the one still-open, undecided case (per #128's own explicit scoping) —
|
|
193
299
|
unaffected by this change.
|
|
300
|
+
- **`encodeExtensions()` is now exported from `macp-sdk-typescript/agent`**
|
|
301
|
+
(issue #139), so a caller constructing a bootstrap `extensions` map
|
|
302
|
+
programmatically can validate/encode a value the same way
|
|
303
|
+
`fromBootstrap()` does, and so it's directly unit-testable (a
|
|
304
|
+
`Buffer`/`Uint8Array` value can't round-trip through a JSON bootstrap
|
|
305
|
+
file otherwise).
|
|
194
306
|
|
|
195
307
|
### Changed
|
|
196
308
|
|
|
@@ -466,6 +578,11 @@ project uses [Semantic Versioning](https://semver.org/).
|
|
|
466
578
|
site — `taskId`, `title`, `requestId`, `proposalId`, `option`, `handoffId`,
|
|
467
579
|
`targetParticipant`, `modeVersion`, `configurationVersion` — is unaffected
|
|
468
580
|
and still rejects an empty value.
|
|
581
|
+
- **`ProposalRecord.status`'s declared type no longer lies about
|
|
582
|
+
`'accepted'`** (issue #146). The union included `'accepted'` since this
|
|
583
|
+
type was first written, but nothing has ever assigned it — see the
|
|
584
|
+
`### ⚠ BREAKING CHANGES` entry above for why, and why that correction is a
|
|
585
|
+
breaking (not purely additive) change.
|
|
469
586
|
|
|
470
587
|
### Documentation
|
|
471
588
|
|
package/dist/agent/runner.d.ts
CHANGED
|
@@ -24,7 +24,7 @@ export interface BootstrapPayload {
|
|
|
24
24
|
policy_version?: string;
|
|
25
25
|
context?: Record<string, unknown>;
|
|
26
26
|
context_id?: string;
|
|
27
|
-
extensions?: Record<string,
|
|
27
|
+
extensions?: Record<string, string | Buffer | Uint8Array>;
|
|
28
28
|
roots?: Array<{
|
|
29
29
|
uri: string;
|
|
30
30
|
name?: string;
|
|
@@ -50,3 +50,25 @@ export interface BootstrapPayload {
|
|
|
50
50
|
};
|
|
51
51
|
}
|
|
52
52
|
export declare function fromBootstrap(bootstrapPath?: string): Participant;
|
|
53
|
+
/**
|
|
54
|
+
* Coerce a bootstrap `session_start.extensions` map into the
|
|
55
|
+
* `Record<string, Buffer>` form the SessionStart envelope expects
|
|
56
|
+
* (`map<string, bytes>` on the wire).
|
|
57
|
+
*
|
|
58
|
+
* Per RFC-MACP-0001 §10.3, a protobuf `bytes` field's canonical JSON
|
|
59
|
+
* representation is a base64 string, so a `string` value is base64-decoded
|
|
60
|
+
* first (strict shape check via `BASE64_PATTERN`, not Node's lenient
|
|
61
|
+
* decoder); a string that isn't valid base64 falls back to raw UTF-8 bytes.
|
|
62
|
+
* This base64-first heuristic — and the resulting ambiguity for a plain
|
|
63
|
+
* string that happens to also be valid base64 — mirrors
|
|
64
|
+
* `macp-sdk-python`'s `_decode_extensions` on purpose, to stay
|
|
65
|
+
* byte-for-byte interoperable; it is a known, tracked wart, not fixed here.
|
|
66
|
+
* `Buffer`/`Uint8Array` values pass through unchanged, for TS-only
|
|
67
|
+
* in-process callers that already hold raw bytes (never reachable via a
|
|
68
|
+
* JSON bootstrap file). Anything else — including the `extensions`
|
|
69
|
+
* container itself being a non-object (string/number/boolean/array) —
|
|
70
|
+
* throws, naming the offending key and/or type, so a malformed bootstrap
|
|
71
|
+
* fails loudly at construction instead of silently corrupting wire bytes a
|
|
72
|
+
* peer can't decode (issue #139).
|
|
73
|
+
*/
|
|
74
|
+
export declare function encodeExtensions(extensions: Record<string, string | Buffer | Uint8Array> | undefined): Record<string, Buffer> | undefined;
|
package/dist/agent/runner.js
CHANGED
|
@@ -34,11 +34,13 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.fromBootstrap = fromBootstrap;
|
|
37
|
+
exports.encodeExtensions = encodeExtensions;
|
|
37
38
|
const fs = __importStar(require("node:fs"));
|
|
38
39
|
const path = __importStar(require("node:path"));
|
|
39
40
|
const auth_1 = require("../auth");
|
|
40
41
|
const client_1 = require("../client");
|
|
41
42
|
const constants_1 = require("../constants");
|
|
43
|
+
const logging_1 = require("../logging");
|
|
42
44
|
const participant_1 = require("./participant");
|
|
43
45
|
function fromBootstrap(bootstrapPath) {
|
|
44
46
|
const resolvedPath = bootstrapPath ?? process.env.MACP_BOOTSTRAP_FILE;
|
|
@@ -111,17 +113,53 @@ function fromBootstrap(bootstrapPath) {
|
|
|
111
113
|
};
|
|
112
114
|
return new participant_1.Participant(config);
|
|
113
115
|
}
|
|
116
|
+
// Matches a syntactically valid base64 string (correct alphabet, correct
|
|
117
|
+
// padding) — a cheap stand-in for Python's `base64.b64decode(value,
|
|
118
|
+
// validate=True)` so a plainly non-base64 string (e.g. "pack:example-001")
|
|
119
|
+
// is rejected up front rather than silently mis-decoded by Node's lenient
|
|
120
|
+
// `Buffer.from(str, 'base64')`, which ignores invalid characters instead of
|
|
121
|
+
// raising. Matches the empty string too (decodes to an empty buffer either
|
|
122
|
+
// way, so which branch handles it is immaterial).
|
|
123
|
+
const BASE64_PATTERN = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
124
|
+
function tryDecodeBase64(value) {
|
|
125
|
+
if (!BASE64_PATTERN.test(value))
|
|
126
|
+
return undefined;
|
|
127
|
+
return Buffer.from(value, 'base64');
|
|
128
|
+
}
|
|
129
|
+
function describeExtensionsValueType(value) {
|
|
130
|
+
if (value === null)
|
|
131
|
+
return 'null';
|
|
132
|
+
if (Array.isArray(value))
|
|
133
|
+
return 'array';
|
|
134
|
+
return typeof value;
|
|
135
|
+
}
|
|
114
136
|
/**
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
137
|
+
* Coerce a bootstrap `session_start.extensions` map into the
|
|
138
|
+
* `Record<string, Buffer>` form the SessionStart envelope expects
|
|
139
|
+
* (`map<string, bytes>` on the wire).
|
|
140
|
+
*
|
|
141
|
+
* Per RFC-MACP-0001 §10.3, a protobuf `bytes` field's canonical JSON
|
|
142
|
+
* representation is a base64 string, so a `string` value is base64-decoded
|
|
143
|
+
* first (strict shape check via `BASE64_PATTERN`, not Node's lenient
|
|
144
|
+
* decoder); a string that isn't valid base64 falls back to raw UTF-8 bytes.
|
|
145
|
+
* This base64-first heuristic — and the resulting ambiguity for a plain
|
|
146
|
+
* string that happens to also be valid base64 — mirrors
|
|
147
|
+
* `macp-sdk-python`'s `_decode_extensions` on purpose, to stay
|
|
148
|
+
* byte-for-byte interoperable; it is a known, tracked wart, not fixed here.
|
|
149
|
+
* `Buffer`/`Uint8Array` values pass through unchanged, for TS-only
|
|
150
|
+
* in-process callers that already hold raw bytes (never reachable via a
|
|
151
|
+
* JSON bootstrap file). Anything else — including the `extensions`
|
|
152
|
+
* container itself being a non-object (string/number/boolean/array) —
|
|
153
|
+
* throws, naming the offending key and/or type, so a malformed bootstrap
|
|
154
|
+
* fails loudly at construction instead of silently corrupting wire bytes a
|
|
155
|
+
* peer can't decode (issue #139).
|
|
121
156
|
*/
|
|
122
157
|
function encodeExtensions(extensions) {
|
|
123
|
-
if (
|
|
158
|
+
if (extensions === undefined || extensions === null)
|
|
124
159
|
return undefined;
|
|
160
|
+
if (typeof extensions !== 'object' || Array.isArray(extensions)) {
|
|
161
|
+
throw new Error(`bootstrap session_start.extensions must be an object, got ${describeExtensionsValueType(extensions)}`);
|
|
162
|
+
}
|
|
125
163
|
const out = {};
|
|
126
164
|
for (const [key, value] of Object.entries(extensions)) {
|
|
127
165
|
if (Buffer.isBuffer(value)) {
|
|
@@ -130,8 +168,19 @@ function encodeExtensions(extensions) {
|
|
|
130
168
|
else if (value instanceof Uint8Array) {
|
|
131
169
|
out[key] = Buffer.from(value);
|
|
132
170
|
}
|
|
171
|
+
else if (typeof value === 'string') {
|
|
172
|
+
const decoded = tryDecodeBase64(value);
|
|
173
|
+
if (decoded !== undefined) {
|
|
174
|
+
logging_1.logger.debug(`[fromBootstrap] extensions["${key}"] decoded as base64`);
|
|
175
|
+
out[key] = decoded;
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
logging_1.logger.debug(`[fromBootstrap] extensions["${key}"] is not valid base64; falling back to raw UTF-8`);
|
|
179
|
+
out[key] = Buffer.from(value, 'utf8');
|
|
180
|
+
}
|
|
181
|
+
}
|
|
133
182
|
else {
|
|
134
|
-
|
|
183
|
+
throw new Error(`bootstrap extensions["${key}"] must be a base64-encoded string (or Buffer/Uint8Array), got ${describeExtensionsValueType(value)}`);
|
|
135
184
|
}
|
|
136
185
|
}
|
|
137
186
|
return out;
|
package/dist/policy.js
CHANGED
|
@@ -105,9 +105,13 @@ function buildDecisionPolicy(policyId, description, rules, options) {
|
|
|
105
105
|
}
|
|
106
106
|
for (const [participant, weight] of Object.entries(weights)) {
|
|
107
107
|
// weight <= 0 alone would let a NaN weight slip through (NaN <= 0 is
|
|
108
|
-
// false); the runtime guards this explicitly (registry.rs:596)
|
|
109
|
-
//
|
|
110
|
-
|
|
108
|
+
// false); the runtime guards this explicitly (registry.rs:596).
|
|
109
|
+
// !Number.isFinite(weight) is true for NaN, +Infinity, and -Infinity
|
|
110
|
+
// alike -- the exact mirror of Python's own guard, policy.py:186:
|
|
111
|
+
// `if not math.isfinite(weight) or weight <= 0:` (issue #152; a
|
|
112
|
+
// +Infinity weight previously slipped past the old Number.isNaN-only
|
|
113
|
+
// check and serialized to JSON `null`, which is schema-invalid).
|
|
114
|
+
if (weight <= 0 || !Number.isFinite(weight)) {
|
|
111
115
|
throw new errors_1.MacpSessionError(`weights['${participant}'] must be > 0, got ${weight} -- a weight-0 observer is expressed by ` +
|
|
112
116
|
'omission from the map, not by an explicit 0');
|
|
113
117
|
}
|
|
@@ -14,9 +14,12 @@ export type ProjectionAnomalyKind = 'duplicate_vote' | 'duplicate_ballot' | 'dup
|
|
|
14
14
|
* this proposal (RFC-MACP-0007 §5.3), or a second distinct ballot across
|
|
15
15
|
* `Approve`/`Reject`/`Abstain` from this sender for this request
|
|
16
16
|
* (RFC-MACP-0011 §5 rule 3), was observed and discarded — the first stands.
|
|
17
|
-
* (RFC-MACP-0011 §5 rule 3
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* (RFC-MACP-0011 §5 rule 3 states "the first accepted ballot stands"
|
|
18
|
+
* directly, since the rule-3 hardening in spec PR
|
|
19
|
+
* multiagentcoordinationprotocol#85 — before that, this SDK's
|
|
20
|
+
* first-ballot-wins behaviour was an inference from parity with
|
|
21
|
+
* RFC-MACP-0007 §5.3 plus runtime-enforced behaviour, not a direct
|
|
22
|
+
* RFC-0011 citation.)
|
|
20
23
|
*
|
|
21
24
|
* **Deliberately narrow claim — do not overclaim "this transcript violates
|
|
22
25
|
* the spec".** A projection cannot tell a genuinely non-conforming source
|
|
@@ -223,4 +226,18 @@ export declare abstract class BaseProjection {
|
|
|
223
226
|
* `MACP_LOG_LEVEL` environment variable.
|
|
224
227
|
*/
|
|
225
228
|
protected recordAnomaly(anomaly: ProjectionAnomaly): void;
|
|
229
|
+
/**
|
|
230
|
+
* Assign `this.phase`, guarding against regression out of the terminal
|
|
231
|
+
* `'Committed'` state — parity with macp-sdk-python's `_set_phase`
|
|
232
|
+
* (`base_projection.py:289-313`). `applyEnvelope` sets `this.phase =
|
|
233
|
+
* 'Committed'` directly on a `Commitment` envelope and returns before
|
|
234
|
+
* `applyMode` is ever called (see `applyEnvelope` above), so this method is
|
|
235
|
+
* never asked to move *into* `'Committed'` — only ever *out of* it, which is
|
|
236
|
+
* exactly the case it exists to reject. Every subclass `applyMode`
|
|
237
|
+
* implementation must call this instead of assigning `this.phase` directly;
|
|
238
|
+
* the one exception is each subclass's own field initializer, which sets
|
|
239
|
+
* the starting phase before any message has been applied and can never
|
|
240
|
+
* observe `'Committed'`.
|
|
241
|
+
*/
|
|
242
|
+
protected setPhase(phase: string): void;
|
|
226
243
|
}
|
package/dist/projections/base.js
CHANGED
|
@@ -249,5 +249,23 @@ class BaseProjection {
|
|
|
249
249
|
this.anomalies.push(anomaly);
|
|
250
250
|
logging_1.logger.warn('projection anomaly', anomaly);
|
|
251
251
|
}
|
|
252
|
+
/**
|
|
253
|
+
* Assign `this.phase`, guarding against regression out of the terminal
|
|
254
|
+
* `'Committed'` state — parity with macp-sdk-python's `_set_phase`
|
|
255
|
+
* (`base_projection.py:289-313`). `applyEnvelope` sets `this.phase =
|
|
256
|
+
* 'Committed'` directly on a `Commitment` envelope and returns before
|
|
257
|
+
* `applyMode` is ever called (see `applyEnvelope` above), so this method is
|
|
258
|
+
* never asked to move *into* `'Committed'` — only ever *out of* it, which is
|
|
259
|
+
* exactly the case it exists to reject. Every subclass `applyMode`
|
|
260
|
+
* implementation must call this instead of assigning `this.phase` directly;
|
|
261
|
+
* the one exception is each subclass's own field initializer, which sets
|
|
262
|
+
* the starting phase before any message has been applied and can never
|
|
263
|
+
* observe `'Committed'`.
|
|
264
|
+
*/
|
|
265
|
+
setPhase(phase) {
|
|
266
|
+
if (this.phase === 'Committed')
|
|
267
|
+
return;
|
|
268
|
+
this.phase = phase;
|
|
269
|
+
}
|
|
252
270
|
}
|
|
253
271
|
exports.BaseProjection = BaseProjection;
|
|
@@ -22,7 +22,15 @@ class DecisionProjection extends base_1.BaseProjection {
|
|
|
22
22
|
rationale: record.rationale,
|
|
23
23
|
sender: envelope.sender,
|
|
24
24
|
});
|
|
25
|
-
|
|
25
|
+
// RFC-MACP-0007 §5 rule 6: a runtime MUST reject any Proposal/
|
|
26
|
+
// Evaluation/Objection after the first accepted Vote. A replayed/
|
|
27
|
+
// retried Proposal (same proposalId, a distinct messageId that still
|
|
28
|
+
// passes applyEnvelope's message_id dedup gate) must therefore not
|
|
29
|
+
// rewind `phase` from 'Voting' back to 'Evaluation' (issue #153).
|
|
30
|
+
// Guarding on the initial phase value — mirrors macp-sdk-python's
|
|
31
|
+
// identical `if self.phase == "Proposal": self._set_phase(...)`.
|
|
32
|
+
if (this.phase === 'Proposal')
|
|
33
|
+
this.setPhase('Evaluation');
|
|
26
34
|
break;
|
|
27
35
|
}
|
|
28
36
|
case 'Evaluation': {
|
|
@@ -80,20 +88,19 @@ class DecisionProjection extends base_1.BaseProjection {
|
|
|
80
88
|
// runtime rejects any session-scoped message once the session is
|
|
81
89
|
// non-OPEN, so a `Vote` cannot legally follow a `Commitment` in
|
|
82
90
|
// accepted history. If one reaches here anyway (the caller violated
|
|
83
|
-
// the accepted-only contract — see `applyEnvelope`'s docblock),
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
// RFC-MACP-0012 `:211`); this
|
|
87
|
-
// terminality, not by a phase
|
|
88
|
-
// recorded here too, but unlike
|
|
89
|
-
// "frozen pending cross-SDK
|
|
90
|
-
//
|
|
91
|
-
// `base.ts`), this
|
|
92
|
-
//
|
|
93
|
-
// per #128's own
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
91
|
+
// the accepted-only contract — see `applyEnvelope`'s docblock),
|
|
92
|
+
// `setPhase` (see `base.ts`) is what keeps `phase` from regressing
|
|
93
|
+
// out of `'Committed'`. Note "phase" itself is not a normative MACP
|
|
94
|
+
// term (it appears once, in passing, at RFC-MACP-0012 `:211`); this
|
|
95
|
+
// guard is justified by session terminality, not by a phase
|
|
96
|
+
// specification. An anomaly would be recorded here too, but unlike
|
|
97
|
+
// five of the six sites that were "frozen pending cross-SDK
|
|
98
|
+
// agreement" (issue #126/#128, now resolved for
|
|
99
|
+
// `duplicate_task_accept`/`settled_handoff` — see `base.ts`), this
|
|
100
|
+
// one is a session-terminality phase-regression guard, not a
|
|
101
|
+
// settlement discard, and remains genuinely undecided per #128's own
|
|
102
|
+
// explicit scoping. No behavior change here.
|
|
103
|
+
this.setPhase('Voting');
|
|
97
104
|
break;
|
|
98
105
|
}
|
|
99
106
|
default:
|
|
@@ -21,7 +21,7 @@ class HandoffProjection extends base_1.BaseProjection {
|
|
|
21
21
|
sender: envelope.sender,
|
|
22
22
|
status: 'offered',
|
|
23
23
|
});
|
|
24
|
-
this.
|
|
24
|
+
this.setPhase('OfferPending');
|
|
25
25
|
break;
|
|
26
26
|
}
|
|
27
27
|
case 'HandoffContext': {
|
|
@@ -40,7 +40,7 @@ class HandoffProjection extends base_1.BaseProjection {
|
|
|
40
40
|
}
|
|
41
41
|
handoff.contextContentType = record.contentType;
|
|
42
42
|
if (this.phase === 'OfferPending')
|
|
43
|
-
this.
|
|
43
|
+
this.setPhase('ContextSharing');
|
|
44
44
|
}
|
|
45
45
|
break;
|
|
46
46
|
}
|
|
@@ -72,7 +72,7 @@ class HandoffProjection extends base_1.BaseProjection {
|
|
|
72
72
|
// (proto-registry), so this is always a real boolean once proto 0.1.6
|
|
73
73
|
// is loaded — `true` marks a runtime synthetic implicit accept.
|
|
74
74
|
handoff.implicit = record.implicit ?? false;
|
|
75
|
-
this.
|
|
75
|
+
this.setPhase('Accepted');
|
|
76
76
|
}
|
|
77
77
|
else {
|
|
78
78
|
this.recordAnomaly({
|
|
@@ -107,7 +107,7 @@ class HandoffProjection extends base_1.BaseProjection {
|
|
|
107
107
|
if (handoff.status === 'offered' || handoff.status === 'context_sent') {
|
|
108
108
|
handoff.status = 'declined';
|
|
109
109
|
handoff.declinedBy = record.declinedBy;
|
|
110
|
-
this.
|
|
110
|
+
this.setPhase('Declined');
|
|
111
111
|
}
|
|
112
112
|
else {
|
|
113
113
|
this.recordAnomaly({
|
|
@@ -8,7 +8,24 @@ export interface ProposalRecord {
|
|
|
8
8
|
tags?: string[];
|
|
9
9
|
sender: string;
|
|
10
10
|
supersedes?: string;
|
|
11
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Reachable values only (issue #146). An `Accept` is recorded on the
|
|
13
|
+
* projection's `accepts` list (and surfaced via `acceptedProposal()` /
|
|
14
|
+
* `isAccepted()`), never on this field — no code path assigns `'accepted'`
|
|
15
|
+
* here. A non-terminal `Reject` likewise leaves this `'open'`; only a
|
|
16
|
+
* terminal `Reject` sets `'rejected'`.
|
|
17
|
+
*
|
|
18
|
+
* That is by design, not an omission: acceptance is a per-sender,
|
|
19
|
+
* supersedable relation (RFC-MACP-0008 §5 rule 5), not a per-proposal
|
|
20
|
+
* fact, so a scalar field here cannot hold it — "alice accepts p2 while
|
|
21
|
+
* bob still accepts p1" is a legal state. This mirrors the runtime, whose
|
|
22
|
+
* `ProposalDisposition` is `{Live, Withdrawn}` with acceptance in a
|
|
23
|
+
* separate `accepts` map, and `macp-sdk-python`'s own `status: str`
|
|
24
|
+
* (documented the same way after its issue #112). `task.ts`/`handoff.ts`
|
|
25
|
+
* do set `'accepted'`-equivalent statuses because their acceptance is one
|
|
26
|
+
* actor claiming one slot — this mode's isn't. See `docs/modes/proposal.md`.
|
|
27
|
+
*/
|
|
28
|
+
status: 'open' | 'rejected' | 'withdrawn';
|
|
12
29
|
}
|
|
13
30
|
export interface ProposalAcceptRecord {
|
|
14
31
|
proposalId: string;
|
|
@@ -21,6 +38,19 @@ export interface ProposalRejectRecord {
|
|
|
21
38
|
reason?: string;
|
|
22
39
|
sender: string;
|
|
23
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* The three reachable `ProposalRecord.status` values, as a runtime array —
|
|
43
|
+
* cross-SDK contract pinned by the spec repo's `schemas/parity/contract.json`
|
|
44
|
+
* (`proposal_disposition.projection_status_values`, `contract_version`
|
|
45
|
+
* 1.3.0, issue #156). `satisfies readonly ProposalRecord['status'][]` links
|
|
46
|
+
* this array to the type above at compile time, and `_ProposalStatusIsFrozen`
|
|
47
|
+
* below closes the loop in the other direction — together they prove at
|
|
48
|
+
* `npm run check` time, not just in a docblock, that `status` can never hold
|
|
49
|
+
* an acceptance-shaped value like `'accepted'` (see `ProposalRecord.status`'s
|
|
50
|
+
* own docblock above for why acceptance is tracked separately). See
|
|
51
|
+
* `tests/parity/contract.test.ts`.
|
|
52
|
+
*/
|
|
53
|
+
export declare const PROPOSAL_STATUS_VALUES: readonly ["open", "rejected", "withdrawn"];
|
|
24
54
|
export declare class ProposalProjection extends BaseProjection {
|
|
25
55
|
protected readonly mode = "macp.mode.proposal.v1";
|
|
26
56
|
readonly proposals: Map<string, ProposalRecord>;
|
|
@@ -1,8 +1,25 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ProposalProjection = void 0;
|
|
3
|
+
exports.ProposalProjection = exports.PROPOSAL_STATUS_VALUES = void 0;
|
|
4
4
|
const constants_1 = require("../constants");
|
|
5
5
|
const base_1 = require("./base");
|
|
6
|
+
/**
|
|
7
|
+
* The three reachable `ProposalRecord.status` values, as a runtime array —
|
|
8
|
+
* cross-SDK contract pinned by the spec repo's `schemas/parity/contract.json`
|
|
9
|
+
* (`proposal_disposition.projection_status_values`, `contract_version`
|
|
10
|
+
* 1.3.0, issue #156). `satisfies readonly ProposalRecord['status'][]` links
|
|
11
|
+
* this array to the type above at compile time, and `_ProposalStatusIsFrozen`
|
|
12
|
+
* below closes the loop in the other direction — together they prove at
|
|
13
|
+
* `npm run check` time, not just in a docblock, that `status` can never hold
|
|
14
|
+
* an acceptance-shaped value like `'accepted'` (see `ProposalRecord.status`'s
|
|
15
|
+
* own docblock above for why acceptance is tracked separately). See
|
|
16
|
+
* `tests/parity/contract.test.ts`.
|
|
17
|
+
*/
|
|
18
|
+
exports.PROPOSAL_STATUS_VALUES = [
|
|
19
|
+
'open',
|
|
20
|
+
'rejected',
|
|
21
|
+
'withdrawn',
|
|
22
|
+
];
|
|
6
23
|
class ProposalProjection extends base_1.BaseProjection {
|
|
7
24
|
mode = constants_1.MODE_PROPOSAL;
|
|
8
25
|
proposals = new Map();
|
|
@@ -86,7 +103,7 @@ class ProposalProjection extends base_1.BaseProjection {
|
|
|
86
103
|
const proposal = this.proposals.get(record.proposalId);
|
|
87
104
|
if (proposal) {
|
|
88
105
|
proposal.status = 'rejected';
|
|
89
|
-
this.
|
|
106
|
+
this.setPhase('TerminalRejected');
|
|
90
107
|
}
|
|
91
108
|
}
|
|
92
109
|
break;
|
|
@@ -15,7 +15,7 @@ class QuorumProjection extends base_1.BaseProjection {
|
|
|
15
15
|
case 'ApprovalRequest': {
|
|
16
16
|
const record = payload;
|
|
17
17
|
this.requests.set(record.requestId, { ...record, sender: envelope.sender });
|
|
18
|
-
this.
|
|
18
|
+
this.setPhase('Voting');
|
|
19
19
|
break;
|
|
20
20
|
}
|
|
21
21
|
case 'Approve': {
|
|
@@ -54,21 +54,24 @@ class QuorumProjection extends base_1.BaseProjection {
|
|
|
54
54
|
const senderMap = this.ballots.get(requestId) ?? new Map();
|
|
55
55
|
const kept = senderMap.get(envelope.sender);
|
|
56
56
|
if (kept !== undefined) {
|
|
57
|
-
// RFC-MACP-0011 §5 rule 3 (`:
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
// not a change of vote, so this guard is keyed on the
|
|
66
|
-
// the request — never on sender + vote.
|
|
67
|
-
// identically in all three arms
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
57
|
+
// RFC-MACP-0011 §5 rule 3 (`:69`, hardened by spec PR
|
|
58
|
+
// multiagentcoordinationprotocol#85): "Each eligible participant MUST
|
|
59
|
+
// cast at most one ballot per request_id... A runtime MUST reject a
|
|
60
|
+
// second ballot from the same sender for the same request_id,
|
|
61
|
+
// regardless of the type of either ballot; the first accepted ballot
|
|
62
|
+
// stands." Participation itself is still MAY (a participant need not
|
|
63
|
+
// ballot at all) — only the one-per-participant cap and first-wins
|
|
64
|
+
// outcome are MUST. A later ballot of a *different* type is still a
|
|
65
|
+
// duplicate, not a change of vote, so this guard is keyed on the
|
|
66
|
+
// sender ALONE within the request — never on sender + vote.
|
|
67
|
+
// macp-runtime enforces this identically in all three arms
|
|
68
|
+
// (quorum.rs:164/184/204).
|
|
69
|
+
//
|
|
70
|
+
// Keeping the sender's FIRST ballot is now a direct rule-3 citation
|
|
71
|
+
// (above), not an inference — before PR #85 hardened the rule, this
|
|
72
|
+
// comment relied on parity with RFC-MACP-0007 §5 item 3's first-stands
|
|
73
|
+
// rule for `Vote` plus macp-runtime's observed behavior, since RFC-0011
|
|
74
|
+
// capped *how many* ballots without yet stating *which* stands.
|
|
72
75
|
this.recordAnomaly({
|
|
73
76
|
kind: 'duplicate_ballot',
|
|
74
77
|
mode: envelope.mode,
|
|
@@ -33,12 +33,19 @@ export interface TaskFailRecord {
|
|
|
33
33
|
retryable: boolean;
|
|
34
34
|
sender: string;
|
|
35
35
|
}
|
|
36
|
+
export interface TaskRejectRecord {
|
|
37
|
+
taskId: string;
|
|
38
|
+
assignee: string;
|
|
39
|
+
reason?: string;
|
|
40
|
+
sender: string;
|
|
41
|
+
}
|
|
36
42
|
export declare class TaskProjection extends BaseProjection {
|
|
37
43
|
protected readonly mode = "macp.mode.task.v1";
|
|
38
44
|
readonly tasks: Map<string, TaskRecord>;
|
|
39
45
|
readonly updates: TaskUpdateRecord[];
|
|
40
46
|
readonly completions: TaskCompleteRecord[];
|
|
41
47
|
readonly failures: TaskFailRecord[];
|
|
48
|
+
readonly rejections: TaskRejectRecord[];
|
|
42
49
|
phase: 'Pending' | 'Requested' | 'InProgress' | 'Completed' | 'Failed' | 'Committed';
|
|
43
50
|
/**
|
|
44
51
|
* The **session-level** active assignment: who currently holds the session's
|
package/dist/projections/task.js
CHANGED
|
@@ -9,6 +9,7 @@ class TaskProjection extends base_1.BaseProjection {
|
|
|
9
9
|
updates = [];
|
|
10
10
|
completions = [];
|
|
11
11
|
failures = [];
|
|
12
|
+
rejections = [];
|
|
12
13
|
phase = 'Pending';
|
|
13
14
|
/**
|
|
14
15
|
* The **session-level** active assignment: who currently holds the session's
|
|
@@ -46,7 +47,7 @@ class TaskProjection extends base_1.BaseProjection {
|
|
|
46
47
|
progress: 0,
|
|
47
48
|
sender: envelope.sender,
|
|
48
49
|
});
|
|
49
|
-
this.
|
|
50
|
+
this.setPhase('Requested');
|
|
50
51
|
break;
|
|
51
52
|
}
|
|
52
53
|
case 'TaskAccept': {
|
|
@@ -90,7 +91,7 @@ class TaskProjection extends base_1.BaseProjection {
|
|
|
90
91
|
task.assignee = record.assignee;
|
|
91
92
|
task.status = 'accepted';
|
|
92
93
|
this.activeAssignment = { sender: envelope.sender, taskId: record.taskId };
|
|
93
|
-
this.
|
|
94
|
+
this.setPhase('InProgress');
|
|
94
95
|
}
|
|
95
96
|
else {
|
|
96
97
|
this.recordAnomaly({
|
|
@@ -109,6 +110,16 @@ class TaskProjection extends base_1.BaseProjection {
|
|
|
109
110
|
}
|
|
110
111
|
case 'TaskReject': {
|
|
111
112
|
const record = payload;
|
|
113
|
+
// Issue #151: unconditional audit record, matching this file's own
|
|
114
|
+
// TaskUpdate/TaskComplete/TaskFail convention (and Python's
|
|
115
|
+
// identical unconditional `self.rejections.append(...)`,
|
|
116
|
+
// task.py:186-196) — `sender` comes from the envelope, never the
|
|
117
|
+
// payload, matching the three sibling records. Deliberately does
|
|
118
|
+
// NOT fall back `assignee` to `envelope.sender` the way Python's
|
|
119
|
+
// `p.assignee or envelope.sender` does: TaskComplete/TaskFail below
|
|
120
|
+
// spread the payload's `assignee` as-is with no such fallback, and
|
|
121
|
+
// intra-SDK consistency wins over replicating Python's nuance here.
|
|
122
|
+
this.rejections.push({ ...record, sender: envelope.sender });
|
|
112
123
|
const task = this.tasks.get(record.taskId);
|
|
113
124
|
if (task)
|
|
114
125
|
task.status = 'rejected';
|
|
@@ -158,7 +169,7 @@ class TaskProjection extends base_1.BaseProjection {
|
|
|
158
169
|
if (task) {
|
|
159
170
|
task.status = 'completed';
|
|
160
171
|
task.progress = 1;
|
|
161
|
-
this.
|
|
172
|
+
this.setPhase('Completed');
|
|
162
173
|
}
|
|
163
174
|
break;
|
|
164
175
|
}
|
|
@@ -168,7 +179,7 @@ class TaskProjection extends base_1.BaseProjection {
|
|
|
168
179
|
const task = this.tasks.get(record.taskId);
|
|
169
180
|
if (task) {
|
|
170
181
|
task.status = 'failed';
|
|
171
|
-
this.
|
|
182
|
+
this.setPhase('Failed');
|
|
172
183
|
}
|
|
173
184
|
break;
|
|
174
185
|
}
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
package/dist/watchers.js
CHANGED
|
@@ -130,7 +130,7 @@ class ModeRegistryWatcher {
|
|
|
130
130
|
const result = await gen.next();
|
|
131
131
|
await gen.return(undefined);
|
|
132
132
|
if (result.done)
|
|
133
|
-
throw new
|
|
133
|
+
throw new errors_1.MacpTransportError('stream ended before receiving a change');
|
|
134
134
|
return result.value;
|
|
135
135
|
}
|
|
136
136
|
}
|
|
@@ -159,7 +159,7 @@ class RootsWatcher {
|
|
|
159
159
|
const result = await gen.next();
|
|
160
160
|
await gen.return(undefined);
|
|
161
161
|
if (result.done)
|
|
162
|
-
throw new
|
|
162
|
+
throw new errors_1.MacpTransportError('stream ended before receiving a change');
|
|
163
163
|
return result.value;
|
|
164
164
|
}
|
|
165
165
|
}
|
|
@@ -202,7 +202,7 @@ class SignalWatcher {
|
|
|
202
202
|
const result = await gen.next();
|
|
203
203
|
await gen.return(undefined);
|
|
204
204
|
if (result.done)
|
|
205
|
-
throw new
|
|
205
|
+
throw new errors_1.MacpTransportError('stream ended before receiving a signal');
|
|
206
206
|
return result.value;
|
|
207
207
|
}
|
|
208
208
|
}
|
|
@@ -231,7 +231,7 @@ class PolicyWatcher {
|
|
|
231
231
|
const result = await gen.next();
|
|
232
232
|
await gen.return(undefined);
|
|
233
233
|
if (result.done)
|
|
234
|
-
throw new
|
|
234
|
+
throw new errors_1.MacpTransportError('stream ended before receiving a policy change');
|
|
235
235
|
return result.value;
|
|
236
236
|
}
|
|
237
237
|
}
|
|
@@ -270,7 +270,7 @@ class SessionLifecycleWatcher {
|
|
|
270
270
|
const result = await gen.next();
|
|
271
271
|
await gen.return(undefined);
|
|
272
272
|
if (result.done)
|
|
273
|
-
throw new
|
|
273
|
+
throw new errors_1.MacpTransportError('stream ended before receiving a session lifecycle event');
|
|
274
274
|
return result.value;
|
|
275
275
|
}
|
|
276
276
|
}
|