Conversation
tiny.place is being removed from the app after 31 August 2026. Show its entry points only to users who already set up a tiny.place identity and give them a persistent notice pointing to tiny.place. - Add useTinyPlaceIdentity (cached self-identity check) and useNavTabs, which hides the agent-world tab for users without an identity in both nav renderers. - Guard the agent-world route (redirect confirmed non-holders to chat, direct links preserved for holders) and hide the Brain orchestration sub-tab. - Add a non-dismissable TinyPlaceSunsetNotice with a link out to tiny.place on the tiny.place surfaces. - Add tinyplaceSunset.* copy to every supported locale. Closes tinyhumansai#5424
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughThe PR adds shared TinyPlace identity detection, hides TinyPlace navigation without an identity, redirects unauthorized direct access, and adds a translated sunset notice with a link to ChangesTinyPlace access lifecycle
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant Navigation
participant useTinyPlaceIdentity
participant TinyPlaceSurface
participant TinyPlace
User->>Navigation: open application navigation
Navigation->>useTinyPlaceIdentity: read identity state
useTinyPlaceIdentity-->>Navigation: return identity status
Navigation-->>User: show or hide agent-world tab
User->>TinyPlaceSurface: open direct TinyPlace route
TinyPlaceSurface->>useTinyPlaceIdentity: read identity state
useTinyPlaceIdentity-->>TinyPlaceSurface: return identity status
TinyPlaceSurface-->>User: render surface or redirect to /chat
User->>TinyPlace: activate sunset notice CTA
TinyPlaceSurface->>TinyPlace: open external TinyPlace URL
Suggested labels: Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
The agent-world tab is now gated on a tiny.place identity (tinyhumansai#5424), so the existing SidebarNav / CollapsedNavRail tests — which assert the full nav and Tiny.Place active state — must mock useTinyPlaceIdentity as present. The gate itself is covered by useNavTabs.test.ts.
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 21017f7a80
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
… caching it A rejected self_identity call (wallet/keyring locked at startup, or a transient relay error) previously published ready/false and latched, so a real identity holder stayed hidden from tiny.place — nav tab gone, direct links redirected — until an app restart. Distinguish a resolved answer (terminal) from a transient rejection: on rejection stay fail-closed but retry with a bounded backoff, and re-check immediately on window focus (e.g. after the user unlocks their wallet), so a one-time startup failure never locks a holder out for the session.
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/src/lib/i18n/bn.ts`:
- Around line 7193-7196: Update only the tinyplaceSunset.title translation to
use the product name “Tiny Place” instead of the domain spelling, while
preserving the existing tiny.place references in tinyplaceSunset.message and
tinyplaceSunset.cta.
In `@app/src/lib/i18n/es.ts`:
- Line 7344: Update the tinyplaceSunset.title translation to use the product
name “Tiny Place” instead of the domain-style “tiny.place”; leave the related
message and CTA translations unchanged so domain references remain “tiny.place”.
In `@app/src/lib/i18n/id.ts`:
- Around line 7232-7235: Update the `tinyplaceSunset.title` UI label to use the
product name “Tiny Place” with the existing Indonesian wording, while preserving
`tiny.place` unchanged in `tinyplaceSunset.message` and `tinyplaceSunset.cta`
where it denotes the external domain.
In `@app/src/pages/Brain.tsx`:
- Around line 115-120: Remove the redirect useEffect in Brain and derive the
ready, non-identity orchestration condition from activeTab, tinyplaceStatus, and
hasTinyplaceIdentity. After all hooks and before the main JSX return, return
<Navigate to="/brain" replace /> for that condition so OrchestrationView never
mounts; add a Brain test covering a confirmed non-holder at ?tab=orchestration.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: a05ec0a0-ace4-43a0-9d77-fbd3e33d1bd7
📒 Files selected for processing (28)
app/src/agentworld/AgentWorldShell.test.tsxapp/src/agentworld/AgentWorldShell.tsxapp/src/agentworld/TinyPlaceSunsetNotice.test.tsxapp/src/agentworld/TinyPlaceSunsetNotice.tsxapp/src/components/layout/shell/CollapsedNavRail.test.tsxapp/src/components/layout/shell/CollapsedNavRail.tsxapp/src/components/layout/shell/SidebarNav.test.tsxapp/src/components/layout/shell/SidebarNav.tsxapp/src/hooks/useNavTabs.test.tsapp/src/hooks/useNavTabs.tsapp/src/hooks/useTinyPlaceIdentity.test.tsapp/src/hooks/useTinyPlaceIdentity.tsapp/src/lib/i18n/ar.tsapp/src/lib/i18n/bn.tsapp/src/lib/i18n/de.tsapp/src/lib/i18n/en.tsapp/src/lib/i18n/es.tsapp/src/lib/i18n/fr.tsapp/src/lib/i18n/hi.tsapp/src/lib/i18n/id.tsapp/src/lib/i18n/it.tsapp/src/lib/i18n/ko.tsapp/src/lib/i18n/pl.tsapp/src/lib/i18n/pt.tsapp/src/lib/i18n/ru.tsapp/src/lib/i18n/zh-CN.tsapp/src/pages/Brain.tsxapp/src/utils/links.ts
…ct-name notice title Address CodeRabbit review on tinyhumansai#5439. - Brain: redirect a confirmed non-holder away from ?tab=orchestration in the render phase (return <Navigate to="/brain" replace />) instead of a post-commit effect, so OrchestrationView — and its tiny.place RPCs — never mount even once for a gated user. The in-flight 'loading' window and holders are untouched. Adds Brain tests for the confirmed-non-holder redirect and the loading window. - i18n: the sunset notice title is a UI product label, so use the product name 'Tiny Place' (matching nav.agentWorld / agentWorld.world.title) across all 14 locales; the message and CTA keep the 'tiny.place' domain spelling. CodeRabbit flagged only bn/es/id — applied consistently to every locale, English source included.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/src/pages/__tests__/Brain.test.tsx`:
- Around line 224-236: Add a hoisted render spy for OrchestrationView and a
useLocation-based pathname probe to the test setup. Update the confirmed
non-holder test to assert OrchestrationView was never rendered and that the
final pathname is /brain, while retaining the existing welcome-tab assertion.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 78ca8195-7750-48d6-99b5-48b27950aaea
📒 Files selected for processing (16)
app/src/lib/i18n/ar.tsapp/src/lib/i18n/bn.tsapp/src/lib/i18n/de.tsapp/src/lib/i18n/en.tsapp/src/lib/i18n/es.tsapp/src/lib/i18n/fr.tsapp/src/lib/i18n/hi.tsapp/src/lib/i18n/id.tsapp/src/lib/i18n/it.tsapp/src/lib/i18n/ko.tsapp/src/lib/i18n/pl.tsapp/src/lib/i18n/pt.tsapp/src/lib/i18n/ru.tsapp/src/lib/i18n/zh-CN.tsapp/src/pages/Brain.tsxapp/src/pages/__tests__/Brain.test.tsx
🚧 Files skipped from review as they are similar to previous changes (15)
- app/src/lib/i18n/pt.ts
- app/src/lib/i18n/en.ts
- app/src/lib/i18n/ko.ts
- app/src/lib/i18n/de.ts
- app/src/lib/i18n/fr.ts
- app/src/lib/i18n/es.ts
- app/src/lib/i18n/bn.ts
- app/src/lib/i18n/id.ts
- app/src/lib/i18n/zh-CN.ts
- app/src/lib/i18n/pl.ts
- app/src/lib/i18n/ru.ts
- app/src/lib/i18n/hi.ts
- app/src/lib/i18n/it.ts
- app/src/lib/i18n/ar.ts
- app/src/pages/Brain.tsx
…brain Address CodeRabbit follow-up on tinyhumansai#5439: queryByTestId only checks final DOM, so a regression that mounts OrchestrationView and redirects from an effect would still pass. Add a hoisted render spy on OrchestrationView (assert not called) and a useLocation pathname probe (assert final route is /brain) to the confirmed non-holder test, alongside the existing welcome-tab assertion.
There was a problem hiding this comment.
Requesting changes: 1 lane(s) blocking, worst finding is high.
Fix or reply to the findings below and push. The next review clears this automatically once they are gone — you should not need to dismiss anything by hand.
$0.0148 · 167,502 in / 46,707 out · 136,638 cached (82%) · z-ai/glm-5.2, deepseek/deepseek-v4-pro
critique: $0.0093 · 70,322 in / 31,790 out · 57,141 cached (81%) · z-ai/glm-5.2, deepseek/deepseek-v4-pro
security: $0.0029 · 64,174 in / 7,035 out · 54,972 cached (86%) · z-ai/glm-5.2
tests: $0.0014 · 16,161 in / 4,457 out · 12,629 cached (78%) · z-ai/glm-5.2
description: $0.0013 · 16,845 in / 3,425 out · 11,896 cached (71%) · z-ai/glm-5.2
| import { useNavTabs } from './useNavTabs'; | ||
| import type { TinyPlaceIdentityState } from './useTinyPlaceIdentity'; | ||
|
|
||
| let identity: TinyPlaceIdentityState = { status: 'ready', hasIdentity: false }; |
There was a problem hiding this comment.
vi.mock factory references a non-mock-prefixed variable
The vi.mock factory references identity, a module-level variable that is not prefixed with mock. Vitest hoists vi.mock calls above all imports and initialization, and its factory is only permitted to close over out-of-scope variables whose names start with mock (e.g. mockIdentity); any other reference triggers a hoisting/reference error at setup time. Because identity is declared after the (hoisted) vi.mock and lacks the required prefix, every test in this file will fail before running, e.g. with “Cannot access ‘identity’ before initialization” or the equivalent Vitest message. Rename the variable to mockIdentity (and update the assignments in beforeEach/each test) so the factory can legally reference it.
[RULE] vi.mock factories may only reference out-of-scope variables prefixed with mock ·
There was a problem hiding this comment.
The "every test will fail before running" claim is not accurate — the file passed in CI (Frontend Checks → Vitest) and locally (vitest run src/hooks/useNavTabs.test.ts → 3/3). Vitest v4 evaluates the factory's () => identity lazily at call time (inside a test), not at hoist/factory-eval time, so the out-of-scope reference never triggers the error. That said, the repo convention for controllable mock state is vi.hoisted (see Brain.test.tsx), so in bc55c6d I switched to a hoisted identityRef to match it and remove the ambiguity.
| // tiny.place surface must not fire even once. Holders and the in-flight | ||
| // `loading` window are left untouched. | ||
| const { status: tinyplaceStatus, hasIdentity: hasTinyplaceIdentity } = useTinyPlaceIdentity(); | ||
| const shouldRedirectFromOrchestration = |
There was a problem hiding this comment.
Loading window lets OrchestrationView mount for a confirmed non-holder
The comment states the goal is that "OrchestrationView never mounts for a confirmed non-holder — its RPCs and tiny.place surface must not fire even once." But the guard only fires once tinyplaceStatus === 'ready'. During the in-flight loading window the redirect is skipped, so a stale ?tab=orchestration deep link causes <OrchestrationView /> to mount and run its RPCs for a user who is later confirmed to have no identity. The redirect then unmounts it after the fact, so the RPCs have already fired once — exactly what the comment says must not happen. If the "must not fire even once" invariant is real, the loading case needs handling (e.g. treat loading on a stale orchestration link as "do not mount OrchestrationView yet" rather than "mount it").
[RULE] , ·
There was a problem hiding this comment.
Good catch on the comment — it overstated the invariant, fixed in bc55c6d. The behaviour is intentional and matches the AgentWorldShell route guard: a confirmed non-holder (status === 'ready' && !hasIdentity) never mounts OrchestrationView (render-phase <Navigate>), while the in-flight loading window renders optimistically so a holder — the common case — never sees a flash. Making loading withhold the mount would regress every holder to protect a rare stale non-holder deep link, so I kept the optimistic behaviour and reworded the comment to describe that trade-off accurately rather than claim "must not fire even once".
What this change touches29 files, +613 -18 across 7 components. The code graph knows nothing about these files yet — normal for newly added files, and a cold index otherwise. flowchart LR
n0["app/src/hooks<br/>4 files +277 -0"]:::changed
n1["app/src/agentworld<br/>4 files +147 -1"]:::changed
n2["app/src/pages/__tests__<br/>1 file +65 -1"]:::changed
n3["app/src/lib/i18n<br/>14 files +56 -0"]:::changed
n4["app/src/pages<br/>1 file +38 -12"]:::changed
n5["app/src/components/layout/shell<br/>4 files +27 -4"]:::changed
n6["app/src/utils<br/>1 file +3 -0"]:::changed
classDef changed fill:#0d4429,stroke:#238636,color:#e6edf3
classDef impacted fill:#161b22,stroke:#6e7681,color:#c9d1d9
classDef flagged fill:#5a1e02,stroke:#d93f0b,color:#ffffff
classDef blocking fill:#67060c,stroke:#f85149,color:#ffffff
Green: changed. Grey: untouched, reached through an import or a call. Orange: has findings. Red: has a finding that blocks the merge.
Changed files
|
…ng comment Address tinysweeper review on tinyhumansai#5439. - useNavTabs.test.ts: switch the controllable identity mock to vi.hoisted (the repo convention, as in Brain.test.tsx). The prior module-scoped `let identity` already passed (Vitest v4 evaluates the factory's `() => identity` lazily at call time, not at hoist time), but the hoisted ref matches the established pattern and removes any ambiguity. - Brain.tsx: the orchestration redirect comment overstated the invariant. A *confirmed* non-holder never mounts OrchestrationView, but the in-flight `loading` window renders optimistically — matching the AgentWorldShell route guard so a holder never sees a flash. Reworded to describe that trade-off accurately; behaviour is unchanged and intentional.
|
@tinysweeper — both findings are addressed in 1. 2. Please re-review when you get a chance so the critique lane can clear. |
There was a problem hiding this comment.
The previously-blocking findings are resolved. Clearing the changes request.
$0.0052 · 47,973 in / 18,132 out · 36,981 cached (77%) · z-ai/glm-5.2
critique: $0.0009 · 8,265 in / 3,270 out · 6,468 cached (78%) · z-ai/glm-5.2
security: $0.0008 · 6,024 in / 2,883 out · 4,676 cached (78%) · z-ai/glm-5.2
tests: $0.0023 · 16,494 in / 8,469 out · 12,664 cached (77%) · z-ai/glm-5.2
description: $0.0012 · 17,190 in / 3,510 out · 13,173 cached (77%) · z-ai/glm-5.2
M3gA-Mind
left a comment
There was a problem hiding this comment.
PR #5439 — feat: show tiny.place only to users with an identity
Walkthrough
Independent human-directed review. This PR adds a module-cached useTinyPlaceIdentity hook over orchestration.self_identity and applies it at four gates: the primary nav tab (both renderers via a new useNavTabs), the /agent-world/* route, the Brain ?tab=orchestration sub-tab, plus a non-dismissible sunset notice and copy in all 14 locales. The shape is right — one shared signal, gating at registration/render sites rather than deleting routes, tests that fail if the gate is reverted.
I re-read the current state of the files (post d4eb8ece / a005cd02 / 2e90d8b4 / bc55c6d2) rather than the original diff, and verified the four previously-raised points independently: the backoff + refocus retry, the render-phase Brain guard, the Tiny Place vs tiny.place locale fix, and the Brain render-spy test. All four hold. What follows is only what those 23 bot reviews did not cover — chiefly one finding that changes the premise the hook is built on, and one entry point the gate missed.
I ran the seven affected test files locally: 48/48 pass.
Changes
| File | Summary |
|---|---|
app/src/hooks/useTinyPlaceIdentity.ts |
New module-singleton identity check; resolved answer is terminal, rejection retried on a 2s/5s/10s backoff plus a window focus recheck |
app/src/hooks/useNavTabs.ts |
NAV_TABS filtered to drop agent-world for non-holders |
app/src/components/layout/shell/{SidebarNav,CollapsedNavRail}.tsx |
Both nav renderers switched from NAV_TABS to useNavTabs() |
app/src/agentworld/AgentWorldShell.tsx |
Route guard (<Navigate to="/chat" replace /> for confirmed non-holders) + mounts the sunset notice |
app/src/agentworld/TinyPlaceSunsetNotice.tsx |
Non-dismissible UpsellBanner with an openUrl(TINYPLACE_URL) CTA |
app/src/pages/Brain.tsx |
Orchestration chip hidden for non-holders; render-phase <Navigate to="/brain"> for a confirmed non-holder on ?tab=orchestration |
app/src/utils/links.ts |
TINYPLACE_URL constant |
app/src/lib/i18n/{14 locales}.ts |
tinyplaceSunset.{title,message,cta} |
| 5 test files | Hook (incl. reject + retry + refocus), nav filter, notice, route guard, Brain gate |
Actionable comments (6)
🛑 Blockers
None.
⚠️ Major
1. app/src/hooks/useTinyPlaceIdentity.ts:50-80 — the "no identity" case is a rejection, not a resolved empty agentId, so resolved never latches for non-holders
This is the premise the whole hook rests on, and I think it's inverted. Tracing the RPC core-side:
orchestration_self_identity → handle_tinyplace_signal_key_status (src/openhuman/tinyplace/manifest.rs:3195) → global_state().client().await? → tinyplace_signer_seed() → secret_material(WalletChain::Solana) → Err(WALLET_NOT_CONFIGURED_MESSAGE) when no wallet exists (src/openhuman/web3/wallet/ops.rs:747-754). And require_signer (manifest.rs:2925-2932) returns Err("no signer configured — unlock wallet to manage Signal keys"). When a signer does exist, agent_id = signer.agent_id() is the wallet address — non-empty.
So { agentId: '' } is essentially unreachable in production. The real discriminator is reject vs resolve, and the "never set up tiny.place" majority — the exact population this PR exists to serve — lands in the catch, which the hook classifies as transient. Consequences:
resolvednever becomestruefor a non-holder, soonFocus(line 105) is never short-circuited: every window focus re-arms a fresh 4-call chain (t=0, +2s, +7s, +17s) for the whole session. Alt-tab twenty times, eighty failing RPCs. Each one does a core-side config load + wallet-state file lock.- This is the pattern this repo already got burned by —
coreRpcClient.ts:253-259: "an unbounded retry loop against an absent method produced ~9k Sentry events/day from a single client (#5157)". Not Sentry-visible here (callCoreRpcdoesn't capture), but the churn is the same shape. - The module doc (line 12-13, "A resolved call (identity present or absent) is terminal and cached") and the PR body ("fetched once per session and cached") are both accurate only for holders.
Suggested change — bound the total attempts across the session, so a permanently wallet-less user settles instead of re-arming on focus:
// before
async function attemptLoad(attempt: number) {
if (resolved || inFlight) return;
...
const delay = RETRY_DELAYS_MS[attempt];
if (delay !== undefined && !retryTimer) { ... }
// after
const MAX_ATTEMPTS = RETRY_DELAYS_MS.length + 1; // session-wide, not per-chain
let attempts = 0;
async function attemptLoad(attempt: number) {
if (resolved || inFlight || attempts >= MAX_ATTEMPTS) return;
attempts += 1;
...
// (in catch) schedule only while the session budget allows
const delay = RETRY_DELAYS_MS[attempt];
if (delay !== undefined && !retryTimer && attempts < MAX_ATTEMPTS) { ... }
else console.debug('[tinyplace-gate] identity unresolved after %d attempts — staying hidden', attempts);Longer term the clean fix is core-side: have self_identity (or a small dedicated method) answer "is there an identity?" with Ok(false) for an absent wallet instead of an Err, so the frontend can tell "no" from "couldn't ask". Worth a follow-up issue either way, because as written the frontend genuinely cannot distinguish them.
2. app/src/components/settings/panels/AgentAccessPanel.tsx:394-413 — missed entry point: the tiny.place autopilot toggle is still shown to non-holders
Six doors are gated; this is the seventh. Settings → Agent access renders an "Autonomous tiny.place agent" section whose only condition is autopilotJobId — the presence of the seeded cron job. That job is backfilled at boot for every user (src/openhuman/cron/seed.rs:119-123, "boot seed — backfilling tinyplace_autopilot (disabled, opt-in)"), so autopilotJobId is non-null regardless of identity.
Net effect after this PR: a user with no tiny.place identity has the tab hidden, the route bounced and the Brain chip removed — and can still switch on an autonomous tiny.place agent from Settings for a feature they cannot open and that is being removed. (The panel isn't in this diff, which is presumably how it was missed.)
Suggested change:
// AgentAccessPanel.tsx
+ import { useTinyPlaceIdentity } from '../../../hooks/useTinyPlaceIdentity';
+ const { hasIdentity: hasTinyplaceIdentity } = useTinyPlaceIdentity();
- {autopilotJobId && (
+ {autopilotJobId && hasTinyplaceIdentity && (
<SettingsSection title={t('settings.agentAccess.tinyplaceAutopilot.title')} …>Worth a test in the panel's spec mirroring useNavTabs.test.ts — the two-branch shape is already there to copy.
3. app/src/agentworld/AgentWorldShell.tsx:35-37 and app/src/pages/Brain.tsx:199-201 — a transient failure evicts a holder who is already on the surface, and the successful retry can't bring them back
The retry fixed the nav-tab half of the transient-failure problem, but not the routing half. Sequence for a real identity holder whose wallet is locked at startup (the motivating case in the hook's own doc):
- They open on
/agent-world(bookmark, deep link, or restored window state).status === 'loading'→ renders optimistically. Good. - The RPC rejects →
publish({ status: 'ready', hasIdentity: false }). status === 'ready' && !hasIdentity→<Navigate to="/chat" replace />. They are now on/chat, and because it'sreplacethe/agent-worldhistory entry is gone.- 2s later the retry succeeds →
hasIdentity: true. The tab reappears in the nav; their location does not come back.
So the doc's claim (line 18-19) that "a one-time startup failure never locks a holder out until an app restart" holds for the nav entry point but not for where they actually were. The same applies to Brain.tsx:199 for ?tab=orchestration.
The fix follows from #1: make a transient error its own state, and let the route guards stay permissive on it while the nav stays fail-closed. Redirect only on a definitive answer.
// useTinyPlaceIdentity.ts
- status: 'loading' | 'ready';
+ /** `error` = we could not ask (transient). Gates that hide should hide;
+ * gates that *evict* must not act on it. */
+ status: 'loading' | 'ready' | 'error';
...
- publish({ status: 'ready', hasIdentity: false }); // in catch
+ publish({ status: 'error', hasIdentity: false });AgentWorldShell and Brain then need no change at all — both already test status === 'ready', so 'error' falls through to rendering — and useNavTabs keeps hiding on hasIdentity === false. Add a case to AgentWorldShell.test.tsx for { status: 'error', hasIdentity: false } asserting no redirect.
💡 Refactor / suggestion
4. app/src/components/walkthrough/walkthroughSteps.ts:162-168 — onboarding step 10 points at a nav item that new users no longer have
Step 10 targets [data-walkthrough="tab-agent-world"]. The walkthrough runs immediately after onboarding (setWalkthroughPending() on wizard completion), and a freshly-onboarded user is by definition not an identity holder — so that node is absent for essentially every run of the tour. It doesn't hang: react-joyride logs Target not mounted, records a target_not_found failure, emits EVENTS.TARGET_NOT_FOUND and auto-advances (react-joyride/dist/index.mjs:1249-1259). But the step, and its walkthrough.steps.agentWorldTab.* copy in 14 locales, is now dead weight that also teaches a feature being removed.
Suggested change — make the step list identity-aware the same way the nav is:
// AppWalkthrough.tsx
- const steps = useMemo(() => createWalkthroughSteps(navigate, t), [navigate, t]);
+ const { hasIdentity } = useTinyPlaceIdentity();
+ const steps = useMemo(
+ () => createWalkthroughSteps(navigate, t).filter(
+ s => hasIdentity || s.target !== '[data-walkthrough="tab-agent-world"]'
+ ),
+ [navigate, t, hasIdentity]
+ );5. app/src/agentworld/TinyPlaceSunsetNotice.tsx:23 — UpsellBanner truncates message to one line, and the removal date is at the end of the sentence
UpsellBanner renders the message as <p className="text-xs … truncate"> (components/upsell/UpsellBanner.tsx:68) — white-space: nowrap + ellipsis, inside a min-w-0 flex child. The English copy is ~90 chars and the payload (after 31 August 2026) is the last clause, so it's the first thing clipped; de/ru/pl/bn are longer still, and the Brain orchestration surface shares width with the sidebar. A sunset notice whose date can be ellipsed away isn't doing its job.
Suggested change — give the banner an opt-out and use it here:
// UpsellBanner.tsx
- <p className={`text-xs ${styles.text} truncate`}>{message}</p>
+ <p className={`text-xs ${styles.text} ${wrapMessage ? '' : 'truncate'}`}>{message}</p>
// TinyPlaceSunsetNotice.tsx
<UpsellBanner variant="info" … rounded={false} dismissible={false}
+ wrapMessageAlso worth role="status" on the wrapper (line 19) so a non-dismissible notice is announced, and aria-hidden="true" on the banner's decorative icon.
6. app/src/hooks/useTinyPlaceIdentity.ts — the new gate has no debug logging
AGENTS.md is explicit: "Default to verbose diagnostics on new/changed flows… Log entry/exit, branches, external calls, retries/timeouts, state transitions, errors… Changes lacking logging are incomplete." The neighbours follow it — [brain] in Brain.tsx:105,158,163, [routes] orchestration-redirect: in AppRoutes.tsx:56,77. This hook has zero log lines, so "why is tiny.place hidden for this user?" is not answerable from a support log — and given #1, that's the question you'll actually get.
+ const LOG = '[tinyplace-gate]';
const identity = await orchestrationClient.selfIdentity();
+ console.debug('%s identity resolved hasIdentity=%s', LOG, identity.agentId.trim().length > 0);
- } catch {
+ } catch (err) {
+ console.debug('%s identity check failed attempt=%d — hidden, retrying', LOG, attempt, err);(Privacy-safe: log the boolean and the attempt number, never agentId.)
Nitpicks (6)
app/src/pages/Brain.tsx:256— the orchestration chip gate is the only gate with no test: deleting the...(hasTinyplaceIdentity ? […] : [])spread wouldn't fail anything (Brain.test.tsxcovers the redirect, not the chip). One assertion on the nav items would close it.app/src/agentworld/TinyPlaceSunsetNotice.test.tsx:26-30— the "not dismissible" test is vacuous:UpsellBannerrequires bothdismissibleandonDismissto render the close button, andonDismissis never passed, so the assertion passes even ifdismissible={true}. Assert on the prop, or pass a dummyonDismissin a control case.app/src/hooks/useNavTabs.ts:18— hides the tab whilestatus === 'loading'too, so a holder gets a nav layout shift when the RPC settles, while the route guard is optimistic in the opposite direction. ConsiderhasIdentity || status === 'loading'for consistency (or state in the doc comment that the asymmetry is deliberate).app/src/hooks/useTinyPlaceIdentity.ts:42—retryTimeris module-scoped and never cleared when the last consumer unmounts, so a pending retry fires an RPC with nothing listening. Harmless in the desktop shell (the nav is always mounted) but it's a real leak in any other host.app/src/agentworld/TinyPlaceSunsetNotice.tsx:10— a deprecation notice built oncomponents/upsell/UpsellBannerreads oddly; thevariant: 'info'path also renders a warning-triangle icon (pre-existing in that component, not this PR).app/src/hooks/useTinyPlaceIdentity.ts:60— given #1,identity.agentId.trim().length > 0is defensive-only; a comment saying so would stop the next reader from assuming the empty-string case is a live path.
Questions for the author (2)
app/src/hooks/useTinyPlaceIdentity.ts:60—agentIdis the wallet address (tinyplace/mod.rs:22-26: "the wallet key becomes the tiny.place identity"), sohasIdentityreally means "a wallet exists and its Solana key is derivable", not "this user used tiny.place".self_identityalso returnsdiscoverable/ card-published / key-published. Is a wallet holder who never published a tiny.place card intended to keep the tab — and conversely, is a tiny.place user whose wallet is merely locked meant to be treated as a non-holder until a retry lands?- Is a
restartApp-less identity flip in scope? The hook's cache is process-lifetime and unkeyed by user. Desktop A→B restarts (CoreStateProvider.tsx:326-338), but that file explicitly handles the non-restart flip ("same-process session attach or web whererestartAppis a no-op") by clearing the thread slice, andlib/composio/connectionCache.tsscopes its module cache byOPENHUMAN_ACTIVE_USER_IDfor the same reason. If those paths matter, this cache wants the same treatment.
Verified / looks good
- i18n, all 14 locales:
tinyplaceSunset.{title,message,cta}present in every file (3/3 keys × 14), every value genuinely translated with no English leakage,Tiny Placeused for the product label andtiny.placefor the domain, noU+2014em dashes. Nothing left behind. - Prior review points re-verified against current code, not the bot replies: the bounded backoff + refocus recheck exist and are wired (
useTinyPlaceIdentity.ts:37,65-71,104-111);inFlightis set synchronously before the firstawait, so N hook consumers sharing one focus event cannot double-fire, and StrictMode's double effect is a no-op; the Brain guard is genuinely render-phase (Brain.tsx:199) andBrain.test.tsxproves non-mount with a render spy plus a/brainpathname probe. - Tests are not vacuous: reverting the
useNavTabsfilter, theAgentWorldShellguard, the Brain redirect or the notice each breaks a specific assertion. 48/48 pass locally across the seven affected files. - Fail-closed direction is sound: I could not find a path where an error or in-flight state resolves to visible for a user with no identity —
hasIdentitystartsfalseand only a successful non-emptyagentIdsets ittrue. The gaps above are all on the other side (a holder wrongly hidden or evicted), which is the right side to fail on. - Routes stay registered, so holders' deep links and the
/orchestration→/brain?tab=orchestration→ settingstasksredirect chain all funnel through the one Brain gate rather than needing four.
| // Transient: stay fail-closed (hidden) but schedule a bounded retry so a | ||
| // real holder is not locked out for the whole session by a startup blip. | ||
| publish({ status: 'ready', hasIdentity: false }); | ||
| const delay = RETRY_DELAYS_MS[attempt]; |
There was a problem hiding this comment.
agentId, so resolved never latches for the majority of users.
Tracing the RPC core-side: orchestration_self_identity → handle_tinyplace_signal_key_status (src/openhuman/tinyplace/manifest.rs:3195) → global_state().client().await? → tinyplace_signer_seed() → secret_material(WalletChain::Solana) → Err(WALLET_NOT_CONFIGURED_MESSAGE) when no wallet exists (src/openhuman/web3/wallet/ops.rs:747-754). require_signer (manifest.rs:2925-2932) likewise returns Err("no signer configured — unlock wallet…"). When a signer does exist, agent_id = signer.agent_id() is the wallet address — never empty.
So { agentId: '' } is effectively unreachable, and the "never set up tiny.place" population lands in this catch, which the hook classifies as transient. Because resolved stays false, the onFocus guard on line 105 never short-circuits: every window focus re-arms a fresh 4-call chain (t=0, +2s, +7s, +17s) for the whole session, each attempt costing a core-side config load + wallet-state file lock. That is the shape coreRpcClient.ts:253-259 already warns about — "an unbounded retry loop against an absent method produced ~9k Sentry events/day from a single client (#5157)" (not Sentry-visible here, but the same churn).
The module doc on line 12-13 and the PR body's "fetched once per session and cached" are therefore true only for holders.
Suggested change — bound total attempts session-wide, not per chain:
// before
async function attemptLoad(attempt: number) {
if (resolved || inFlight) return;
...
const delay = RETRY_DELAYS_MS[attempt];
if (delay !== undefined && !retryTimer) { ... }
// after
const MAX_ATTEMPTS = RETRY_DELAYS_MS.length + 1; // session-wide
let attempts = 0;
async function attemptLoad(attempt: number) {
if (resolved || inFlight || attempts >= MAX_ATTEMPTS) return;
attempts += 1;
...
const delay = RETRY_DELAYS_MS[attempt];
if (delay !== undefined && !retryTimer && attempts < MAX_ATTEMPTS) { ... }
else console.debug('[tinyplace-gate] unresolved after %d attempts — staying hidden', attempts);The clean long-term fix is core-side: answer "is there an identity?" with Ok(false) for an absent wallet instead of Err, so the frontend can tell "no" from "couldn't ask". As written it genuinely cannot.
| // in flight the surface renders optimistically so a holder never sees a flash. | ||
| const { status, hasIdentity } = useTinyPlaceIdentity(); | ||
| if (status === 'ready' && !hasIdentity) { | ||
| return <Navigate to="/chat" replace />; |
There was a problem hiding this comment.
The backoff added in d4eb8ece fixed the nav-tab half of the transient-failure problem, not the routing half. For a real holder whose wallet is locked at startup (the motivating case in the hook's own doc comment):
- They open on
/agent-world(bookmark, deep link, restored window).status === 'loading'→ renders optimistically. ✅ - The RPC rejects →
publish({ status: 'ready', hasIdentity: false }). - This line fires
<Navigate to="/chat" replace />— and because it isreplace, the/agent-worldhistory entry is gone. - ~2s later the retry succeeds →
hasIdentity: true. The nav tab reappears; their location does not.
So useTinyPlaceIdentity.ts:18-19 ("a one-time startup failure never locks a holder out until an app restart") holds for the entry point but not for where the user actually was. Brain.tsx:199 has the same behaviour for ?tab=orchestration.
Suggested change — give a transient error its own state, so gates that hide still hide but gates that evict stay permissive:
// useTinyPlaceIdentity.ts
- status: 'loading' | 'ready';
+ /** `error` = we could not ask (transient). Hide on it; never evict on it. */
+ status: 'loading' | 'ready' | 'error';
...
- publish({ status: 'ready', hasIdentity: false }); // in catch
+ publish({ status: 'error', hasIdentity: false });Neither this file nor Brain.tsx then needs a change — both already test status === 'ready', so 'error' falls through to rendering — and useNavTabs keeps hiding on hasIdentity === false. Worth adding a { status: 'error', hasIdentity: false } case to AgentWorldShell.test.tsx asserting no redirect.
| export function useNavTabs(): NavTab[] { | ||
| const { hasIdentity } = useTinyPlaceIdentity(); | ||
| return useMemo( | ||
| () => NAV_TABS.filter(tab => tab.id !== 'agent-world' || hasIdentity), |
There was a problem hiding this comment.
Six doors are gated by this PR (nav ×2, /agent-world/*, the Brain chip, ?tab=orchestration, and the legacy /orchestration + settings-tasks redirects that funnel into it). This is the seventh:
app/src/components/settings/panels/AgentAccessPanel.tsx:394-413 renders an "Autonomous tiny.place agent" section whose only condition is autopilotJobId — i.e. the seeded cron job exists. That job is backfilled at boot for every user (src/openhuman/cron/seed.rs:119-123 — "boot seed — backfilling tinyplace_autopilot (disabled, opt-in)"), so the condition is identity-independent.
Net effect after this PR: a user with no tiny.place identity has the tab hidden, the route bounced and the Brain chip removed — and can still switch on an autonomous tiny.place agent from Settings, for a surface they cannot open and that is being removed. (The panel isn't in this diff, which is presumably how it slipped.)
Suggested change:
// AgentAccessPanel.tsx
+ import { useTinyPlaceIdentity } from '../../../hooks/useTinyPlaceIdentity';
+ const { hasIdentity: hasTinyplaceIdentity } = useTinyPlaceIdentity();
- {autopilotJobId && (
+ {autopilotJobId && hasTinyplaceIdentity && (
<SettingsSection title={t('settings.agentAccess.tinyplaceAutopilot.title')} …>Plus a two-branch test in the panel spec, mirroring useNavTabs.test.ts.
| const companionActive = useAppSelector(selectCompanionSessionActive); | ||
|
|
||
| const tabs = useMemo(() => NAV_TABS.map(tab => ({ ...tab, label: t(tab.labelKey) })), [t]); | ||
| const navTabs = useNavTabs(); |
There was a problem hiding this comment.
💡 suggestion — the onboarding walkthrough still points at this now-conditional tab.
components/walkthrough/walkthroughSteps.ts:162-168 (step 10) targets [data-walkthrough="tab-agent-world"]. The tour runs immediately after onboarding (setWalkthroughPending() on wizard completion), and a freshly-onboarded user is by definition not an identity holder — so that node is absent for essentially every run of the tour.
It doesn't hang: react-joyride logs Target not mounted, records a target_not_found failure, emits EVENTS.TARGET_NOT_FOUND and auto-advances (react-joyride/dist/index.mjs:1249-1259), and ACTIONS.PREV decrements correctly too. But the step — plus its walkthrough.steps.agentWorldTab.* copy in 14 locales — is now dead for most users, and it teaches a feature that is being removed.
Suggested change — make the step list identity-aware the same way the nav now is:
// AppWalkthrough.tsx
- const steps = useMemo(() => createWalkthroughSteps(navigate, t), [navigate, t]);
+ const { hasIdentity } = useTinyPlaceIdentity();
+ const steps = useMemo(
+ () => createWalkthroughSteps(navigate, t).filter(
+ s => hasIdentity || s.target !== '[data-walkthrough="tab-agent-world"]'
+ ),
+ [navigate, t, hasIdentity]
+ );| <UpsellBanner | ||
| variant="info" | ||
| title={t('tinyplaceSunset.title')} | ||
| message={t('tinyplaceSunset.message')} |
There was a problem hiding this comment.
💡 suggestion — UpsellBanner truncates message to a single line, and the removal date is the last clause.
UpsellBanner renders the message as <p className="text-xs … truncate"> (components/upsell/UpsellBanner.tsx:68) — white-space: nowrap + ellipsis, inside a min-w-0 flex child. The English copy is ~90 chars and ends with after 31 August 2026, so the payload is the first thing clipped; de/ru/pl/bn are longer, and the Brain orchestration surface shares width with the sidebar. A sunset notice whose date can be ellipsed away isn't doing its job.
Suggested change — add an opt-out and use it here:
// UpsellBanner.tsx
- <p className={`text-xs ${styles.text} truncate`}>{message}</p>
+ <p className={`text-xs ${styles.text} ${wrapMessage ? '' : 'truncate'}`}>{message}</p>
// TinyPlaceSunsetNotice.tsx
<UpsellBanner variant="info" … rounded={false} dismissible={false}
+ wrapMessageAlso worth role="status" on the wrapper div (line 19) so a non-dismissible notice is announced, and aria-hidden="true" on the banner's decorative icon.
| } | ||
|
|
||
| /** Kick a load if one isn't already settled, in flight, or scheduled. */ | ||
| function ensureLoad() { |
There was a problem hiding this comment.
💡 suggestion — the new gate ships with no debug logging.
AGENTS.md is explicit: "Default to verbose diagnostics on new/changed flows… Log entry/exit, branches, external calls, retries/timeouts, state transitions, errors… Changes lacking logging are incomplete." The neighbouring code follows it — [brain] in Brain.tsx:105,158,163, [routes] orchestration-redirect: in AppRoutes.tsx:56,77. This hook has zero log lines, so "why is tiny.place hidden for this user?" isn't answerable from a support log — and given the reject-vs-resolve issue flagged above, that is the question you will actually get.
+ const LOG = '[tinyplace-gate]';
const identity = await orchestrationClient.selfIdentity();
+ console.debug('%s identity resolved hasIdentity=%s', LOG, identity.agentId.trim().length > 0);
- } catch {
+ } catch (err) {
+ console.debug('%s identity check failed attempt=%d — hidden, retrying', LOG, attempt, err);Privacy-safe: the boolean and the attempt number only, never agentId.
Two smaller things on this same function while you're here:
retryTimer(line 42) is module-scoped and never cleared when the last consumer unmounts, so a pending retry fires an RPC with nothing listening. Harmless in the desktop shell (the nav is always mounted), but it is a real leak in any other host.identity.agentId.trim().length > 0(line 60) is defensive-only in practice —agent_idcomes fromsigner.agent_id(), so an empty string is unreachable once a signer exists. A one-line comment saying so would stop the next reader assuming it's a live branch.
| // TinyPlace multi-agent orchestration, folded back under Brain | ||
| // from the former top-level `/orchestration` tab. Hidden from | ||
| // users without a tiny.place identity (#5424). | ||
| ...(hasTinyplaceIdentity |
There was a problem hiding this comment.
nitpick (test coverage) — this is the only gate in the PR with no test behind it.
Brain.test.tsx covers the ?tab=orchestration redirect and the loading window, but nothing asserts the chip itself is absent for a non-holder: delete this ...(hasTinyplaceIdentity ? […] : []) spread and the suite still passes. Since TwoPaneNav is already mocked in that spec, one assertion closes it:
it('hides the orchestration chip for a non-holder', async () => {
tinyplaceIdentityRef.current = { status: 'ready', hasIdentity: false };
graphExportMock.mockResolvedValue(makeGraph(0));
await act(async () => { renderWithProviders(<Brain />, { initialEntries: ['/'] }); });
expect(screen.queryByText('brain.tabs.orchestration')).not.toBeInTheDocument();
});Related, and worth a decision rather than a fix: the chip (and useNavTabs) hide while status === 'loading', whereas the route guards render optimistically in that same window. A holder therefore gets a nav layout shift when the RPC settles. Either hasIdentity || status === 'loading' for symmetry, or say in the doc comment that the asymmetry is deliberate (hide-by-default for advertising, permissive for navigation) — right now the two read as an oversight.
| it('is not dismissible — no dismiss control is rendered', () => { | ||
| render(<TinyPlaceSunsetNotice />); | ||
|
|
||
| expect(screen.queryByRole('button', { name: 'common.dismiss' })).toBeNull(); |
There was a problem hiding this comment.
nitpick — this assertion is vacuous.
UpsellBanner renders the close button only when both dismissible and onDismiss are truthy (components/upsell/UpsellBanner.tsx:80). TinyPlaceSunsetNotice never passes onDismiss, so this query returns null even if dismissible were flipped to true — the test cannot fail for the reason it claims to check.
Either assert the prop directly, or make the control case real:
// prove the query would find a dismiss button when one exists,
// so its absence in TinyPlaceSunsetNotice means something
it('is not dismissible — no dismiss control is rendered', () => {
render(<TinyPlaceSunsetNotice />);
expect(screen.queryByRole('button', { name: 'common.dismiss' })).toBeNull();
// control: the same query does find one when the banner is dismissible
render(<UpsellBanner variant="info" title="t" message="m" dismissible onDismiss={() => {}} />);
expect(screen.getByRole('button', { name: 'common.dismiss' })).toBeInTheDocument();
});…ct-name notice title\n\nAddress CodeRabbit review on tinyhumansai#5439.\n\n- Brain: redirect a confirmed non-holder away from ?tab=orchestration in the\n render phase (return <Navigate to="/brain" replace />) instead of a\n post-commit effect, so OrchestrationView — and its tiny.place RPCs — never\n mount even once for a gated user. The in-flight 'loading' window and holders\n are untouched. Adds Brain tests for the confirmed-non-holder redirect and the\n loading window.\n- i18n: the sunset notice title is a UI product label, so use the product name\n 'Tiny Place' (matching nav.agentWorld / agentWorld.world.title) across all 14\n locales; the message and CTA keep the 'tiny.place' domain spelling. CodeRabbit\n flagged only bn/es/id — applied consistently to every locale, English source\n included.\n
…brain\n\nAddress CodeRabbit follow-up on tinyhumansai#5439: queryByTestId only checks final DOM, so a\nregression that mounts OrchestrationView and redirects from an effect would still\npass. Add a hoisted render spy on OrchestrationView (assert not called) and a\nuseLocation pathname probe (assert final route is /brain) to the confirmed\nnon-holder test, alongside the existing welcome-tab assertion.\n
…ng comment\n\nAddress tinysweeper review on tinyhumansai#5439.\n\n- useNavTabs.test.ts: switch the controllable identity mock to vi.hoisted (the\n repo convention, as in Brain.test.tsx). The prior module-scoped `let identity`\n already passed (Vitest v4 evaluates the factory's `() => identity` lazily at\n call time, not at hoist time), but the hoisted ref matches the established\n pattern and removes any ambiguity.\n- Brain.tsx: the orchestration redirect comment overstated the invariant. A\n *confirmed* non-holder never mounts OrchestrationView, but the in-flight\n `loading` window renders optimistically — matching the AgentWorldShell route\n guard so a holder never sees a flash. Reworded to describe that trade-off\n accurately; behaviour is unchanged and intentional.\n
…inyplace-visibility-5424\n\nfeat: show tiny.place only to users with an identity\n
Summary
useTinyPlaceIdentity(cached self-identity check) +useNavTabs, which hides the agent-world tab for users without an identity in both nav renderers.Orchestrationsub-tab chip is hidden and?tab=orchestrationredirects to welcome for non-holders.TinyPlaceSunsetNoticewith a link out to tiny.place, shown on the tiny.place surfaces.tinyplaceSunset.*copy added to every supported locale.Problem
Someone without a tiny.place identity never really started, so they shouldn't be shown an entry point that's about to disappear; identity-holders need a clear path to keep using tiny.place. The app had no way to answer "has this user set up tiny.place?" at the UI layer.
Solution
The authoritative signal is the
orchestration.self_identityRPC: a non-emptyagentIdmeans the wallet-backed identity exists (a locked/unconfigured wallet or a rejection is the "never set up" case → fail closed to hidden). The result is fetched once per session and cached, then read by the nav filter, the agent-world route guard, the Brain orchestration gate, and the notice. Routes stay registered so identity-holders' bookmarks keep working.Submission Checklist
Closes #NNNImpact
Related
AI Authored PR Metadata (required for Codex/Linear PRs)
N/A — not a Codex/Linear-authored PR.
Validation Run
pnpm --filter openhuman-app format:checkpnpm typecheckSummary by CodeRabbit
New Features
Bug Fixes