State returned from FileNode/get is not the current state

Issue Description

FileNode/get should return the state on the server of all FileNode records, but the state returned is out of date.

Expected Behavior

See the FileNode/get expected response behavior from the JMAP spec.

Actual Behavior

The state returned is older than the current state.

Reproduction Steps

Request:

{
    "using": [
        "urn:ietf:params:jmap:core",
        "urn:ietf:params:jmap:filenode"
    ],
    "methodCalls": [
        [
            "FileNode/get",
            {
                "accountId": "c",
                "ids": null,
                "properties": [
                    "id",
                    "name",
                    "parentId",
                    "nodeType",
                    "modified",
                    "size",
                    "blobId",
                    "type"
                ]
            },
            "nodes"
        ]
    ]
}

Response:

{
    "methodResponses": [
        [
            "FileNode/get",
            {
                "accountId": "c",
                "state": "sei",
                "list": [
                    {
                        "id": "be",
                        "name": "apple.md",
                        "parentId": null,
                        "nodeType": "file",
                        "modified": "2026-06-02T14:02:15Z",
                        "size": 56,
                        "blobId": "cbrij9timsflhxzbidww1i7uq19soc3hib31x0iy99jnyyitsknkwaqmeq",
                        "type": "application/octet-stream"
                    },
                    {
                        "id": "bf",
                        "name": "banana.md",
                        "parentId": null,
                        "nodeType": "file",
                        "modified": "2026-06-02T14:02:15Z",
                        "size": 38,
                        "blobId": "caq1s9yh1uz1gvtwl2npak2ujelu7dpit91w7rbyfn7dqv7mefxgsaqmeu",
                        "type": "application/octet-stream"
                    }
                ],
                "notFound": []
            },
            "nodes"
        ]
    ],
    "sessionState": "9f3fd14e"
}

The returned state is “sei”.

Now, without making any changes, request all changes since “sei”. There shouldn’t be any, because “sei” was just returned, so it should be the current state.

Request:

{
    "using": [
        "urn:ietf:params:jmap:core",
        "urn:ietf:params:jmap:filenode"
    ],
    "methodCalls": [
        [
            "FileNode/changes",
            {
                "accountId": "c",
                "sinceState": "sei",
                "maxChanges": null
            },
            "changes"
        ],
        [
            "FileNode/get",
            {
                "accountId": "c",
                "#ids": {
                    "resultOf": "changes",
                    "name": "FileNode/changes",
                    "path": "/created"
                },
                "properties": [
                    "id",
                    "name",
                    "parentId",
                    "nodeType",
                    "modified",
                    "size",
                    "blobId",
                    "type"
                ]
            },
            "created"
        ],
        [
            "FileNode/get",
            {
                "accountId": "c",
                "#ids": {
                    "resultOf": "changes",
                    "name": "FileNode/changes",
                    "path": "/updated"
                },
                "properties": [
                    "id",
                    "name",
                    "parentId",
                    "nodeType",
                    "modified",
                    "size",
                    "blobId",
                    "type"
                ]
            },
            "updated"
        ]
    ]
}

Response:

{
    "methodResponses": [
        [
            "FileNode/changes",
            {
                "accountId": "c",
                "oldState": "sei",
                "newState": "sjq",
                "hasMoreChanges": false,
                "created": [
                    "be",
                    "bf"
                ],
                "updated": [],
                "destroyed": [
                    "w",
                    "z",
                    "s",
                    "k",
                    "j",
                    "t",
                    "o",
                    "m",
                    "l",
                    "n",
                    "u",
                    "r",
                    "q",
                    "p",
                    "i",
                    "c",
                    "bb",
                    "bc",
                    "3",
                    "1",
                    "0",
                    "2",
                    "7",
                    "v",
                    "y",
                    "f",
                    "g",
                    "e",
                    "h",
                    "ba",
                    "x",
                    "d",
                    "9",
                    "b"
                ]
            },
            "changes"
        ],
        [
            "FileNode/get",
            {
                "accountId": "c",
                "state": "sei",
                "list": [
                    {
                        "id": "be",
                        "name": "apple.md",
                        "parentId": null,
                        "nodeType": "file",
                        "modified": "2026-06-02T14:02:15Z",
                        "size": 56,
                        "blobId": "cbrij9timsflhxzbidww1i7uq19soc3hib31x0iy99jnyyitsknkwaqmeq",
                        "type": "application/octet-stream"
                    },
                    {
                        "id": "bf",
                        "name": "banana.md",
                        "parentId": null,
                        "nodeType": "file",
                        "modified": "2026-06-02T14:02:15Z",
                        "size": 38,
                        "blobId": "caq1s9yh1uz1gvtwl2npak2ujelu7dpit91w7rbyfn7dqv7mefxgsaqmeu",
                        "type": "application/octet-stream"
                    }
                ],
                "notFound": []
            },
            "created"
        ],
        [
            "FileNode/get",
            {
                "accountId": "c",
                "state": "sei",
                "list": [],
                "notFound": []
            },
            "updated"
        ]
    ],
    "sessionState": "9f3fd14e"
}

Notice that oldState is “sei” and newState is “sjq”.

Before setting up this test with just two files for the bug report, I had deleted all folders and files using the File Manager with a WebDAV connection to Stalwart. The list of destroyed nodes refers to all those old folders and files.

When I first asked for all the ids in this new session, FileNode/get said the current state was “sei”, but according to FileNode/changes, the current state is “sjq”.

Notice also that the state in the two FileNode/get responses for the created and updated nodes. Again, it is reporting the curent state to be “sei”, when FileNode/changes has just told me that newState is “sjq”.

Stalwart Version

v0.16.x

Installation Method

Docker

Database Backend

RocksDB

Blob Storage

RocksDB

Search Engine

Internal

Directory Backend

Internal

I have reviewed the documentation and FAQ and confirm that my issue is NOT addressed there.

on

I have searched this support forum (open and closed topics) and confirm this is not a duplicate.

on

I understand that topics in this category are triaged by a bot first but a human reply will follow up. If I’d prefer a human-only reply, I’ll add the no-ai tag to my topic.

on

This has been fixed on the main branch. The fix will be included in v0.16.8.