From 78bd95156832c9c48c0e17947a406396c7660088 Mon Sep 17 00:00:00 2001 From: tegwick Date: Fri, 25 Sep 2026 11:02:03 +0200 Subject: [PATCH] Exercise Matrix SDK recovery under controlled transport failures Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a0cffe-206f-7323-89d3-b20ca4cf4bf9 --- .gitignore | 3 + TREE.md | 7 + research/README.md | 2 + .../candidates/MutationOutcomeTracking.md | 10 ++ research/catalog/SourceCatalog.md | 8 + .../experiments/matrix-recovery/README.md | 66 +++++++ .../matrix-recovery/experiment.mjs | 134 ++++++++++++++ .../matrix-recovery/package-lock.json | 170 ++++++++++++++++++ .../experiments/matrix-recovery/package.json | 7 + .../matrix-recovery/provenance.json | 36 ++++ .../experiments/matrix-recovery/result.json | 50 ++++++ .../FrontendRecoveryImplementations.md | 9 + research/reviews/MatrixRecoveryExperiment.md | 62 +++++++ .../VC01-TransactionalAdministration.md | 10 ++ .../FEP-WP-0002-seed-language-validation.md | 16 ++ 15 files changed, 590 insertions(+) create mode 100644 research/experiments/matrix-recovery/README.md create mode 100644 research/experiments/matrix-recovery/experiment.mjs create mode 100644 research/experiments/matrix-recovery/package-lock.json create mode 100644 research/experiments/matrix-recovery/package.json create mode 100644 research/experiments/matrix-recovery/provenance.json create mode 100644 research/experiments/matrix-recovery/result.json create mode 100644 research/reviews/MatrixRecoveryExperiment.md diff --git a/.gitignore b/.gitignore index 8c21869..24233e3 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,6 @@ __pycache__/ .pytest_cache/ *.pyc + +# Optional isolated research experiment dependencies +research/experiments/*/node_modules/ diff --git a/TREE.md b/TREE.md index 24b9d6b..d5dc7cd 100644 --- a/TREE.md +++ b/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 diff --git a/research/README.md b/research/README.md index 3b4588c..c1bb7b2 100644 --- a/research/README.md +++ b/research/README.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. diff --git a/research/candidates/MutationOutcomeTracking.md b/research/candidates/MutationOutcomeTracking.md index 6b565db..46b7b71 100644 --- a/research/candidates/MutationOutcomeTracking.md +++ b/research/candidates/MutationOutcomeTracking.md @@ -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. diff --git a/research/catalog/SourceCatalog.md b/research/catalog/SourceCatalog.md index a30896c..5729650 100644 --- a/research/catalog/SourceCatalog.md +++ b/research/catalog/SourceCatalog.md @@ -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). diff --git a/research/experiments/matrix-recovery/README.md b/research/experiments/matrix-recovery/README.md new file mode 100644 index 0000000..37dad2c --- /dev/null +++ b/research/experiments/matrix-recovery/README.md @@ -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). diff --git a/research/experiments/matrix-recovery/experiment.mjs b/research/experiments/matrix-recovery/experiment.mjs new file mode 100644 index 0000000..6fca515 --- /dev/null +++ b/research/experiments/matrix-recovery/experiment.mjs @@ -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)); diff --git a/research/experiments/matrix-recovery/package-lock.json b/research/experiments/matrix-recovery/package-lock.json new file mode 100644 index 0000000..e1b8634 --- /dev/null +++ b/research/experiments/matrix-recovery/package-lock.json @@ -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" + } + } +} diff --git a/research/experiments/matrix-recovery/package.json b/research/experiments/matrix-recovery/package.json new file mode 100644 index 0000000..3aa97b2 --- /dev/null +++ b/research/experiments/matrix-recovery/package.json @@ -0,0 +1,7 @@ +{ + "name": "fep-matrix-recovery", + "private": true, + "type": "module", + "scripts": {"test": "node experiment.mjs"}, + "dependencies": {"matrix-js-sdk": "42.3.0"} +} diff --git a/research/experiments/matrix-recovery/provenance.json b/research/experiments/matrix-recovery/provenance.json new file mode 100644 index 0000000..d4a7054 --- /dev/null +++ b/research/experiments/matrix-recovery/provenance.json @@ -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" + ] +} diff --git a/research/experiments/matrix-recovery/result.json b/research/experiments/matrix-recovery/result.json new file mode 100644 index 0000000..57807b5 --- /dev/null +++ b/research/experiments/matrix-recovery/result.json @@ -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 + } + ] +} diff --git a/research/reviews/FrontendRecoveryImplementations.md b/research/reviews/FrontendRecoveryImplementations.md index a12b327..0b6f8a3 100644 --- a/research/reviews/FrontendRecoveryImplementations.md +++ b/research/reviews/FrontendRecoveryImplementations.md @@ -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. diff --git a/research/reviews/MatrixRecoveryExperiment.md b/research/reviews/MatrixRecoveryExperiment.md new file mode 100644 index 0000000..31accfb --- /dev/null +++ b/research/reviews/MatrixRecoveryExperiment.md @@ -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. diff --git a/research/walkthroughs/VC01-TransactionalAdministration.md b/research/walkthroughs/VC01-TransactionalAdministration.md index 2cb60e9..40c7664 100644 --- a/research/walkthroughs/VC01-TransactionalAdministration.md +++ b/research/walkthroughs/VC01-TransactionalAdministration.md @@ -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. diff --git a/workplans/FEP-WP-0002-seed-language-validation.md b/workplans/FEP-WP-0002-seed-language-validation.md index e5b5c4f..b9c83d2 100644 --- a/workplans/FEP-WP-0002-seed-language-validation.md +++ b/workplans/FEP-WP-0002-seed-language-validation.md @@ -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.