The SDK reports a device verified after discarding both results that could say otherwise #130

Open
opened 2026-08-26 16:30:53 +00:00 by jlxq0 · 5 comments
Owner

A bot logs that it verified its own device, the person reading its messages sees "Encrypted by a device not verified by its owner", and nothing anywhere disagrees.

The measurement

Synapse's signature table, with a control strong enough to trust the negatives: 164 signature rows across all users, and one human account's own 11 devices splitting 8 signed to 3 not, so the query does report both outcomes.

@honokahonoka_ai_bot  KZNWDXGAEP  signed
@nexonexo_ai_bot      VLGBZAMQKG  signed
@pennypenny_ai_bot    QFESBGWPRF  signed
@vryanvryan_ai_bot    PGDJUUSFRJ  signed
@mantis_ai_bot        MDFPODBQQJ  NOT SIGNED
@alan_ai_bot          HWXIWTGHIB  NOT SIGNED
@clark_ai_bot         UBPORSHGOC  NOT SIGNED
@lucybot              KKWZVKODWE  NOT SIGNED, and no cross-signing keys at all

@mantis_ai_bot is the one whose logs say it succeeded. At 16:25:12 on the current pod the SDK logged, in order: cross-signing keys fetched with master, self-signing and user-signing all true; "Successfully imported the self-signing key, attempting to sign our own device"; then "Successfully signed our own device, the device is now verified". It is unsigned in the table.

Why the log and the table do not conflict

Verified in the vendored sources rather than inferred.

/keys/signatures/upload returns a failures map and the response is thrown away. matrix-sdk-0.17.0/src/encryption/identities/devices.rs:291:

pub async fn verify(&self) -> Result<(), ManualVerifyError> {
    let request = self.inner.verify().await?;
    self.client.send(request).await?;
    Ok(())
}

The discarded type is ruma-client-api-0.23.1/src/keys/upload_signatures.rs:46-49, whose only field is pub failures: BTreeMap<OwnedUserId, BTreeMap<String, Failure>> — a per-key rejection map returned inside a 200. Grepping matrix-sdk-crypto-0.17.0 for failures finds it in ten files and in no signature-upload path, and the ten hits are the control that the string is present and greppable.

The follow-up query that exists to check the result also discards it. matrix-sdk-0.17.0/src/encryption/secret_storage/secret_store.rs:443-452:

own_device.verify().await?;

// Another /keys/query request to ensure that the signatures we uploaded using
// `own_device.verify()` are attached to the `Device` we have in storage.
let (request_id, request) = olm_machine.query_keys_for_users([olm_machine.user_id()]);
self.client.keys_query(&request_id, request.device_keys).await?;

info!("Successfully signed our own device, the device is now verified");

The comment says the query exists to ensure the signatures are attached. Nothing reads whether they are. So the log line means a 2xx came back and a second request was sent, and every value that could contradict it is dropped before it reaches a branch.

What to build

A guard of our own, because the library will not give us one. After recover, re-query and assert that the device carries a self-signature before reporting anything. That is the half that closes the observable.

Reading the failures map is the other half, and it needs a path that does not go through Device::verify, since that function has already thrown the response away. Either build the signature-upload request directly, or treat the post-query as authoritative and drop the claim entirely.

Whichever is chosen, the log line has to stop asserting what it cannot observe. "Uploaded a self-signature" is true and cheap; "the device is now verified" is a statement about the homeserver's state made without reading it.

What this is not

Not #88. That is device keys never landing server-side, where the SDK logs that our own device might have been deleted and a client reports the device does not support encryption. This is a device whose keys published fine and whose self-signature did not attach. Different layer, and merging #88 would neither fix nor obscure it.

Not something bootstrap_cross_signing should be pointed at. It would sign one account and leave the reporting defect in place, which is how this stays invisible for another three months.

@lucybot having no cross-signing keys at all is #128, a third state with the same root as its empty allowlist: nothing runs a new bot's setup.

The general shape

A call that succeeds because the request was accepted, read as a statement about what the far side did with it. Same as a JMAP notCreated under an HTTP 200, and the same as a docker status tick that is a skip.

A bot logs that it verified its own device, the person reading its messages sees "Encrypted by a device not verified by its owner", and nothing anywhere disagrees. ## The measurement Synapse's signature table, with a control strong enough to trust the negatives: 164 signature rows across all users, and one human account's own 11 devices splitting 8 signed to 3 not, so the query does report both outcomes. @honokahonoka_ai_bot KZNWDXGAEP signed @nexonexo_ai_bot VLGBZAMQKG signed @pennypenny_ai_bot QFESBGWPRF signed @vryanvryan_ai_bot PGDJUUSFRJ signed @mantis_ai_bot MDFPODBQQJ NOT SIGNED @alan_ai_bot HWXIWTGHIB NOT SIGNED @clark_ai_bot UBPORSHGOC NOT SIGNED @lucybot KKWZVKODWE NOT SIGNED, and no cross-signing keys at all **`@mantis_ai_bot` is the one whose logs say it succeeded.** At 16:25:12 on the current pod the SDK logged, in order: cross-signing keys fetched with master, self-signing and user-signing all true; "Successfully imported the self-signing key, attempting to sign our own device"; then "Successfully signed our own device, the device is now verified". It is unsigned in the table. ## Why the log and the table do not conflict Verified in the vendored sources rather than inferred. **`/keys/signatures/upload` returns a `failures` map and the response is thrown away.** `matrix-sdk-0.17.0/src/encryption/identities/devices.rs:291`: pub async fn verify(&self) -> Result<(), ManualVerifyError> { let request = self.inner.verify().await?; self.client.send(request).await?; Ok(()) } The discarded type is `ruma-client-api-0.23.1/src/keys/upload_signatures.rs:46-49`, whose only field is `pub failures: BTreeMap<OwnedUserId, BTreeMap<String, Failure>>` — a per-key rejection map returned **inside a 200**. Grepping `matrix-sdk-crypto-0.17.0` for `failures` finds it in ten files and in **no signature-upload path**, and the ten hits are the control that the string is present and greppable. **The follow-up query that exists to check the result also discards it.** `matrix-sdk-0.17.0/src/encryption/secret_storage/secret_store.rs:443-452`: own_device.verify().await?; // Another /keys/query request to ensure that the signatures we uploaded using // `own_device.verify()` are attached to the `Device` we have in storage. let (request_id, request) = olm_machine.query_keys_for_users([olm_machine.user_id()]); self.client.keys_query(&request_id, request.device_keys).await?; info!("Successfully signed our own device, the device is now verified"); The comment says the query exists to ensure the signatures are attached. Nothing reads whether they are. **So the log line means a 2xx came back and a second request was sent**, and every value that could contradict it is dropped before it reaches a branch. ## What to build **A guard of our own, because the library will not give us one.** After `recover`, re-query and assert that the device carries a self-signature before reporting anything. That is the half that closes the observable. **Reading the `failures` map is the other half**, and it needs a path that does not go through `Device::verify`, since that function has already thrown the response away. Either build the signature-upload request directly, or treat the post-query as authoritative and drop the claim entirely. **Whichever is chosen, the log line has to stop asserting what it cannot observe.** "Uploaded a self-signature" is true and cheap; "the device is now verified" is a statement about the homeserver's state made without reading it. ## What this is not **Not #88.** That is device keys never landing server-side, where the SDK logs that our own device might have been deleted and a client reports the device does not support encryption. This is a device whose keys published fine and whose self-signature did not attach. Different layer, and merging #88 would neither fix nor obscure it. **Not something `bootstrap_cross_signing` should be pointed at.** It would sign one account and leave the reporting defect in place, which is how this stays invisible for another three months. **`@lucybot` having no cross-signing keys at all is #128**, a third state with the same root as its empty allowlist: nothing runs a new bot's setup. ## The general shape A call that succeeds because the request was accepted, read as a statement about what the far side did with it. Same as a JMAP `notCreated` under an HTTP 200, and the same as a `docker` status tick that is a skip.
Author
Owner

Two issues, two owners, and this one should not be picked up as a one-liner.

the guard       matrix-mcp's, cheap        after `recover`, re-query and confirm the
                                           device carries a self-signature before
                                           reporting anything
the diagnosis   upstream's, not cheap      reading the `failures` map, which means not
                                           calling `Device::verify` at all

Device::verify sends and discards inside matrix-sdk:

let request = self.inner.verify().await?;
self.client.send(request).await?;
Ok(())

So by the time this crate sees anything, the map is gone. Logging it means building and sending /keys/signatures/upload directly and reading the body, rather than adding a line.

An estimate in an issue body is a claim like any other, and a wrong one gets the issue picked up by the wrong person. Someone with twenty minutes takes a one-liner, and the twenty minutes is where they find out. That does not show up as a mistake anywhere afterwards.

The third thing, which is neither of the above and is free. The log line has to stop asserting what it cannot observe. "Uploaded a self-signature" is true and costs nothing. "The device is now verified" is a claim about the far side made without reading it, and while that line exists any guard built here sits beside a message contradicting it.

Evidence this is live rather than theoretical, measured on v0.10.6 on 2026-08-26: @mantis_ai_bot on device MDFPODBQQJ logged the success line at 16:25:12, and fifteen minutes later Synapse held no self-signature for that device. Four of eight bots unsigned. The query carries its own validation, 164 self-signature rows across all users and Julian's own eleven devices splitting 8 signed to 3 not, without which the table is a claim rather than a measurement.

**Two issues, two owners, and this one should not be picked up as a one-liner.** the guard matrix-mcp's, cheap after `recover`, re-query and confirm the device carries a self-signature before reporting anything the diagnosis upstream's, not cheap reading the `failures` map, which means not calling `Device::verify` at all `Device::verify` sends and discards **inside matrix-sdk**: let request = self.inner.verify().await?; self.client.send(request).await?; Ok(()) So by the time this crate sees anything, the map is gone. Logging it means building and sending `/keys/signatures/upload` directly and reading the body, rather than adding a line. **An estimate in an issue body is a claim like any other, and a wrong one gets the issue picked up by the wrong person.** Someone with twenty minutes takes a one-liner, and the twenty minutes is where they find out. That does not show up as a mistake anywhere afterwards. **The third thing, which is neither of the above and is free.** The log line has to stop asserting what it cannot observe. *"Uploaded a self-signature"* is true and costs nothing. *"The device is now verified"* is a claim about the far side made without reading it, and while that line exists any guard built here sits beside a message contradicting it. **Evidence this is live rather than theoretical**, measured on `v0.10.6` on 2026-08-26: `@mantis_ai_bot` on device `MDFPODBQQJ` logged the success line at 16:25:12, and fifteen minutes later Synapse held no self-signature for that device. Four of eight bots unsigned. The query carries its own validation, 164 self-signature rows across all users and Julian's own eleven devices splitting 8 signed to 3 not, without which the table is a claim rather than a measurement.
Author
Owner

This is downstream of #143, and the upload had nothing to land in

Measured by Clark against Synapse and MAS, 2026-08-29:

seven live MAS sessions on MATRIXMCP-DLX8SKHK, finished_at IS NULL, back to 29 July
Synapse for that device:                      has_keys = 0   sigs = 0
control MATRIXMCP-KZ9M8ZY8, one session:      1 key          1 signature

Synapse has never held a device key for that device. Not a stale key, not a superseded one: none. Seven OAuth sessions contending over /keys/upload have left the record empty, where the one-session control device carries both a key and a signature.

So the reporting defect described in this issue is real and is not the cause. Device::verify discarding the failures map, and secret_store.rs discarding the follow-up /keys/query that exists to confirm the signature attached, are why nobody could see it. What they were hiding is an upload into a device record that does not exist. @mantis_ai_bot logged "Successfully signed our own device, the device is now verified" and is unsigned in Clark's table, which is exactly that.

Fixing the reporting is still worth doing on its own terms — a call that succeeds because the request was accepted, read as a statement about what the far side did with it, is the shape this repository keeps paying for. But it will not make a device signed, and anyone picking this up should read #143 first.

Treat #143, #130 and #139 as one cause until the code contradicts it. Nothing in this repository signs or uploads anything that would behave differently under seven sessions than under one.

## This is downstream of #143, and the upload had nothing to land in Measured by Clark against Synapse and MAS, 2026-08-29: seven live MAS sessions on MATRIXMCP-DLX8SKHK, finished_at IS NULL, back to 29 July Synapse for that device: has_keys = 0 sigs = 0 control MATRIXMCP-KZ9M8ZY8, one session: 1 key 1 signature **Synapse has never held a device key for that device.** Not a stale key, not a superseded one: none. Seven OAuth sessions contending over `/keys/upload` have left the record empty, where the one-session control device carries both a key and a signature. **So the reporting defect described in this issue is real and is not the cause.** `Device::verify` discarding the `failures` map, and `secret_store.rs` discarding the follow-up `/keys/query` that exists to confirm the signature attached, are why nobody could see it. What they were hiding is an upload into a device record that does not exist. `@mantis_ai_bot` logged *"Successfully signed our own device, the device is now verified"* and is unsigned in Clark's table, which is exactly that. **Fixing the reporting is still worth doing on its own terms** — a call that succeeds because the request was accepted, read as a statement about what the far side did with it, is the shape this repository keeps paying for. **But it will not make a device signed**, and anyone picking this up should read #143 first. **Treat #143, #130 and #139 as one cause** until the code contradicts it. Nothing in this repository signs or uploads anything that would behave differently under seven sessions than under one.
Author
Owner

The one-cause reading is under a question: the control may have dissolved

Do not build on the single-mechanism conclusion in the comment above until this resolves.

That conclusion rests on a contrast, not on a single number:

DLX8SKHK   seven live sessions    has_keys = 0   sigs = 0
KZ9M8ZY8   one settled session    1 key          1 signature

The control device has since been reported as 8 live. If it holds eight live sessions and a key and a signature, then contention does not prevent keys from sticking, and the argument loses the only thing supporting it.

It may be two predicates rather than two answers, since settled and live need not count the same thing, and which query produced each is being established rather than guessed at.

What stands regardless: has_keys = 0, sigs = 0 on DLX8SKHK is a direct measurement of the device this issue is about. What is in doubt is the inference from it — that seven contending sessions are the reason — which is the part the one-cause statement, and its echo on the sibling issues, were built on.

Recording the doubt rather than retracting, because a retraction should carry the resolving measurement rather than the uncertainty. This line exists so that nobody reads the paragraph above it as settled in the meantime.

One thing that is settled and does not depend on any of it: the bearer-to-session mapping exists in MAS's database as oauth2_access_tokens.oauth2_session_id, so telling the seven apart needs no change to this server and no jti field. The runtime jti would be a convenience and nothing currently turns on it.

## The one-cause reading is under a question: the control may have dissolved **Do not build on the single-mechanism conclusion in the comment above until this resolves.** That conclusion rests on a contrast, not on a single number: DLX8SKHK seven live sessions has_keys = 0 sigs = 0 KZ9M8ZY8 one settled session 1 key 1 signature **The control device has since been reported as `8 live`.** If it holds eight live sessions *and* a key *and* a signature, then contention does not prevent keys from sticking, and the argument loses the only thing supporting it. It may be two predicates rather than two answers, since *settled* and *live* need not count the same thing, and which query produced each is being established rather than guessed at. **What stands regardless**: `has_keys = 0, sigs = 0` on `DLX8SKHK` is a direct measurement of the device this issue is about. **What is in doubt is the inference from it** — that seven contending sessions are the reason — which is the part the one-cause statement, and its echo on the sibling issues, were built on. **Recording the doubt rather than retracting**, because a retraction should carry the resolving measurement rather than the uncertainty. This line exists so that nobody reads the paragraph above it as settled in the meantime. **One thing that is settled and does not depend on any of it**: the bearer-to-session mapping exists in MAS's database as `oauth2_access_tokens.oauth2_session_id`, so telling the seven apart needs no change to this server and no `jti` field. The runtime `jti` would be a convenience and nothing currently turns on it.
Author
Owner

Retracted: contention by session count is not the mechanism

The resolving measurement, both rows from the same query with the same predicate:

MATRIXMCP-KZ9M8ZY8   8 live sessions   has_keys 1   sigs 1
MATRIXMCP-DLX8SKHK   7 live sessions   has_keys 0   sigs 0

The device with more sessions is the signed one. The earlier "one settled session" was a mislabel: Synapse's sigs = 1 was read as a session count and written as one. The counts never moved; the noun was wrong.

So every inference from that contrast is withdrawn, including the one-cause statement placed on #143, #130 and #139, and the claim that seven contending sessions are why the record is empty. This replaces it rather than sitting beside it.

What stands, and always did: has_keys = 0, sigs = 0 on MATRIXMCP-DLX8SKHK is a direct measurement of that device. The why has gone, not the what.

Nothing here should be folded into a replacement hypothesis yet. One is on the table, that KZ9M8ZY8 has been quiescent for eight weeks while DLX8SKHK is still accumulating sessions, but that is a single variable chosen after seeing the outcome on two devices, which is the shape that fits perfectly and explains the wrong thing. The cheaper discriminator is whether DLX8SKHK was ever quiescent and still keyless, which would kill it without spending anybody's time.

How the retracted claim got here is on #143 under Provenance of the one-cause statement, so a later reader can see it arrived as an instruction on an unmeasured contrast rather than as a measurement.

And this issue's prediction goes with it

I wrote that fixing the reporting "will not make a device signed". That rested entirely on the dead contrast and should not survive as an orphan: it is withdrawn.

What this issue claims on its own evidence is unchanged and is still true. Device::verify discards the failures map returned inside a 200, and secret_store.rs discards the follow-up /keys/query whose comment says it exists to confirm the signature attached. So the SDK logs "Successfully signed our own device, the device is now verified" on evidence that cannot support it, and @mantis_ai_bot produced that line while unsigned. That is measured here and does not depend on why the record is empty.

Whether repairing the reporting also repairs the signing is now open rather than answered in the negative.

## Retracted: contention by session count is not the mechanism The resolving measurement, both rows from the same query with the same predicate: MATRIXMCP-KZ9M8ZY8 8 live sessions has_keys 1 sigs 1 MATRIXMCP-DLX8SKHK 7 live sessions has_keys 0 sigs 0 **The device with *more* sessions is the signed one.** The earlier "one settled session" was a mislabel: Synapse's `sigs = 1` was read as a session count and written as *one*. The counts never moved; the noun was wrong. **So every inference from that contrast is withdrawn**, including the one-cause statement placed on #143, #130 and #139, and the claim that seven contending sessions are why the record is empty. **This replaces it rather than sitting beside it.** **What stands, and always did**: `has_keys = 0, sigs = 0` on `MATRIXMCP-DLX8SKHK` is a direct measurement of that device. **The *why* has gone, not the *what*.** **Nothing here should be folded into a replacement hypothesis yet.** One is on the table, that `KZ9M8ZY8` has been quiescent for eight weeks while `DLX8SKHK` is still accumulating sessions, but that is a single variable chosen after seeing the outcome on two devices, which is the shape that fits perfectly and explains the wrong thing. The cheaper discriminator is whether `DLX8SKHK` was ever quiescent and still keyless, which would kill it without spending anybody's time. **How the retracted claim got here is on #143** under *Provenance of the one-cause statement*, so a later reader can see it arrived as an instruction on an unmeasured contrast rather than as a measurement. ## And this issue's prediction goes with it I wrote that fixing the reporting **"will not make a device signed"**. That rested entirely on the dead contrast and should not survive as an orphan: **it is withdrawn.** **What this issue claims on its own evidence is unchanged and is still true.** `Device::verify` discards the `failures` map returned inside a 200, and `secret_store.rs` discards the follow-up `/keys/query` whose comment says it exists to confirm the signature attached. So the SDK logs *"Successfully signed our own device, the device is now verified"* on evidence that cannot support it, and `@mantis_ai_bot` produced that line while unsigned. **That is measured here and does not depend on why the record is empty.** Whether repairing the reporting also repairs the signing is now open rather than answered in the negative.
Author
Owner

Churn-recency is dead too, and the state to leave here is a measurement with no mechanism

2026-08-01 → 08-06   DLX8SKHK had ZERO live sessions for five days
2026-08-06 → 08-15   exactly ONE live session for nine days
now                  has_keys = 0, sigs = 0

A lone session for nine days did not leave a key. So neither session count nor recency explains it: eight-session KZ9M8ZY8 is signed, and DLX8SKHK is keyless through two quiescent windows. Withdrawn, like contention-by-count before it.

The current-state reading is load-bearing rather than a gap: device keys live in e2e_device_keys_json keyed on (user, device), persist across sessions, and go when the device is deleted. The same device id spans all eight sessions including one that finished on 08-01 and is still in devices, so a key uploaded during the lone-session window would still be there. One device_lists_stream row and zero device_keys_json rows corroborate: one announcement, no key lifecycle.

has_keys = 0, sigs = 0 has now survived every explanation offered for it. That is the state to leave on this issue: a measurement with no mechanism, rather than a mechanism.

So do not build a contention fix, and the PVC constraint recorded above is a constraint on a design nobody should now be starting.

Where the upload is actually gated, and a candidate that is already filed

matrix-sdk-crypto-0.17.0/src/olm/account.rs:666:

let device_keys = self.shared().not().then(|| self.device_keys());

Device keys are offered for upload only while shared is false. Once that flag latches true, keys_for_upload returns None for device keys forever, and every later /keys/upload carries one-time keys only.

That is the exact mechanism #88 describes, open since 2026-05-21 and mergeable: the per-account shared flag latches on the first apparently-successful upload and is never re-verified against the homeserver, so a client operates against a device that does not exist server-side. Its repro was a device with 50 one-time keys and zero rows in e2e_device_keys_json.

The discriminator is one query and it is cheap: does @julian's account hold one-time keys for DLX8SKHK while holding no device keys? If yes, the state matches #88's signature exactly and #88 becomes a candidate fix rather than an adjacent issue. If no, it is something else again.

Offered as a candidate with a test, not as the answer. Two hypotheses have already died here and this one has the same shape as both: it fits, and fitting is what the last two did.

And one bound on my own contribution. #88's canonical SDK line, "Our own device might have been deleted", appears zero times in this pod's entire log. That is not evidence. There is no matrix-sdk client for @julian on this pod at all: sixteen introspects, zero tool spans, no client-lifecycle line, measured earlier in this thread. A line that only a running client can emit, absent where no client runs, says nothing. The control is that the same grep returns 39 for a string that is there.

Earlier in this thread I said #88 was not the fix. That was about the cross-signing warning and it remains correct for it: publication and signing are different layers. This is a publication measurement, which is #88's own subject.

## Churn-recency is dead too, and the state to leave here is a measurement with no mechanism 2026-08-01 → 08-06 DLX8SKHK had ZERO live sessions for five days 2026-08-06 → 08-15 exactly ONE live session for nine days now has_keys = 0, sigs = 0 **A lone session for nine days did not leave a key.** So neither session count nor recency explains it: eight-session `KZ9M8ZY8` is signed, and `DLX8SKHK` is keyless through two quiescent windows. **Withdrawn, like contention-by-count before it.** The current-state reading is load-bearing rather than a gap: device keys live in `e2e_device_keys_json` keyed on `(user, device)`, persist across sessions, and go when the **device** is deleted. The same device id spans all eight sessions including one that finished on 08-01 and is still in `devices`, **so a key uploaded during the lone-session window would still be there.** One `device_lists_stream` row and zero `device_keys_json` rows corroborate: one announcement, no key lifecycle. **`has_keys = 0, sigs = 0` has now survived every explanation offered for it.** That is the state to leave on this issue: a measurement with no mechanism, rather than a mechanism. **So do not build a contention fix**, and the PVC constraint recorded above is a constraint on a design nobody should now be starting. ## Where the upload is actually gated, and a candidate that is already filed `matrix-sdk-crypto-0.17.0/src/olm/account.rs:666`: let device_keys = self.shared().not().then(|| self.device_keys()); **Device keys are offered for upload only while `shared` is false.** Once that flag latches true, `keys_for_upload` returns `None` for device keys **forever**, and every later `/keys/upload` carries one-time keys only. **That is the exact mechanism #88 describes**, open since 2026-05-21 and mergeable: the per-account `shared` flag latches on the first apparently-successful upload and is never re-verified against the homeserver, so a client operates against a device that does not exist server-side. Its repro was a device with **50 one-time keys and zero rows in `e2e_device_keys_json`**. **The discriminator is one query and it is cheap**: does `@julian`'s account hold one-time keys for `DLX8SKHK` while holding no device keys? **If yes, the state matches #88's signature exactly** and #88 becomes a candidate fix rather than an adjacent issue. If no, it is something else again. **Offered as a candidate with a test, not as the answer.** Two hypotheses have already died here and this one has the same shape as both: it fits, and fitting is what the last two did. **And one bound on my own contribution.** #88's canonical SDK line, *"Our own device might have been deleted"*, appears **zero** times in this pod's entire log. **That is not evidence.** There is no matrix-sdk client for `@julian` on this pod at all: sixteen introspects, zero tool spans, no client-lifecycle line, measured earlier in this thread. **A line that only a running client can emit, absent where no client runs, says nothing.** The control is that the same grep returns 39 for a string that is there. Earlier in this thread I said #88 was not the fix. **That was about the cross-signing warning and it remains correct for it**: publication and signing are different layers. This is a publication measurement, which is #88's own subject.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
jlxq0/matrix-mcp#130
No description provided.