FKITDEV-8787 / SLARAFIPI-60 — Raiffeisen Myra “Already authorized” / “Already has some kind of room”
One-line summary
Three Raiffeisen Myra mobile self-service rooms (186120, 186126, 186135) failed in production. Two of the three customer logs show the strings "Already authorized" and "Already has some kind of room". Triage finds these strings live in the FaceKom mobile SDK, not in vuer_oss — server-side selfService:v2:start silently resumes any non-closed room. Working hypothesis: in-app Restart button leaves stale SDK session state. Class is the same phantom-room family as FKITDEV-4736 (instrumentation, 2024-09) and same flow surface as the SLARAFIPI-53 / FKITDEV-7667 fix (2025-10).
Fix implemented (2026-06-02)
Fix pushed (no PR yet)
Server-side self-heal + fail-closed hardening landed on vuer_css branch fix/FKITDEV-8787-selfservice-v2-abort-clear-state. Two commits pushed to origin, no PR opened yet. The reported phantom-room / “Already has some kind of room” symptom is addressed server-side; the mobile-SDK angle from triage remains the upstream cause but the server now recovers from stale room state instead of refusing re-init.
The branch was originally named bugfix/FKITDEV-8787-... but TechTeamer/vuer_css enforces a branch-name regex ^(feature|feat|chore|fix|release)/FKITDEV-\d+... — bugfix/ is not in the allowed prefix set and is rejected on push. Must use fix/ (or feature/feat/chore/release). This matches the InstaCash-update note’s mention of the parallel bugfix/FKITDEV-8787 WIP that pre-dated the rename.
The fix (server/socket/events/selfservice-v2.js)
Self-heal in selfService:v2:start — before the ALREADY_HAS_ROOM throw: if the socket carries stale selfServiceRoomData, call OSS rpcClient.selfServiceV2.getRemainingSeconds(roomId). If remainingSeconds < 1 (room positively expired), delete the stale selfServiceRoomData so re-init proceeds. This is the actual fix for the phantom-room / “Already has some kind of room” bug — the server now lets a customer re-start after their room has timed out, instead of staying wedged.
Cleanup in selfService:v2:abort — delete selfServiceRoomData after the OSS abort RPC succeeds, so a subsequent start on the same socket doesn’t throw ALREADY_HAS_ROOM.
Key design learning — fail closed on ambiguity (4-perspective review)
Fail CLOSED for a duplicate-prevention bug
For a bug whose whole purpose is preventing duplicate / phantom rooms, the safe default on ambiguity is fail closed (preserve state, keep the ALREADY_HAS_ROOM guard) unless the room is positively confirmed gone. Do not treat “the RPC threw” as “the resource is dead.”
The first version of the fix used a catch-all: on any error from getRemainingSeconds it deleted the room state (“fail open”).
A multi-agent review (silent-failure + test-quality perspectives) caught that this re-introduces the duplicate-room problem from the opposite direction: a transient RPC/OSS error (network blip, OSS overload) while the room is actually alive would clear the state and let start() create a second live room, orphaning the original. (The de-registration loop at start only cleans sockets matching the new room id, so the old live room is left dangling.)
Verified against OSS: SelfServiceV2Service.getRemainingSeconds returns Math.max(0, floor((expireAt - now) / 1000)), and the RPC wrapper does resolveModels(...) first. So a genuinely timed-out room returns 0 — handled by the < 1 branch, not the catch. Therefore the catch is only reached for truly-absent rooms, and we can fail closed there (preserve selfServiceRoomData, keep the ALREADY_HAS_ROOM guard, log at error) while still fixing the reported bug.
Net: commit 923e4c70 replaced the catch-all “delete on any error” with “delete only when remainingSeconds < 1; on RPC error, preserve state + log error.”
Also verified (NOT bugs)
The abort handler is guarded by createEndpoint’s default selfServiceRoomRequired = true, so there is no unguarded .id dereference on a missing room.
The self-heal only clears selfServiceRoomData, not roomData — which is fine: self-service sockets never carry roomData.
Tests now use the realserver/auth.js predicates (via jest.requireActual) instead of hand-rolled fakes — the fakes had diverged from production: they dropped the isAuthorized / customerId conjunct and collapsed the hasAnyRoomroomData branch.
Next steps (still open)
Open PR targeting customization/raiffeisen.
Move FKITDEV-8787 from Triaged/blocked → In Review.
Coordinate with m3szi (owns the prior FKITDEV-7667 / SLARAFIPI-53 fix on the same flow surface).
Ticket chain
FKITDEV-8787 (FaceKom Dev) — triaged 2026-05-13.
SLARAFIPI-60 (Raiffeisen SLA) — partner-tagged Minor; upgraded to P2-Medium here (customer-blocking, no in-app recovery path).
Three SelfServiceRooms on Raiffeisen Myra mobile self-service: 186120, 186126, 186135.
Customer of room 186120 has a vestigial duplicate (csökevény szoba) in 186126 — same customerId.
Customer of room 186135 has no sibling room.
186126 customer never received tasks on the phone, retried 5x; after restart couldn’t pass the first photo.
Both 186126 and 186135 logs contain the strings "Already authorized" and "Already has some kind of room".
Partner (Raiffeisen) suspects the in-app “Restart” button leaves stale SDK memory.
Triage outcome
Severity: P2-Medium. Customer-blocking on the bank’s primary self-service flow with no in-app recovery — upgrades partner-tagged Minor.
Class: phantom-room family. Same surface as FKITDEV-4736 (instrumentation) and SLARAFIPI-53 / FKITDEV-7667 (2025-10 fix).
State: triaged; blocked on Raiffeisen for mobile-SDK confirmation.
Where the error strings come from
The error strings are not in vuer_oss
Verified by git grep on both devel and customization/raiffeisen: the literals "Already authorized" and "Already has some kind of room" do not appear in the OSS server source. They originate in the FaceKom mobile SDK (Android/iOS) local guard layer.
Best reading of the SDK guards:
Already authorized — SDK already holds a customer JWT in memory; refusing to re-register.
Already has some kind of room — SDK already holds a selfServiceRoomData.id cached locally; refusing to start a new room.
Server side has no equivalent throw on V2. SelfServiceV2Service.start() silently resumes:
V1 does throw at SelfServiceRoomService.js:217 (Customer already has an open room), but SelfServiceActions.js:27-34 swallows it into a generic { success: false } — so even the V1 error never surfaces to the SDK as a rich code.
Csökevény szoba mechanism (by design)
By-design, but a debugging trap
customization/raiffeisen/server/service/RaiffeisenService.js::resolveExternalToken() returns the same customer.id for the same offerId. When the bank reissues the link or the customer gets a new attempt, SelfServiceV2Service.start() sees the previous SSR is closed and creates a new SSR for the same customerId. The previous closed SSR is the “vestigial room” (csökevény). Intentional, but it makes the duplicate-room arrangement look like a bug to anyone reading the DB cold.
Suspected race that produces the rarer non-closed duplicate:
Two start() calls in quick succession on a flapping SDK both pass the _findOpenRoomForCustomer null check.
Result: two SSRs for the same customerId in non-closed states.
Nothing at the DB layer guards against this — no partial unique index on (customerId) for ('waiting','incall','left').
Working hypothesis (root-cause path)
Hypothesis pending Raiffeisen confirmation
The mobile-SDK angle is the leading hypothesis. It is consistent with all observable evidence (especially the two error strings being SDK-local), but only the Raiffeisen mobile team can confirm what Restart does to SDK state. Treat as hypothesis until they answer.
Working hypothesis sequence
sequenceDiagram
participant C as Customer (Myra app)
participant SDK as FaceKom SDK
participant OSS as vuer_oss server
participant DB as SelfServiceRoom
C->>SDK: Start KYC
SDK->>OSS: register / start
OSS->>DB: create SSR (waiting)
OSS-->>SDK: {created:true, ssrId, currentStep}
Note over C,SDK: Flow stalls (network or missing task)
C->>SDK: Tap in-app Restart
Note over SDK: clearSession() NOT called<br/>JWT + ssrData still in memory
SDK->>SDK: register guard hits → 'Already authorized'
SDK->>SDK: start guard hits → 'Already has some kind of room'
Note over OSS: never receives a fresh request
Note over DB: SSR still waiting/incall/left → no new tasks dispatched
Note over C: 'no tasks on phone'
OSS->>DB: timeout closes original SSR
Note over C: bank reissues link → new SSR for same customerId
C->>SDK: New attempt
SDK-->>C: stale state still refuses re-init → couldn't pass first photo
Mobile flow stalls (intermittent connectivity or missing task).
Customer taps in-app Restart instead of OS-killing the app.
SDK retains in-memory selfServiceRoomData + customer JWT — no clearSession().
SDK calls register / start again → SDK local guard short-circuits with the two error strings without contacting OSS.
OSS still has the previous SSR in waiting / incall / left → no new tasks dispatched → “no tasks” symptom.
Eventually OSS-side timeout closes the SSR → bank reissues → new SSR (e.g. 186126) created for same customerId → previous SSR (186120) becomes csökevény.
After the in-app restart the SDK’s stale state still refuses to re-init → “couldn’t pass first photo”.
Code surfaces (file:line)
Layer
Location
Note
OSS V2 service
server/service/SelfServiceV2Service.js:357-394
start() silently resumes any non-closed room
OSS V2 service
server/service/SelfServiceV2Service.js:224-232
_findOpenRoomForCustomer (used by V2)
OSS V1 service
server/service/SelfServiceRoomService.js:213-228
V1 throw at line 217: Customer already has an open room
resolveExternalToken() returns existing customer.id for same offerId — mechanism behind the csökevény arrangement
Open questions / data needed from Raiffeisen
Blocked on Raiffeisen data
The hypothesis cannot be confirmed without mobile-side artifacts. Items below are the asks for the partner.
Mobile SDK version + which file/symbol emits each error string.
Does the in-app “Restart” call SDK clearSession() or just re-run the mobile flow?
JWT decoded offerId for 186120 vs 186126 vs 186135 — does the shared customerId come from the same offerId?
Mobile-side log timestamps for the two error strings, correlated with server-side selfService:v2:register / :start calls.
Confirm raiffeisen.debug.phantomRoomLog is true in PROD config.
Is OneDrive_1_5-5-2026.zip (attachment on the ticket) the mobile log bundle? — almost certainly yes.
Recommended fixes (ordered)
Order
Type
Action
D
Operational, immediate
Enable raiffeisen.debug.phantomRoomLog: true in PROD; ask Raiffeisen to repro
C
Mobile, primary
In-app Restart must call selfService:v2:abort then clearSession() before re-starting
A
Server, defense in depth
In SelfServiceV2Service.start(), when _findOpenRoomForCustomer returns a room whose Flow is failed/aborted/cancelledorserviceProgress === 'preparation' and createdAt older than N minutes — abort+close it and create fresh
B
Server, long-term
Partial unique index on (customerId) WHERE status IN ('waiting','incall','left') on selfServiceRoom, with cleanup migration
E
Runbook
SQL to manually close a vestigial blocking room: UPDATE "selfServiceRoom" SET status='closed', "closedAt"=now() WHERE id=<X> — for support
F
Server, error UX
Stop swallowing the V1 throw in SelfServiceActions.js:31-34; return the actual error code
Escalation path
Ask reporter for mobile app version + the data items in Open questions.
Enable phantomRoomLog in prod.
Coordinate with m3szi (author of f830fd8e5a / FKITDEV-7667) — owns the same flow handler.
If mobile fix needed, ticket the mobile team (Raiffeisen Myra app).
Server fixes target raiffeisen first, forward-port to devel.
Cross-references
SLARAFIPI-53 / FKITDEV-8581 / FKITDEV-7667 — same flow, prior fix (no Obsidian note yet).
PR #3051 OPEN — supersedes the "Fix pushed (no PR yet)" note above
The server-side fix is now an open PR against the Raiffeisen customization branch. This section is the current source of truth for the resolution; the older “Fix implemented (2026-06-02)” section above records the same work but predates the PR (and its earlier commit SHAs 8586df65 / 923e4c70 were superseded by the rebased branch). Verified live on GitHub on 2026-06-11.
c0712c3a99, 428cbe3cca, 15ac89da1c (3 commits on the rebased branch; supersede the earlier 8586df65 / 923e4c70)
Final fix approach (PR #3051)
The fix lives entirely in vuer_cssserver/socket/events/selfservice-v2.js plus its unit test. Three behaviours:
Self-heal stale room state on selfService:v2:start. Before the ALREADY_HAS_ROOM guard, if the client still carries client.sessionData.selfServiceRoomData, the handler RPC-calls serviceContainer.rpcClient.selfServiceV2.getRemainingSeconds(roomId). If remainingSeconds is falsy or < 1 (room dead / expired), it runs delete client.sessionData.selfServiceRoomData and logs at warn (message self-heal: clearing stale selfServiceRoomData for dead room) — so a fresh room spawns instead of throwing ALREADY_HAS_ROOM. This is the actual fix for the phantom-room / “Already has some kind of room” symptom.
Fail-closed on lookup error. If getRemainingSecondsthrows, the handler does NOT delete the state — it logs at error and preservesselfServiceRoomData. Rationale embedded in the PR: an unexpected lookup failure is not proof the room is dead, and clearing on error could spawn a duplicate live room.
Abort-path cleanup. Also runs delete client.sessionData.selfServiceRoomData on the abort path (~line 569), so a subsequent start on the same socket does not re-trip ALREADY_HAS_ROOM.
Tests
test/tests/unit/socket/events/selfservice-v2.test.js adds 149 lines of unit tests covering the self-heal branches: remainingSeconds === 0, null / undefined (falsy, clears), and error-preserves-state (the fail-closed branch). Checks are green (2/2) on the PR.
Open items on the PR
Awaiting an approval decision — m3szi (owner of the prior FKITDEV-7667 / SLARAFIPI-53 fix on the same flow surface) has only left an empty-body COMMENTED review so far.
The mobile-SDK angle from triage remains the upstream cause — this PR makes the server recover from stale room state rather than refusing re-init; it does not change the SDK guards that emit the two error strings (recommendation C above stays open). PR #3051 corresponds to server-side recommendation A, scoped to the socket start / abort handlers.
FKITDEV-8787 (this ticket — phantom-room / “Already has some kind of room”) is distinct fromFKITDEV-8581 (Raiffeisen GIRinfO / logical data-match verification gate, under SLARAFIPI-53). Same partner (Raiffeisen) and same customization/raiffeisen branch documented in customization-branches, but different tickets and different fixes — they are frequently conflated. See also FKITDEV-4736 (phantom-room instrumentation).
Manual verification (partner hand-off script)
Purpose
Reusable end-to-end test procedure to verify the server-side self-heal against the real Raiffeisen Myra self-service (önkiszolgáló) flow, with a tester who has live access to it. This complements the existing automated coverage — the 76 Jest unit tests on server/socket/events/selfservice-v2.js exercise the self-heal branches in isolation, but cannot prove the fix against the actual mobile SDK that emits the “Already authorized” string.
Precondition — must be on the build with the fix
The fix exists only in PR #3064 → base branch release/FKITDEV-8902-raiffeisen-1.9.11.95 (rollout target = the 1.9.11.95 Raiffeisen release). The tester MUST be on an environment/build that has that fix deployed. On any earlier build they will simply reproduce the original bug and the test proves nothing.
Note: this PR (#3064 → release/...-1.9.11.95) is the release-targeted delivery of the same server self-heal first landed via PR #3051 (→ customization/raiffeisen) documented above.
Critical gotcha — must reuse the SAME socket (in-app Restart)
The bug only arms when the mobile SDK reuses the same socket — i.e. the in-app Restart. The tester must stay in the same app session and use in-app Restart/Cancel.
Fully closing & reopening the app (or re-scanning a fresh token) creates a NEW socket with no stale selfServiceRoomData — so the bug never triggers and the test is meaningless. If the procedure ever has the tester kill the app, the run is invalid.
Scenario A — reported failure (expired / dead room recovery)
Start a self-service flow with a valid offer / token.
Drive it into the broken state: room times out / fails to deliver tasks (original report: no tasks after ~5 retries). Do NOT close the app.
Tap the in-app Restart button.
Evaluate:
PASS — a fresh flow loads, tasks appear, the first photo works, and the flow continues.
FAIL — "Already authorized" / "Already has some kind of room", no tasks, stuck at the first photo.
Scenario B — controllable variant (abort → restart)
A more reliably-triggerable variant when the timeout in Scenario A is hard to force on demand:
Start a flow.
Cancel / abort in-app.
Start again in the same session.
Evaluate:
PASS — starts cleanly, no"Already has some kind of room".
Evidence to capture
Screen recording of: Restart → tasks appear → first-photo OK.
Note the room IDs if visible on screen.
Server-side confirmation (engineer side)
Tail the css server logs on the test environment:
docker logs -f vuer_css | grep -i "self-heal"
Log line
Meaning
self-heal: clearing stale selfServiceRoomData for dead room … (warn)
Positive proof — the self-heal fired: room confirmed expired (remainingSeconds < 1), stale state cleared, fresh room will spawn.
getRemainingSeconds failed … preserving state (error)
Fail-closed branch — the lookup threw, so the room is not confirmed-expired and state is deliberately preserved (see the “fail CLOSED on ambiguity” callout above). Recovery from here is abort, then start (Scenario B), not Restart.
Caveat — why this is handed off, not mocked locally
The "Already authorized" string originates in the Raiffeisen mobile SDK, so a faithful reproduction needs their real mobile client — hence handing this off to someone with live flow access rather than running a local mock. The server self-heal that PR #3064 changes is fully exercised by Scenarios A/B (and by the 76 unit tests); this hand-off script is specifically about confirming the SDK-side end of the flow recovers in production.
Deploy to dev box (2026-06-18)
Fix branch deployed to lederera-447-fk-hardver — server-side verification PASS
Deployed the 8787 fix to the dev box and confirmed clean server-side boot. Functional verification (Scenarios A/B) still pending — needs the real Raiffeisen Myra mobile client pointed at https://css-lederera.facekomdev.net. Satisfies the “must be on the build with the fix” precondition of the hand-off script above: the box now runs the PR #3064 tip.
What was deployed
Box:lederera-447-fk-hardver, reached via command ssh Facekom (ProxyJump FKJumpBox). The box had been offline ~4h on Tailscale and had to be brought back online before the deploy. ⚠️ That box is now DECOMMISSIONED (offline since ~2026-06-27). Any re-deploy / the still-pending functional verification must be redone on the fk-dev Tailscale VM (command ssh ops@fk-dev.taild4189d.ts.net) — see dev-build-host; the Myra client would point at the fk-dev MagicDNS css URL, not css-lederera.facekomdev.net.
vuer_css: switched from customization/instacash @ e3f7a1d6e→fix/FKITDEV-8787-selfservice-v2-abort-clear-state-95 @ d69f7272e (PR #3064 tip). Left on the fix branch.
vuer_oss: left untouched — customization/raiffeisen, up. Provides the getRemainingSeconds RPC the self-heal calls.
Stash: a whitespace-only config/dev.json reformat was stashed as stash@{0} ("fk-8787-deploy: instacash dev.json whitespace reformat") before the branch switch.
New process booted clean: RabbitMQ connection established · Web server listening on 10083 · Socket server listening on 10082.
Zero error lines since boot.
Self-heal code present at selfservice-v2.js:313.
Operational corrections / observations
vuer_css has NO docker healthcheck — verify via supervisorctl, not docker health
docker inspect reports health = nohealthcheck, and docker ps shows “Up 5 weeks” because only the supervisord-managed inner processes restart, not the container itself. So the “~60s unhealthy anti-flap” note that applies to esign containers does NOT apply to vuer_css. Verify a restart took via docker exec vuer_css supervisorctl status (expect RUNNING + climbing uptime), not via docker health.
Pre-existing error on customization/instacash (not 8787-related) — flag for branch owner
The OLDcustomization/instacash process logged a TypeError: Cannot read properties of undefined (reading 'stack') at server/web/WebServer.js:481 (findRoute) just before the restart. It is unrelated to the 8787 fix and does not recur on the fix branch, but it is a real error-handling fault on customization/instacash — flag for whoever owns that branch.
Restore command — return the box to customization/instacash later
cd /workspace/vuer_css && git checkout customization/instacash && git stash pop && docker exec vuer_css sh -c 'cd /workspace/vuer_css && yarn && yarn build && supervisorctl restart all'
Still pending
Functional verification (Scenarios A/B) against the real Raiffeisen Myra mobile client pointed at https://css-lederera.facekomdev.net — not yet run. See Manual verification (partner hand-off script).