web+native: the setup flow's known-name skip is RECORDED — clients converge (#1179)
Paul's handoff test surfaced a permanent divergence: setup onboarding
started on the web (name never answered), then a Mac joined mid-flow
via an add-device link — which ships the member's name — and the two
clients disagreed about the current step forever. The name step's
skipIf mixed a log-derived signal (done) with a device-local one
(bridge.myName), and a bare skipIf writes nothing, so the name-knowing
client advanced to "group" while the nameless one stayed armed on
"Your name:" with no event ever reconciling them (the missing
"Fill in a form instead" chip on native was the same skip, downstream).
Fix is spec-only, in both twins (setup.flow.js + Flows/setup.json,
lockstepped by the drift gate): the known-name shortcut becomes a
recorded entry — entries: [{ if: "bridge.myName", recordIfNew, goto:
"group" }] — the exact pattern the keys step already uses, so the first
client that resolves the name writes done.name into the shared log and
every other client folds to the same position. resumedIf still ignores
bridge.myName on purpose: the creator's first run keeps its greeting.
Regression test: a name-knowing client records the skip; a nameless
client folding that record converges on group. Both engines' suites
pass untouched.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
4 files changed,
+54
−12
ios/LChatCore/Package.resolved+0 −9ios/LChatCore/Sources/LChatCore/Flows/setup.json+11 −2test/flow.test.mjs+27 −0web/static/setup.flow.js+16 −1
diff --git a/ios/LChatCore/Package.resolved b/ios/LChatCore/Package.resolved| index fe25d1c..d82f91f 100644 |
| --- a/ios/LChatCore/Package.resolved |
| +++ b/ios/LChatCore/Package.resolved |
| @@ -1,14 +1,5 @@ |
| { |
| "pins" : [ |
| - { |
| - "identity" : "sparkle", |
| - "kind" : "remoteSourceControl", |
| - "location" : "https://github.com/sparkle-project/Sparkle", |
| - "state" : { |
| - "revision" : "b6496a74a087257ef5e6da1c5b29a447a60f5bd7", |
| - "version" : "2.9.4" |
| - } |
| - }, |
| { |
| "identity" : "swift-cmark", |
| "kind" : "remoteSourceControl", |
diff --git a/ios/LChatCore/Sources/LChatCore/Flows/setup.json b/ios/LChatCore/Sources/LChatCore/Flows/setup.json| index be120b9..5740c80 100644 |
| --- a/ios/LChatCore/Sources/LChatCore/Flows/setup.json |
| +++ b/ios/LChatCore/Sources/LChatCore/Flows/setup.json |
| @@ -35,9 +35,18 @@ |
| }, |
| { |
| "id": "name", |
| + "entries": [ |
| + { |
| + "if": "bridge.myName", |
| + "recordIfNew": { |
| + "label": "noted the name you go by", |
| + "value": "known" |
| + }, |
| + "goto": "group" |
| + } |
| + ], |
| "skipIf": [ |
| - "done", |
| - "bridge.myName" |
| + "done" |
| ], |
| "bubbles": [ |
| { |
diff --git a/test/flow.test.mjs b/test/flow.test.mjs| index 14e3773..8baa06a 100644 |
| --- a/test/flow.test.mjs |
| +++ b/test/flow.test.mjs |
| @@ -229,6 +229,33 @@ await test("setup: the full first-run transcript, happy path with detours", asyn |
| assert.ok(h.ended, "flow signalled completion"); |
| }); |
| +await test("setup: a device that already knows the name RECORDS the skip so other clients converge", async () => { |
| + // The 2026-07-19 web-vs-Mac divergence: a second device joining mid-flow |
| + // with the member's name already known (an add-device link ships it in the |
| + // fragment) skipped the name step via a bare skipIf — nothing landed in |
| + // the log, so a client that did NOT know the name stayed armed on |
| + // "Your name:" forever while this one moved on. The skip must be a |
| + // recorded entry: done.name folds to every client. |
| + const h = harness({ myName: "Paul" }); |
| + runFlow(SETUP_FLOW, h.env, h.io); |
| + await tick(); |
| + |
| + // First run with a name: still greeted (resumedIf deliberately ignores |
| + // bridge.myName — the creator's first run keeps its hello), the name |
| + // question never asked, and — the fix — the skip is IN THE LOG. |
| + assert.match(h.transcript[0].text, /^Hi — I'm Eleven!/); |
| + assert.equal(h.transcript.some((t) => /what should everyone here call you/.test(t.text)), false); |
| + assert.ok(h.records.find((r) => r.step === "name"), "the known-name skip writes done.name"); |
| + |
| + // A nameless client folding that record lands on the same step (group) |
| + // instead of re-arming the name question. |
| + const h2 = harness({ done: { name: true } }); |
| + runFlow(SETUP_FLOW, h2.env, h2.io); |
| + await tick(); |
| + assert.equal(h2.transcript.some((t) => /what should everyone here call you/.test(t.text)), false); |
| + assert.ok(h2.transcript.find((t) => t.extra.key === "q-group"), "converged on the group step"); |
| +}); |
| + |
| await test("setup: resumed run — skips, Keep chip, skip-everything path", async () => { |
| // Resumed = a recorded step (done.name), not merely a set name — a name |
| // alone is the native creator's FIRST run and still gets the greeting. |
diff --git a/web/static/setup.flow.js b/web/static/setup.flow.js| index 3804b67..dbb9d83 100644 |
| --- a/web/static/setup.flow.js |
| +++ b/web/static/setup.flow.js |
| @@ -30,7 +30,22 @@ export default { |
| }, |
| { |
| id: "name", |
| - skipIf: ["done", "bridge.myName"], |
| + // A device that already knows this member's name (an add-device link |
| + // ships it in the fragment; a native shell carries the rail's) must |
| + // not skip PAST this step with a bare skipIf: that writes nothing to |
| + // the log, so every client that does NOT know the name stays parked |
| + // here forever while this one moves on — two composers armed on |
| + // different steps of the same thread (the 2026-07-19 web-vs-Mac |
| + // divergence). The entry RECORDS the settled name, so done.name folds |
| + // to every client and they all derive the same position from the log. |
| + entries: [ |
| + { |
| + if: "bridge.myName", |
| + recordIfNew: { label: "noted the name you go by", value: "known" }, |
| + goto: "group", |
| + }, |
| + ], |
| + skipIf: ["done"], |
| bubbles: [ |
| { text: "First — what should everyone here call you? Reply with your name.", label: "Your name:", hint: "e.g. Finn Wolfhard" }, |
| ], |