# Plan: fix interactive `cloud login` retry killing the shell ## Context Interactive `cloud login` now verifies a candidate password by asking the cloud daemon to reconnect with that candidate, then waiting for either `trapos_cloud_connected` or `trapos_cloud_unauthorized`. Observed bug: - Run `cloud login` with no password argument. - Enter a wrong password. - Instead of printing `invalid password, try again` and prompting again, the command exits back to the CraftOS shell. - The shell's in-memory history is gone, so up-arrow no longer recalls the previous `cloud login` command. The interactive branch in `packages/trapos-cloud/programs/cloud.lua` already looks logically correct: on an `unauthorized` verdict it calls `restore()`, prints the retry message, and continues the loop. Existing unit coverage also models wrong-then-right retry successfully. That points to the live boot/runtime topology rather than the branch itself. ## Root Cause Hypothesis The short-lived verifier event loop inside `cloud login` can accidentally stop the boot event loop. Relevant flow: - `packages/trapos-cloud/programs/cloud.lua` creates a local event loop inside `verifyPassword()`. - When the daemon emits `trapos_cloud_unauthorized`, the verifier calls `el.stopLoop()`. - `packages/trapos-core/apis/eventloop.lua` implements `stopLoop()` by globally queueing a synthetic event named `@libeventloop/END_OF_LOOP/`. - Event loop IDs currently come from a module-local counter. - If `/apis/eventloop.lua` is loaded as independent module instances, both the boot event loop and the local verifier loop can use ID `1`. - The verifier's synthetic stop event can therefore match the boot loop's stop event name. - `packages/trapos-boot/startup/boot.lua` runs the shell and boot event loop with `parallel.waitForAny(shellFn, eventLoopFn)`. - If the boot event loop exits, `parallel.waitForAny` returns and the shell coroutine is killed. - Killing/restarting the shell explains losing shell history. The stale hello `messageId` issue found during exploration is real hardening work, but it does not directly explain shell history loss. It can cause wrong verdict attribution, not shell teardown. ## Goals - Wrong passwords in interactive `cloud login` re-prompt without leaving the command. - Stopping a short-lived local event loop must not stop `_G.bootEventLoop`. - Preserve CraftOS shell history after wrong-password retry. - Keep the fix minimal and local to the event loop semantics where possible. - Add regression tests that reproduce the event-loop collision class. ## Non-Goals - Do not replace `cloud login` verification with a program-owned websocket. - Do not add a standalone Lua test harness. - Do not redesign `startup/boot.lua` or shell lifecycle unless the event-loop fix proves insufficient. - Do not add persisted shell history as a workaround for shell teardown. ## Implementation ### 1. Make event loop IDs process-global File: `packages/trapos-core/apis/eventloop.lua` Replace the module-local `next_eventloop_id` allocation with a `_G`-backed counter so independently loaded copies of the module cannot reuse the same synthetic end event name. Sketch: ```lua local COUNTER_KEY = '__trapos_next_eventloop_id'; local function nextEventLoopId() local id = tonumber(_G[COUNTER_KEY]) or 1; _G[COUNTER_KEY] = id + 1; return id; end ``` Then use: ```lua local eventloop_id = nextEventLoopId(); ``` This keeps the existing `@libeventloop/END_OF_LOOP/` design but removes collisions across module instances. ### 2. Avoid queued stop events when stopping from inside the same loop File: `packages/trapos-core/apis/eventloop.lua` Add a local `stopRequested = false` flag. When `stopLoop()` is called while the loop is actively dispatching its own handler or timeout, set `stopRequested = true` instead of queueing a global event. After dispatching the current event, break the loop if `stopRequested` is set. Keep queueing the synthetic end event when `stopLoop()` is called from another coroutine, because the loop may be blocked inside `os.pullEventRaw()` and needs to be woken. This is a second layer of protection: the verifier's `finish()` calls `stopLoop()` from inside its own event handler, so no global end event should be needed for that path at all. ### 3. Add an event-loop collision regression test File: `packages/trapos-core/tests/eventloop.lua` Add a test that simulates independent module loads: - Load `/apis/eventloop.lua` twice via `loadfile()` or an equivalent isolated environment. - Create one boot-like loop from the first module and one verifier-like loop from the second module. - Run both under `parallel`. - Stop the verifier loop from one of its handlers. - Assert the boot-like loop still handles a later probe event. - Stop the boot-like loop explicitly at the end so the test cannot hang. The regression should fail against the current module-local ID allocation and pass after the fix. ### 4. Add or extend cloud program coverage File: `packages/trapos-cloud/tests/cloud-program.lua` Keep the existing wrong-then-right interactive test, because it verifies the program branch behavior. Add a small test for prompt-time terminate handling if practical: - Fake `read('*')` raises `Terminated`. - Assert `cloud login` queues `login-restore` when needed and re-raises `Terminated`. This is not the primary wrong-password bug, but it closes an adjacent interactive cancellation gap. ### 5. Optional cloud hardening: match hello responses by `messageId` File: `packages/trapos-cloud/apis/libcloud.lua` Store the active hello `messageId`, pass `frame.messageId` through `api.onMessage()` to `onHello`, and ignore hello responses that do not match the currently active hello attempt. This prevents stale hello responses from earlier reconnects being attributed to the current `cloud login` token. It should be treated as a separate hardening change if the event-loop fix is intended to stay small. ## Package Versions If only `eventloop.lua` changes: - Bump `trapos-core` in `packages/trapos-core/ccpm.json`. - Mirror the same version in `packages/index.json`. If cloud program or cloud daemon behavior also changes: - Bump `trapos-cloud` in `packages/trapos-cloud/ccpm.json`. - Mirror the same version in `packages/index.json`. Only bump the full `trapos` meta-package if the repository's release convention for this change requires it. ## Verification Targeted iteration: ```sh just trapos-exec 'shell.run("/programs/runtest.lua", "--pretty", "/tests/eventloop.lua", "/tests/cloud-program.lua", "/tests/cloud.lua")' ``` Required after Lua/package edits: ```sh just check just test --pretty ``` Manual validation on a running TrapOS computer: - Run `cloud login --force`. - Enter a known wrong password. - Confirm it prints `invalid password, try again (ctrl+t to cancel)` and prompts again. - Press up-arrow after eventually cancelling or completing, and confirm shell history still contains `cloud login --force`. ## Risks - Event loop stop semantics are shared by servers and programs, so regression tests should cover both same-loop stop and cross-coroutine stop. - If CraftOS-PC loads `require('/apis/eventloop')` as a singleton in more cases than expected, the ID collision may be intermittent; the `_G` counter still makes the behavior deterministic and safer. - The optional hello `messageId` hardening will require updating existing cloud tests that currently send hello responses without message IDs.