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:
parent
d7b2ea49ce
commit
78bd951568
15 changed files with 590 additions and 0 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -7,3 +7,6 @@
|
|||
__pycache__/
|
||||
.pytest_cache/
|
||||
*.pyc
|
||||
|
||||
# Optional isolated research experiment dependencies
|
||||
research/experiments/*/node_modules/
|
||||
|
|
|
|||
7
TREE.md
7
TREE.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
66
research/experiments/matrix-recovery/README.md
Normal file
66
research/experiments/matrix-recovery/README.md
Normal 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).
|
||||
134
research/experiments/matrix-recovery/experiment.mjs
Normal file
134
research/experiments/matrix-recovery/experiment.mjs
Normal 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));
|
||||
170
research/experiments/matrix-recovery/package-lock.json
generated
Normal file
170
research/experiments/matrix-recovery/package-lock.json
generated
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
7
research/experiments/matrix-recovery/package.json
Normal file
7
research/experiments/matrix-recovery/package.json
Normal 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"}
|
||||
}
|
||||
36
research/experiments/matrix-recovery/provenance.json
Normal file
36
research/experiments/matrix-recovery/provenance.json
Normal 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"
|
||||
]
|
||||
}
|
||||
50
research/experiments/matrix-recovery/result.json
Normal file
50
research/experiments/matrix-recovery/result.json
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
62
research/reviews/MatrixRecoveryExperiment.md
Normal file
62
research/reviews/MatrixRecoveryExperiment.md
Normal 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.
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue