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