Exercise Matrix SDK recovery under controlled transport failures

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a0cffe-206f-7323-89d3-b20ca4cf4bf9
This commit is contained in:
tegwick 2026-09-25 11:02:03 +02:00
parent d7b2ea49ce
commit 78bd951568
15 changed files with 590 additions and 0 deletions

3
.gitignore vendored
View file

@ -7,3 +7,6 @@
__pycache__/
.pytest_cache/
*.pyc
# Optional isolated research experiment dependencies
research/experiments/*/node_modules/

View file

@ -258,6 +258,12 @@ research/concepts/GenerativeSequence.md
research/concepts/ResponsibilityModel.md
research/examples/SchemaEvidence.md
research/examples/schema-contract.yaml
research/experiments/matrix-recovery/README.md
research/experiments/matrix-recovery/experiment.mjs
research/experiments/matrix-recovery/package-lock.json
research/experiments/matrix-recovery/package.json
research/experiments/matrix-recovery/provenance.json
research/experiments/matrix-recovery/result.json
research/plan/OpenQuestions.md
research/plan/ResearchProgram.md
research/plan/ResearchProtocol.md
@ -270,6 +276,7 @@ research/reviews/JoplinConflictScreen.json
research/reviews/LLMConnectPilot.md
research/reviews/LocalFirstAdmissionScreen.json
research/reviews/LocalFirstEvidenceExpansion.md
research/reviews/MatrixRecoveryExperiment.md
research/reviews/ObsidianConflictScreen.json
research/reviews/RecoveryReviewMetrics.json
research/reviews/RecoveryWalkthroughReview.md

View file

@ -32,3 +32,5 @@ Its checked-in fixture verifies the pipeline and is excluded from research evide
Current contrast: [VC-01 server-validated editing](walkthroughs/VC01-TransactionalAdministration.md), [VC-02 offline editing](walkthroughs/VC02-OfflineFieldNotes.md), and [cross-case assessment](reviews/VC01-VC02-Comparison.md).
Latest implementation evidence: [frontend recovery](reviews/FrontendRecoveryImplementations.md) and [walkthrough correction review](reviews/RecoveryWalkthroughReview.md).
Executable evidence: [Matrix SDK recovery experiment](experiments/matrix-recovery/README.md), with a synthetic receiver and explicit limits.

View file

@ -46,3 +46,13 @@ Correlation IDs, server deduplication and authoritative acceptance require separ
evidence. A later matching authoritative event can resolve uncertainty; stale errors
must not overwrite stronger evidence. Keep hypothesis maturity; assess whether this
is best retained as a shared review model rather than a standalone pattern.
## Executable observation — 2026-09-25
[Matrix SDK experiment](../reviews/MatrixRecoveryExperiment.md) X01–X03 executes
retry and early-echo behavior with a synthetic receiver. A rejected send promise
can coexist with an already reconciled event. Separate transport-attempt outcome
from domain evidence. Cancelling a local pending record does not revoke a remotely
accepted operation; issuing a new identity afterward can duplicate logical intent.
This is one partial implementation observation in the Element/Matrix family, not
independent recurrence or proof of real-server deduplication.

View file

@ -72,3 +72,11 @@ The [manifest](../reviews/FrontendRecoverySources.json) records exact revisions,
file URLs and SHA-256 hashes. State: bounded static review; no runtime validation.
Element and its SDK are one implementation family. Complete-pattern recurrence
remains unproven despite two partial draft-preservation uses.
## Executable SDK follow-up — 2026-09-25
SRC-0017: published matrix-js-sdk 42.3.0; npm archive and integrity recorded in
[experiment provenance](../experiments/matrix-recovery/provenance.json). State:
executed under controlled synthetic transport. Four cases pass; server guarantees
remain assumptions. This is the SRC-0014/0015 implementation family, not another
independent application. [Review](../reviews/MatrixRecoveryExperiment.md).

View file

@ -0,0 +1,66 @@
# Matrix recovery experiment
Bounded implementation experiment, executed 2026-09-25. It runs the unmodified
published `matrix-js-sdk@42.3.0` package with an injected fetch function. This tests
real SDK state transitions against a **synthetic receiver**, not Element UI or a
real Matrix homeserver. No messages leave the process and no inference is used.
Question: can caller-visible request failure disagree with acceptance evidence, and
what happens to identity and local state on retry or early remote echo?
Hypothesis: request promises, pending-event status and authoritative event evidence
are distinct. A counterexample would be a late error regressing an already reconciled
event, or a retry changing the transaction identity/payload.
## Reproduce
Node >=22 is required by the pinned SDK; recorded run used Node 24.11.1. From this
directory, install the locked dependencies and execute:
```sh
npm ci --ignore-scripts --no-audit --no-fund
npm test
```
Alternatively install the same locked dependencies outside the repo, then set
`FEP_MATRIX_SDK_DIR` to that installation's `node_modules/matrix-js-sdk` directory
when invoking `node experiment.mjs`. The script checks the version and reports hashes
of the loaded SDK files, its own source and the dependency lock. Compare them with
[result.json](result.json); the alternate path alone does not establish provenance.
Do not overwrite the recorded result with a new run; retain new evidence separately.
[package-lock.json](package-lock.json) pins package integrity and transitive versions.
[provenance.json](provenance.json) separates the published runtime package from the
Element source/SDK pairing. The Element lockfile does match the previously inspected
SDK commit, but this published package is not claimed to be that exact build.
## Controls and results
| Case | Observed SDK behavior | Synthetic receiver observation |
| --- | --- | --- |
| Accept, lose response, resend same event | NOT_SENT retains content; resend preserves transaction/path/body; echo leaves one timeline entry | Two requests, one stored acceptance |
| Fresh identity after uncertain outcome | Fresh send initially blocked by pending queue; explicit local cancellation permits a new send | Same content accepted twice under different identities |
| Echo before successful response | Pending entry reconciled once; later success does not duplicate it | One acceptance |
| Echo before transport error | Send promise rejects, but reconciled event remains non-pending with one timeline entry | One acceptance |
All four assertions passed in the recorded run. The negative control confirms that
the fixture distinguishes identity reuse from replacement; it does not demonstrate
a production duplication incident. Receiver deduplication is programmed into this
fixture, so its correctness on a real server remains an assumption.
Automatic scheduler retries are disabled to expose the failed attempt. Remote echo
is injected through Room.addLiveEvents; no real /sync loop, encryption, browser UI,
storage restart, device change, authorization failure or server retention is tested.
The fake response loss is a thrown transport error after fixture acceptance, not a
real packet drop. No runtime conclusion about Zulip follows from this SDK experiment.
## Interpretation
The strongest result is the disagreement between a rejected request promise and an
already reconciled event. Treat promises as attempt results, not complete domain
outcomes. Preserve operation identity and submitted content across retry. Local
cancellation removes local tracking; it cannot undo an accepted remote operation.
This supports parts of Mutation Outcome Tracking. It does not prove that its exact
four-state vocabulary recurs independently or improves architecture decisions.
See [editorial review](../../reviews/MatrixRecoveryExperiment.md).

View file

@ -0,0 +1,134 @@
import assert from 'node:assert/strict';
import {createHash} from 'node:crypto';
import {readFileSync} from 'node:fs';
import {pathToFileURL} from 'node:url';
// An installed SDK directory can be supplied to keep node_modules outside the repo.
const sdkDir = process.env.FEP_MATRIX_SDK_DIR;
const sdk = await import(sdkDir ? pathToFileURL(`${sdkDir}/lib/index.js`).href : 'matrix-js-sdk');
const {createClient, MatrixScheduler, Room, MatrixEvent} = sdk;
const loggerModule = await import(sdkDir ? pathToFileURL(`${sdkDir}/lib/logger.js`).href : 'matrix-js-sdk/lib/logger.js');
loggerModule.logger.setLevel('silent');
const installedDir = sdkDir ? pathToFileURL(`${sdkDir}/`) : new URL('./node_modules/matrix-js-sdk/', import.meta.url);
const packageUrl = new URL('package.json', installedDir);
assert.equal(JSON.parse(readFileSync(packageUrl)).version, '42.3.0');
const userId = '@research:example.invalid';
const roomId = '!research:example.invalid';
const digest = data => createHash('sha256').update(data).digest('hex');
function fixture({dropFirst = false, holdFirst = false} = {}) {
const requests = [];
// Synthetic receiver: idempotency is implemented here, not proved about a homeserver.
const accepted = new Map();
let release;
let arrived;
const firstRequest = new Promise(resolve => { arrived = resolve; });
const gate = new Promise(resolve => { release = resolve; });
const client = createClient({
baseUrl: 'https://example.invalid', userId, deviceId: 'RESEARCH',
accessToken: 'synthetic-not-a-credential',
// Disable scheduler retries to expose the first failed attempt deterministically.
scheduler: new MatrixScheduler(() => -1),
fetchFn: async (url, init) => {
const path = new URL(url).pathname;
assert.equal(init.method, 'PUT');
assert.match(path, /\/send\/m.room.message\//);
const body = JSON.parse(init.body);
requests.push({path, body});
let result = accepted.get(path);
if (result) assert.deepEqual(result.body, body, 'retry payload changed');
else {
result = {body, event_id: `$accepted-${accepted.size + 1}`};
accepted.set(path, result);
}
if (requests.length === 1) {
arrived();
if (holdFirst) await gate;
if (dropFirst) throw new TypeError('synthetic response loss after acceptance');
}
return new Response(JSON.stringify({event_id: result.event_id}), {
status: 200, headers: {'Content-Type': 'application/json'},
});
},
});
const room = new Room(roomId, client, userId, {pendingEventOrdering: 'detached'});
client.store.storeRoom(room);
async function echo() {
const [path, data] = [...accepted][0];
const txn = decodeURIComponent(path.split('/').at(-1));
await room.addLiveEvents([new MatrixEvent({
event_id: data.event_id, room_id: roomId, sender: userId,
type: 'm.room.message', content: data.body, origin_server_ts: 1,
unsigned: {transaction_id: txn},
})], {});
}
return {client, room, requests, accepted, release, firstRequest, echo};
}
const results = [];
{
const f = fixture({dropFirst: true});
await assert.rejects(f.client.sendTextMessage(roomId, 'preserve this text', 'operation-a'));
const [pending] = f.room.getPendingEvents();
assert.equal(pending.status, 'not_sent');
assert.equal(f.accepted.size, 1, 'receiver accepted despite local not_sent');
assert.equal(pending.getContent().body, 'preserve this text');
const txn = pending.getTxnId();
await f.client.resendEvent(pending, f.room);
assert.equal(pending.getTxnId(), txn);
assert.equal(f.requests.length, 2);
assert.deepEqual(f.requests[0], f.requests[1]);
assert.equal(f.accepted.size, 1);
assert.equal(pending.status, 'sent');
await f.echo();
assert.equal(f.room.getPendingEvents().length, 0);
assert.equal(f.room.getLiveTimeline().getEvents().length, 1);
results.push({case: 'lost-response-same-operation-retry', passed: true,
requests: 2, synthetic_receiver_commits: 1, status_after_loss: 'not_sent',
pending_after_echo: 0, timeline_events: 1});
f.client.stopClient();
}
{
const f = fixture({dropFirst: true});
await assert.rejects(f.client.sendTextMessage(roomId, 'same intent', 'operation-b'));
await assert.rejects(f.client.sendTextMessage(roomId, 'same intent', 'operation-b-new'), /blocked/);
assert.equal(f.requests.length, 1, 'pending queue should block a fresh send');
for (const event of [...f.room.getPendingEvents()]) f.client.cancelPendingEvent(event);
await f.client.sendTextMessage(roomId, 'same intent', 'operation-b-after-cancel');
assert.equal(f.accepted.size, 2);
results.push({case: 'negative-control-new-identity', passed: true,
synthetic_receiver_commits: 2, fresh_send_initially_blocked: true,
conclusion: 'cancelling local pending records then using a new identity permits a duplicate logical send'});
f.client.stopClient();
}
for (const dropFirst of [false, true]) {
const f = fixture({holdFirst: true, dropFirst});
// Attach rejection handling immediately, before deliberately delaying the response.
const outcome = f.client.sendTextMessage(roomId, 'echo first', 'operation-c')
.then(() => 'resolved', () => 'rejected');
await f.firstRequest;
const pending = f.room.getPendingEvents()[0];
await f.echo();
assert.equal(f.room.getPendingEvents().length, 0);
assert.equal(pending.status, null);
f.release();
const promiseOutcome = await outcome;
assert.equal(promiseOutcome, dropFirst ? 'rejected' : 'resolved');
assert.equal(f.room.getPendingEvents().length, 0);
assert.equal(f.room.getLiveTimeline().getEvents().length, 1);
assert.equal(pending.status, null, 'late transport result regressed reconciled event');
results.push({case: dropFirst ? 'echo-before-transport-error' : 'echo-before-success-response',
passed: true, promise_outcome: promiseOutcome, event_status: pending.status,
pending_events: 0, timeline_events: 1});
f.client.stopClient();
}
console.log(JSON.stringify({
kind: 'real-sdk-with-synthetic-transport', sdk: 'matrix-js-sdk@42.3.0',
node: process.version, live_server: false, encryption: false,
scheduler_automatic_retries: false,
experiment_sha256: digest(readFileSync(new URL(import.meta.url))),
lock_sha256: digest(readFileSync(new URL('./package-lock.json', import.meta.url))),
sdk_file_sha256: Object.fromEntries(['package.json', 'lib/client.js', 'lib/models/room.js', 'lib/scheduler.js']
.map(file => [file, digest(readFileSync(new URL(file, installedDir)))])),
results,
}, null, 2));

View file

@ -0,0 +1,170 @@
{
"name": "fep-matrix-recovery",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"matrix-js-sdk": "42.3.0"
},
"name": "fep-matrix-recovery"
},
"node_modules/@babel/runtime": {
"version": "8.0.5",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-8.0.5.tgz",
"integrity": "sha512-7NK+Lz3spQ52XsUGTxIEVU4jYN2/dIaX8sTxRFAUtYmttYZnVh3aehiihv+/Gb7+duMNHl2OrFcBQRyOHScxpg==",
"license": "MIT"
},
"node_modules/@matrix-org/matrix-sdk-crypto-wasm": {
"version": "18.9.0",
"resolved": "https://registry.npmjs.org/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-18.9.0.tgz",
"integrity": "sha512-NtLzNDTEfNASBjNCTGZHuk7N0jrQhS6Zx2CM857rCHZYYh/rpbKGQNrJ7azktm4S1P5Jy3WU3nwJTGohGw26qQ==",
"license": "Apache-2.0",
"engines": {
"node": ">= 18"
}
},
"node_modules/@types/events": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.3.tgz",
"integrity": "sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g==",
"license": "MIT"
},
"node_modules/another-json": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/another-json/-/another-json-0.2.0.tgz",
"integrity": "sha512-/Ndrl68UQLhnCdsAzEXLMFuOR546o2qbYRqCglaNHbjXrwG1ayTcdwr3zkSGOGtGXDyR5X9nCFfnyG2AFJIsqg==",
"license": "Apache-2.0"
},
"node_modules/base-x": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz",
"integrity": "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==",
"license": "MIT"
},
"node_modules/bs58": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz",
"integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==",
"license": "MIT",
"dependencies": {
"base-x": "^5.0.0"
}
},
"node_modules/content-type": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz",
"integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
"license": "MIT",
"engines": {
"node": ">=0.8.x"
}
},
"node_modules/is-network-error": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.2.tgz",
"integrity": "sha512-PhBY86zaxNZUuWP6h13Vu5oFe0XY6/UlKzQnYFELzGVHygP3MxmvTfYSG7GN3aIab/iWudSMgjSnG9Dq+nHrgA==",
"license": "MIT",
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/loglevel": {
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz",
"integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==",
"license": "MIT",
"engines": {
"node": ">= 0.6.0"
},
"funding": {
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/loglevel"
}
},
"node_modules/matrix-events-sdk": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz",
"integrity": "sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==",
"license": "Apache-2.0"
},
"node_modules/matrix-js-sdk": {
"version": "42.3.0",
"resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-42.3.0.tgz",
"integrity": "sha512-jXXFLlaA25zQ2oxXCgbAEubGC8KpF8OabSf387W0RXfkwqhNXEbU+veeUaZMT+bCbyE5nDxkN3iBy038VbntQg==",
"license": "Apache-2.0",
"dependencies": {
"@babel/runtime": "^8.0.0",
"@matrix-org/matrix-sdk-crypto-wasm": "^18.4.0",
"another-json": "^0.2.0",
"bs58": "^6.0.0",
"content-type": "^2.0.0",
"loglevel": "^1.9.2",
"matrix-events-sdk": "0.0.1",
"matrix-widget-api": "^1.18.0",
"p-retry": "8",
"sdp-transform": "^3.0.0",
"unhomoglyph": "^1.0.6"
},
"engines": {
"node": ">=22.0.0"
}
},
"node_modules/matrix-widget-api": {
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/matrix-widget-api/-/matrix-widget-api-1.19.0.tgz",
"integrity": "sha512-Qe7L6G1mQJphBC47/Bzp5ljPIK1UARgLiuGgS+/KmEs/eaNHkQ3+ArD0/0kvMV+d6kCGAZ9OKRqsnuPjizI+tw==",
"license": "Apache-2.0",
"dependencies": {
"@types/events": "^3.0.0",
"events": "^3.2.0"
}
},
"node_modules/p-retry": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-8.0.1.tgz",
"integrity": "sha512-NAigyu8hfvJe7MsS18mnc4is0MZtau3QMdBklaJKK23oBQb6occO3UPM3vHmTckW8KhfyjMZaUOqdTFEYliHgg==",
"license": "MIT",
"dependencies": {
"is-network-error": "^1.3.0"
},
"engines": {
"node": ">=22"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/sdp-transform": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-3.0.0.tgz",
"integrity": "sha512-gfYVRGxjHkGF2NPeUWHw5u6T/KGFtS5/drPms73gaSuMaVHKCY3lpLnGDfswVQO0kddeePoti09AwhYP4zA8dQ==",
"license": "MIT",
"bin": {
"sdp-verify": "checker.js"
}
},
"node_modules/unhomoglyph": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/unhomoglyph/-/unhomoglyph-1.0.6.tgz",
"integrity": "sha512-7uvcWI3hWshSADBu4JpnyYbTVc7YlhF5GDW/oPD5AxIxl34k4wXR3WDkPnzLxkN32LiTCTKMQLtKVZiwki3zGg==",
"license": "MIT"
}
}
}

View file

@ -0,0 +1,7 @@
{
"name": "fep-matrix-recovery",
"private": true,
"type": "module",
"scripts": {"test": "node experiment.mjs"},
"dependencies": {"matrix-js-sdk": "42.3.0"}
}

View file

@ -0,0 +1,36 @@
{
"accessed": "2026-09-25",
"element_revision": "60b54fc8446ad1a88963660927cd7e1865905e63",
"element_lock_url": "https://github.com/element-hq/element-web/blob/60b54fc8446ad1a88963660927cd7e1865905e63/pnpm-lock.yaml#L663",
"element_lock_sha256": "b331406a2d154b5d398520dba7af8def58f50f40868bd017386e565fd411a976",
"element_resolved_sdk_commit": "c0cf1015980b48a6f1332701cd731740d3f58272",
"runtime_package": {
"version": "42.3.0",
"resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-42.3.0.tgz",
"integrity": "sha512-jXXFLlaA25zQ2oxXCgbAEubGC8KpF8OabSf387W0RXfkwqhNXEbU+veeUaZMT+bCbyE5nDxkN3iBy038VbntQg==",
"license": "Apache-2.0",
"dependencies": {
"@babel/runtime": "^8.0.0",
"@matrix-org/matrix-sdk-crypto-wasm": "^18.4.0",
"another-json": "^0.2.0",
"bs58": "^6.0.0",
"content-type": "^2.0.0",
"loglevel": "^1.9.2",
"matrix-events-sdk": "0.0.1",
"matrix-widget-api": "^1.18.0",
"p-retry": "8",
"sdp-transform": "^3.0.0",
"unhomoglyph": "^1.0.6"
},
"engines": {
"node": ">=22.0.0"
}
},
"runtime_is_element_locked_build": false,
"runtime_git_revision": "not reported by npm metadata; do not equate version with source HEAD",
"transport": "in-process fetchFn; no real server or network requests during cases",
"initial_harness_corrections": [
"addLiveEvents requires an options object; supplied {}",
"SDK blocks fresh sends behind failed pending events; negative control now asserts this then explicitly cancels local pending records before fresh send"
]
}

View file

@ -0,0 +1,50 @@
{
"kind": "real-sdk-with-synthetic-transport",
"sdk": "matrix-js-sdk@42.3.0",
"node": "v24.11.1",
"live_server": false,
"encryption": false,
"scheduler_automatic_retries": false,
"experiment_sha256": "244016a1b7abf802283397ee10e4d54e67863f3b5cac012cd473c23841cb8f69",
"lock_sha256": "c371f1c9770a1f51bc9e6a500ecbcc75c77b19657be6db46986fbc7a55afe73d",
"sdk_file_sha256": {
"package.json": "1d16942ff31bf62e7ba13caa624df51de2cbfe216939dca1fc63b6cf3dbd4ecb",
"lib/client.js": "e125a02b7747d681abddba93f871ca94b60baf4a0e686227acbc9f2b7f7223db",
"lib/models/room.js": "08414847bfd6c5fea08ee6f1c7e2263a1239b64dd11785aa2360e0d129dfe145",
"lib/scheduler.js": "57e95775529974c3030656da090804f4eebe54f4406689a9a74e605cee262f7d"
},
"results": [
{
"case": "lost-response-same-operation-retry",
"passed": true,
"requests": 2,
"synthetic_receiver_commits": 1,
"status_after_loss": "not_sent",
"pending_after_echo": 0,
"timeline_events": 1
},
{
"case": "negative-control-new-identity",
"passed": true,
"synthetic_receiver_commits": 2,
"fresh_send_initially_blocked": true,
"conclusion": "cancelling local pending records then using a new identity permits a duplicate logical send"
},
{
"case": "echo-before-success-response",
"passed": true,
"promise_outcome": "resolved",
"event_status": null,
"pending_events": 0,
"timeline_events": 1
},
{
"case": "echo-before-transport-error",
"passed": true,
"promise_outcome": "rejected",
"event_status": null,
"pending_events": 0,
"timeline_events": 1
}
]
}

View file

@ -98,3 +98,12 @@ acceptance-with-lost-response plus delayed echo, checking operation identity and
content preservation across retry. Under T03: compare blinded, matched baseline and
pattern-guided decisions on new variants with an independent reviewer. The present
self-authored cases cannot establish that names improve decisions.
## Follow-up — source pairing and execution
The [Element lockfile](https://github.com/element-hq/element-web/blob/60b54fc8446ad1a88963660927cd7e1865905e63/pnpm-lock.yaml#L663)
resolves SDK commit c0cf1015980b48a6f1332701cd731740d3f58272, closing the earlier
source pairing gap. A [separate published SDK experiment](MatrixRecoveryExperiment.md)
now exercises recovery with a synthetic receiver. It is not the locked Element
build or a production server test. Earlier statements of static-only evidence
above describe the original inspection scope.

View file

@ -0,0 +1,62 @@
# Executable recovery evidence
Codex, 2026-09-25; baseline `d7b2ea4`. Scope: one published SDK implementation under
a controlled transport. [Experiment](../experiments/matrix-recovery/README.md),
[source](../experiments/matrix-recovery/experiment.mjs),
[results](../experiments/matrix-recovery/result.json), and
[provenance](../experiments/matrix-recovery/provenance.json) are retained.
## Findings and disposition
X01: Loss of a response after simulated acceptance leaves content in an SDK pending
event labelled NOT_SENT. Resending that event preserves path, payload and transaction
identity. This is executable evidence for client retention/retry behavior; duplicate
prevention on a real server is not established because the receiver is synthetic.
X02: The SDK blocks a fresh send while a failed event is pending. After explicit
local cancellation, a new identity for the same content produces a second fixture
acceptance. Keep that queue guard in the description; omitting it would overstate
how readily the client creates a duplicate. Cancellation is not remote rollback.
X03: When a matching remote event arrives first, both later success and later
transport failure leave one reconciled timeline entry. In the failure case the
send promise still rejects. Therefore a caller cannot derive domain acceptance
solely from that promise's result. This directly exercises the evidence-precedence
requirement proposed in VC-01.
X04: Element's pinned lockfile resolves the exact SDK commit previously inspected.
This closes the source pairing question from FrontendRecoveryImplementations. It
still does not prove a deployed build, and the runtime experiment deliberately
uses a separately pinned published SDK version, not that source checkout.
## Effect on the language
Retain eight drafts. Strengthen Mutation Outcome Tracking with attempt/domain
separation and explicit cancellation semantics; no new pattern is needed. Use the
experiment as one partial implementation observation, not a second independent
use alongside Element (same family). Local Draft State's complete merge contract,
VC-02 durability, frontend accessibility and baseline usefulness remain unverified.
No llm-connect call was needed: these findings come from executable assertions and
source/lockfile inspection. This is a narrow increase in empirical evidence, not
canonical admission or M1 completion.
## Next bounded comparison
Under T03, prepare matched tasks for an independent reviewer: same context and
information budget, pattern labels available in one condition and plain narrative
in the other. Score missed constraints, unsupported guarantees and correct recovery
choices; do not score use of preferred names. Counterbalance order and freeze the
corpus before drafting unseen variants. Until a reviewer completes that comparison,
decision usefulness stays unknown.
Under T02, keep the real client/server lost-response experiment open. This harness
reduces uncertainty about SDK behavior but intentionally does not validate server
deduplication, browser storage or a full Element deployment. Select a local disposable
server only if those server guarantees become the next question; no production
deployment is part of this research cycle.
Validation: a fresh `npm ci --ignore-scripts` installation from the checked-in lock
reproduced all four results and loaded SDK hashes exactly. The 35 existing repository
tests passed; local links and staged whitespace were checked. The four experiment
cases are additional SDK assertions, not frontend end-to-end tests.

View file

@ -122,3 +122,13 @@ Correlate every acknowledgement, server event and error to its original operatio
Once authoritative evidence confirms z, a late transport error cannot regress it to
unknown; a receipt for z cannot clear a new draft or mark a later operation accepted.
A status label such as failed_request is not itself rejection evidence.
## Executable supporting observation — 2026-09-25
[Matrix recovery experiment](../reviews/MatrixRecoveryExperiment.md) X03 exercises
one related client mechanism: an early server event can reconcile an operation even
when its request promise later rejects. In this case, derive z's outcome from
correlated authoritative evidence rather than treating any rejected promise as a
failed approval. A user cancelling local tracking cannot undo z; reversal requires
a separate authorized domain action after its outcome is known. The SDK experiment
does not implement this approval application or its server transaction contract.

View file

@ -260,3 +260,19 @@ response/delayed echo recovery; retain existing offline crash/ancestry/accessibi
gaps. T03 prepare a blinded matched comparison using unseen variants and independent
review. See research/reviews/FrontendRecoveryImplementations.md and
research/reviews/RecoveryWalkthroughReview.md. T02/T03 remain progress.
## Executable SDK recovery experiment — 2026-09-25
T02: resolved Element lockfile to the previously inspected SDK commit. Separately
executed published matrix-js-sdk 42.3.0 with a locked dependency set and controlled
transport. Four cases pass: lost response/same-event retry, negative control with
fresh identity after local cancellation, echo before success, and echo before error.
A rejected promise can coexist with an already reconciled event. The SDK initially
blocks fresh sends behind unresolved pending events; preserve this qualification.
Receiver deduplication is synthetic, not real-server evidence. No production changes,
external messages or inference calls. T02 remains progress for actual server,
offline storage/ancestry and accessibility guarantees. T03 remains progress for an
independent matched baseline comparison; experiment does not settle usefulness.
Eight drafts remain hypotheses, with no canonical admission or M1 completion.
See research/reviews/MatrixRecoveryExperiment.md and reproducible experiment files.