Skip to content

fix(sync): restore session permission mode from last message across devices#9

Closed
leeroybrun wants to merge 3 commits intomainfrom
slopus/pr/permission-mode-from-last-message-2026-01-13
Closed

fix(sync): restore session permission mode from last message across devices#9
leeroybrun wants to merge 3 commits intomainfrom
slopus/pr/permission-mode-from-last-message-2026-01-13

Conversation

@leeroybrun
Copy link
Copy Markdown
Collaborator

@leeroybrun leeroybrun commented Jan 13, 2026

Summary

Permission mode (yolo/bypassPermissions/etc) is now preserved when switching between devices. The app infers the permission mode from the most recent session's user message metadata, ensuring consistent behavior across mobile and desktop.

How it works:

  • When a message is sent, the current permission mode is stored in message metadata
  • When loading a session on another device, the mode is inferred from the most recent user message
  • Local UI toggles are protected from older messages but yield to newer ones

Fixes

Related

Test plan

  • Set yolo mode on Device A, send message, verify Device B picks up yolo
  • Toggle mode on Device B without sending message, verify it persists on re-sync
  • Send message from Device A after Device B toggle, verify Device A's mode wins
  • Restart app, verify permission mode is restored from persistence

Summary by CodeRabbit

  • New Features

    • Per-session permission modes now persist with server-aligned timestamps and survive reloads; inferred modes from recent user messages are applied when newer than local state.
    • Message timing uses server-calibrated timestamps for consistent ordering.
    • Message metadata expanded (display text, allowed/disallowed tools, appended prompts) for richer previews.
  • Bug Fixes

    • Older in-message inferences no longer overwrite user-chosen modes.
    • User updates to permission mode now timestamp, persist, and clear on session deletion.
  • Style

    • Permission mode options unified and exposed in the UI selector.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Jan 13, 2026

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

Loads and persists per-session permissionModeUpdatedAts, infers permissionMode from recent message meta using server-aligned timestamps, applies updates when newer than local timestamps, exposes new persistence/time APIs, and centralizes PermissionMode types.

Changes

Cohort / File(s) Summary
Session storage & merge logic
sources/sync/storage.ts
Load sessionPermissionModeUpdatedAts on init; infer permissionMode from latest user-text message meta using server-aligned timestamps; compare inferred permissionModeUpdatedAt vs session local value; apply and persist updates when newer; set permissionModeUpdatedAt on user updates; clear persisted ats on delete; copy latestUsage into session.
Types & schema
sources/sync/storageTypes.ts, sources/sync/typesMessageMeta.ts, sources/sync/typesRaw.ts
Add permissionMode and permissionModeUpdatedAt to Metadata/Session schemas and types; replace hard-coded enums with PERMISSION_MODES; extend MessageMetaSchema with appendSystemPrompt, allowedTools, disallowedTools, displayText.
Persistence APIs
sources/sync/persistence.ts
Add loadSessionPermissionModeUpdatedAts() and saveSessionPermissionModeUpdatedAts(...) to read/write session-permission-mode-updated-ats JSON with validation and parse-error handling.
Permission modes constant & usage
sources/constants/PermissionModes.ts, sources/components/PermissionModeSelector.tsx, sources/-session/SessionView.tsx, sources/sync/settings.ts
Introduce PERMISSION_MODES constant and PermissionMode type; replace local union types with shared type; use PERMISSION_MODES in schemas and component typings; update callback signatures to accept PermissionMode.
Server time sync & HTTP calibration
sources/sync/time.ts, sources/sync/sync.ts, sources/sync/apiSocket.ts
Add observeServerTimestamp() and nowServerMs() for best-effort server-aligned time; use nowServerMs() for message createdAt; read HTTP Date header from responses and feed to observeServerTimestamp() for calibration.
Small interop/type updates
sources/components/PermissionModeSelector.tsx, sources/-session/SessionView.tsx
Replace local PermissionMode type with centralized export and adjust usages/signatures accordingly.

Sequence Diagram(s)

sequenceDiagram
  participant Client as MessageStream
  participant Storage as SessionStorage
  participant Reducer as ReducerState
  participant Persistence as LocalPersistence
  participant Time as ServerTime

  Client->>Storage: applyMessages(messages)
  Storage->>Time: (optionally) observeServerTimestamp(message.createdAt)
  Storage->>Reducer: request inferred permission meta for messages
  Reducer-->>Storage: inferredPermissionMode, inferredAt, latestUsage
  Storage->>Storage: compare inferredAt vs session.permissionModeUpdatedAt
  alt inferredAt newer
    Storage->>Storage: set session.permissionMode & permissionModeUpdatedAt
    Storage->>Persistence: saveSessionPermissionModes(...)
    Storage->>Persistence: saveSessionPermissionModeUpdatedAts(...)
  end
  Storage->>Storage: copy latestUsage into session
  Note over Storage,Persistence: On init Storage loads session-permission-mode-updated-ats and merges into sessions
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I tuck timestamps in a cozy spot,
So "yolo" stays put and won't get forgot.
I listen to the server clock's soft chime,
Keep modes in sync across space and time.
— a hopping rabbit, guarding your mode 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: restoring/persisting session permission mode across devices by inferring from message metadata.
Linked Issues check ✅ Passed The PR implementation directly addresses all linked issue requirements: permission mode persists across devices via message metadata inference (#206, #29, #64, #11), server time calibration prevents stale messages (#64), and the changes establish the foundation for UI permission control (#229).
Out of Scope Changes check ✅ Passed All changes are within scope: permission mode persistence infrastructure, server time calibration, type consolidation to PERMISSION_MODES constant, and persistence APIs directly support the linked issue objectives.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@leeroybrun
Copy link
Copy Markdown
Collaborator Author

@codex please review

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
sources/sync/storage.ts (1)

844-858: Consider updating the in-memory cache for consistency.

Unlike applyMessages (line 578), this function doesn't update the sessionPermissionModes in-memory cache after calling saveSessionPermissionModes. While this has no functional impact (the cache is only used during initial session load), keeping both update paths consistent would improve maintainability.

♻️ Suggested fix for consistency
             // Persist permission modes (only non-default values to save space)
             saveSessionPermissionModes(allModes);
+            sessionPermissionModes = allModes;
 
             // No need to rebuild sessionListViewData since permission mode doesn't affect the list display
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 450016c and 44190a1.

📒 Files selected for processing (2)
  • sources/sync/storage.ts
  • sources/sync/storageTypes.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Run yarn typecheck after all changes to ensure type safety
Ensure TypeScript strict mode is enabled - all code must be properly typed
Never use Alert module from React Native, always use @sources/modal/index.ts instead
Always apply layout width constraints from @/components/layout to full-screen ScrollViews and content containers for responsive design
Use StyleSheet.create from 'react-native-unistyles' for all styling
Always use function mode in StyleSheet.create when you need theme or runtime access
For Expo Image components, size properties (width, height) must be set as inline styles, not in Unistyles stylesheet, and tintColor must be set directly on the component
Always use t(...) function from @/text for ALL user-visible strings for internationalization
Always use expo-router API, not react-navigation API directly
Use ItemList for most containers for UI, only use custom components or alternatives for custom layouts like chat
Always try to use 'useHappyAction' from @sources/hooks/useHappyAction.ts for async operations instead of handling errors manually
For hotkeys, use 'useGlobalKeyboard' from @sources/hooks/useGlobalKeyboard - do not change it, it only works on Web
Use 'AsyncLock' class for exclusive async locks
Always use 'Avatar' component for displaying avatars

Files:

  • sources/sync/storageTypes.ts
  • sources/sync/storage.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Use 4 spaces for indentation
Use path alias @/* which maps to ./sources/*

Files:

  • sources/sync/storageTypes.ts
  • sources/sync/storage.ts
🧬 Code graph analysis (1)
sources/sync/storage.ts (1)
sources/sync/persistence.ts (1)
  • saveSessionPermissionModes (187-189)
🔇 Additional comments (5)
sources/sync/storageTypes.ts (1)

73-73: LGTM!

The new field correctly supports the coordination mechanism with appropriate typing. The optional number | null type allows distinguishing between "never set" (undefined), "explicitly reset" (null), and "set at timestamp" (number), which aligns with the usage in storage.ts.

sources/sync/storage.ts (4)

320-328: LGTM!

The session merge logic correctly preserves the local coordination timestamp. Using ?? null ensures consistent initialization when the field doesn't exist yet.


520-532: LGTM!

The inference logic correctly identifies the permission mode from the most recent user message. Since messagesArray is sorted descending by createdAt (line 518), the first matching message will be the newest, allowing early termination with break.


540-579: LGTM!

The conditional update logic correctly protects local user selections from being overwritten by older message inferences. The timestamp comparison on line 544 ensures that newer message-based modes will still take precedence over local UI toggles, which aligns with the PR objective of "Device A's mode to win" when sending after a local toggle.


830-840: LGTM!

Setting permissionModeUpdatedAt to the current time correctly shields the user's explicit selection from being overwritten by older message-based inferences, while still allowing newer messages from other devices to take precedence.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 44190a1dca

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
sources/sync/storage.ts (1)

982-1011: Missing cleanup of permissionModeUpdatedAts when deleting a session.

The deleteSession function cleans up drafts and permission modes but does not remove the corresponding entry from session-permission-mode-updated-ats. This will cause timestamps for deleted sessions to accumulate in persistence over time.

🐛 Proposed fix to clean up timestamps
             const modes = loadSessionPermissionModes();
             delete modes[sessionId];
             saveSessionPermissionModes(modes);
+            
+            const updatedAts = loadSessionPermissionModeUpdatedAts();
+            delete updatedAts[sessionId];
+            saveSessionPermissionModeUpdatedAts(updatedAts);
             
             // Rebuild sessionListViewData without the deleted session

Note: You'll need to import loadSessionPermissionModeUpdatedAts and saveSessionPermissionModeUpdatedAts if not already available in scope (they are already imported at line 14).

🧹 Nitpick comments (2)
sources/sync/persistence.ts (1)

191-206: Consider adding stricter validation for the parsed data.

The current validation typeof parsed !== 'object' will pass for arrays since typeof [] === 'object'. Additionally, the values are not validated to be numbers, which could lead to unexpected behavior if corrupted data is loaded.

🔧 Suggested stricter validation
 export function loadSessionPermissionModeUpdatedAts(): Record<string, number> {
     const raw = mmkv.getString('session-permission-mode-updated-ats');
     if (raw) {
         try {
             const parsed = JSON.parse(raw);
-            if (!parsed || typeof parsed !== 'object') {
+            if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
                 return {};
             }
-            return parsed;
+            // Filter to only valid number values
+            const result: Record<string, number> = {};
+            for (const [key, value] of Object.entries(parsed)) {
+                if (typeof value === 'number') {
+                    result[key] = value;
+                }
+            }
+            return result;
         } catch (e) {
             console.error('Failed to parse session permission mode updated timestamps', e);
             return {};
         }
     }
     return {};
 }

Note: This is a defensive improvement. The existing code follows the same pattern as loadSessionDrafts and other similar functions in this file, so it's consistent with current conventions.

sources/sync/storage.ts (1)

839-869: Correct implementation of user-initiated permission mode updates.

Setting permissionModeUpdatedAt to Date.now() ensures local user selections are protected from older message-based inference until a newer message arrives from another device.

The persistence logic (building allModes/allUpdatedAts maps and saving) is duplicated between applyMessages (lines 574-588) and updateSessionPermissionMode (lines 854-869). Consider extracting to a helper function if this pattern expands further.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 44190a1 and c5b2167.

📒 Files selected for processing (2)
  • sources/sync/persistence.ts
  • sources/sync/storage.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Run yarn typecheck after all changes to ensure type safety
Ensure TypeScript strict mode is enabled - all code must be properly typed
Never use Alert module from React Native, always use @sources/modal/index.ts instead
Always apply layout width constraints from @/components/layout to full-screen ScrollViews and content containers for responsive design
Use StyleSheet.create from 'react-native-unistyles' for all styling
Always use function mode in StyleSheet.create when you need theme or runtime access
For Expo Image components, size properties (width, height) must be set as inline styles, not in Unistyles stylesheet, and tintColor must be set directly on the component
Always use t(...) function from @/text for ALL user-visible strings for internationalization
Always use expo-router API, not react-navigation API directly
Use ItemList for most containers for UI, only use custom components or alternatives for custom layouts like chat
Always try to use 'useHappyAction' from @sources/hooks/useHappyAction.ts for async operations instead of handling errors manually
For hotkeys, use 'useGlobalKeyboard' from @sources/hooks/useGlobalKeyboard - do not change it, it only works on Web
Use 'AsyncLock' class for exclusive async locks
Always use 'Avatar' component for displaying avatars

Files:

  • sources/sync/storage.ts
  • sources/sync/persistence.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Use 4 spaces for indentation
Use path alias @/* which maps to ./sources/*

Files:

  • sources/sync/storage.ts
  • sources/sync/persistence.ts
🧬 Code graph analysis (1)
sources/sync/storage.ts (2)
sources/sync/persistence.ts (3)
  • loadSessionPermissionModeUpdatedAts (191-206)
  • saveSessionPermissionModes (187-189)
  • saveSessionPermissionModeUpdatedAts (208-210)
sources/components/PermissionModeSelector.tsx (1)
  • PermissionMode (7-7)
🔇 Additional comments (5)
sources/sync/persistence.ts (1)

208-210: LGTM!

The save function follows the established pattern used by other persistence functions in this file.

sources/sync/storage.ts (4)

14-14: LGTM!

Import additions are correct and consistent with existing patterns.


253-253: LGTM!

Initialization follows the existing pattern for sessionDrafts and sessionPermissionModes.


307-331: LGTM!

The merging logic correctly prioritizes existing state > saved persistence > incoming session data. The use of nullish coalescing (??) for permissionModeUpdatedAt is appropriate since 0 could theoretically be a valid timestamp, and the comment clarifies this field is not server-synced.


523-588: Well-implemented permission mode inference logic.

The implementation correctly:

  1. Finds the most recent user message with permission mode metadata (lines 528-535).
  2. Only applies inference if the message timestamp is newer than the local update timestamp (line 547).
  3. Persists both modes and timestamps to ensure restart-safe arbitration.

One observation: The persistence loop (lines 576-583) iterates all sessions on each inference write. This is acceptable for typical session counts but worth noting if session volume grows significantly.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
sources/sync/storage.ts (1)

1017-1024: Missing cleanup for permissionModeUpdatedAts storage on session deletion.

The session deletion cleans up drafts and permission modes but leaves orphaned permissionModeUpdatedAt entries in persistent storage. This won't cause functional issues but will leak storage over time.

Proposed fix
             const modes = loadSessionPermissionModes();
             delete modes[sessionId];
             saveSessionPermissionModes(modes);
+            
+            const updatedAts = loadSessionPermissionModeUpdatedAts();
+            delete updatedAts[sessionId];
+            saveSessionPermissionModeUpdatedAts(updatedAts);
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c5b2167 and fecb131cb17439121375d77a3730f9d3d6f35900.

📒 Files selected for processing (2)
  • sources/sync/storage.ts
  • sources/sync/storageTypes.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Run yarn typecheck after all changes to ensure type safety
Ensure TypeScript strict mode is enabled - all code must be properly typed
Never use Alert module from React Native, always use @sources/modal/index.ts instead
Always apply layout width constraints from @/components/layout to full-screen ScrollViews and content containers for responsive design
Use StyleSheet.create from 'react-native-unistyles' for all styling
Always use function mode in StyleSheet.create when you need theme or runtime access
For Expo Image components, size properties (width, height) must be set as inline styles, not in Unistyles stylesheet, and tintColor must be set directly on the component
Always use t(...) function from @/text for ALL user-visible strings for internationalization
Always use expo-router API, not react-navigation API directly
Use ItemList for most containers for UI, only use custom components or alternatives for custom layouts like chat
Always try to use 'useHappyAction' from @sources/hooks/useHappyAction.ts for async operations instead of handling errors manually
For hotkeys, use 'useGlobalKeyboard' from @sources/hooks/useGlobalKeyboard - do not change it, it only works on Web
Use 'AsyncLock' class for exclusive async locks
Always use 'Avatar' component for displaying avatars

Files:

  • sources/sync/storage.ts
  • sources/sync/storageTypes.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Use 4 spaces for indentation
Use path alias @/* which maps to ./sources/*

Files:

  • sources/sync/storage.ts
  • sources/sync/storageTypes.ts
🔇 Additional comments (6)
sources/sync/storageTypes.ts (2)

24-27: LGTM! Schema extensions align well with the permission sync feature.

The new fields correctly extend the metadata schema:

  • flavor as .nullish() allows flexibility for optional session variants
  • permissionMode enum values match the PermissionMode type from PermissionModeSelector
  • permissionModeUpdatedAt enables timestamp-based conflict resolution

76-76: LGTM!

The permissionModeUpdatedAt field correctly mirrors the structure added to MetadataSchema and supports the local coordination timestamp pattern described in the PR.

sources/sync/storage.ts (4)

14-14: LGTM!

Import additions for the new persistence functions are correct and complete.


322-356: LGTM! Well-structured permission mode merging with timestamp-based conflict resolution.

The merge logic correctly implements the priority chain:

  1. Existing in-memory state
  2. Saved from persistence
  3. Session default
  4. CLI metadata (only if timestamp is newer)

The timestamp comparison at line 343 properly prevents stale CLI metadata from overwriting more recent local changes.


548-613: LGTM! Clean implementation of permission mode inference from messages.

The inference logic correctly:

  • Finds the most recent user-text message (leveraging the descending sort order)
  • Uses timestamp comparison to prevent older messages from overriding user selections
  • Only persists when the mode actually changes
  • Updates both sessionPermissionModes and sessionPermissionModeUpdatedAts for consistency

864-894: LGTM! Correct timestamp update on user-initiated permission mode changes.

Setting permissionModeUpdatedAt = Date.now() ensures local user selections are protected from older message-based inference while still allowing newer messages from other devices to take precedence.

@leeroybrun leeroybrun force-pushed the slopus/pr/permission-mode-from-last-message-2026-01-13 branch 2 times, most recently from 16dba98 to e00bbbf Compare January 13, 2026 11:26
@leeroybrun
Copy link
Copy Markdown
Collaborator Author

@coderabbitai @codex @gemini-code-assist please review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Jan 13, 2026

@leeroybrun, I'll review the changes in this PR right away.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a mechanism to synchronize the permission mode of a session across different devices. The implementation correctly uses an updatedAt timestamp (permissionModeUpdatedAt) to resolve conflicts between local changes and updates from other devices, ensuring the most recent change always wins. New persistence functions for these timestamps are added, and the logic is integrated into session creation, message processing, and manual permission mode updates.

The changes are well-thought-out and address the problem described. I have a couple of suggestions to improve code maintainability by reducing code duplication and centralizing type definitions. Please see my detailed comments.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e00bbbff5e

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@leeroybrun leeroybrun force-pushed the slopus/pr/permission-mode-from-last-message-2026-01-13 branch from e00bbbf to b5ebadc Compare January 13, 2026 12:12
@leeroybrun
Copy link
Copy Markdown
Collaborator Author

@codex @coderabbitai @gemini-code-assist please review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Jan 13, 2026

@leeroybrun, I'll review the changes in this PR right away.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request effectively implements cross-device synchronization for session permission modes by storing the mode in message metadata and using a server-aligned timestamp to resolve conflicts between local changes and remote updates. The introduction of a centralized PermissionModes.ts constant and a time.ts utility for clock-skew correction are excellent architectural improvements that enhance maintainability and robustness. The core logic for inferring, persisting, and updating the permission mode appears sound. I've left a couple of minor suggestions to improve error handling and code formatting.

@leeroybrun leeroybrun force-pushed the slopus/pr/permission-mode-from-last-message-2026-01-13 branch 2 times, most recently from c80380b to 1a9acb4 Compare January 13, 2026 13:37
@leeroybrun
Copy link
Copy Markdown
Collaborator Author

@codex @coderabbitai @gemini-code-assist please review the fixes we added/force-pushed to ensure they fixes the issues you reported, and that no new issue exists or was introduced

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Jan 13, 2026

@leeroybrun, I'll review the force-pushed changes to verify that the reported issues have been fixed and check for any new issues.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a robust mechanism to synchronize session permission modes across devices, addressing several related issues. The core changes involve centralizing the PermissionMode type definition, implementing server-aligned timestamps for consistent ordering, and carefully managing the precedence between user-initiated and inferred permission modes. The refactoring to use a shared PERMISSION_MODES constant and PermissionMode type significantly improves maintainability and type safety throughout the codebase. The addition of permissionModeUpdatedAt is a critical component, ensuring that the most recent permission setting, whether user-defined or inferred from messages, is always respected.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1a9acb4505

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +334 to +336
sessions.forEach(session => {
// Keep a best-effort server time offset for clock-safe local timestamps.
observeServerTimestamp(session.updatedAt);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid calibrating server time from stale updatedAt

The new call to observeServerTimestamp(session.updatedAt) uses the session’s last-update time as if it were “current server time”. If a session hasn’t been updated recently (e.g., last message was hours ago), this sets the server offset far into the past, so nowServerMs() (used for permissionModeUpdatedAt) will be earlier than actual recent server messages. In that case, a user toggling permission mode locally right after loading the session can be incorrectly overridden by a newer message that was actually sent before the toggle, because the local timestamp is skewed backward. Consider only updating the offset from timestamps known to be close to “now” (e.g., response headers or realtime message events), or guard against stale updatedAt values.

Useful? React with 👍 / 👎.

@leeroybrun leeroybrun force-pushed the slopus/pr/permission-mode-from-last-message-2026-01-13 branch 2 times, most recently from 6e2e747 to ae1e0db Compare January 13, 2026 17:13
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
sources/sync/storage.ts (2)

256-273: Consider adding error handling for persistence failures.

If saveSessionPermissionModes throws, saveSessionPermissionModeUpdatedAts won't execute and the in-memory cache won't be updated, potentially leaving the state inconsistent between modes and timestamps.

♻️ Suggested improvement
 const persistSessionPermissionData = (sessions: Record<string, Session>) => {
     const allModes: Record<string, PermissionMode> = {};
     const allUpdatedAts: Record<string, number> = {};

     Object.entries(sessions).forEach(([id, sess]) => {
         if (sess.permissionMode && sess.permissionMode !== 'default') {
             allModes[id] = sess.permissionMode;
         }
         if (typeof sess.permissionModeUpdatedAt === 'number') {
             allUpdatedAts[id] = sess.permissionModeUpdatedAt;
         }
     });

-    saveSessionPermissionModes(allModes);
-    saveSessionPermissionModeUpdatedAts(allUpdatedAts);
-    sessionPermissionModes = allModes;
-    sessionPermissionModeUpdatedAts = allUpdatedAts;
+    try {
+        saveSessionPermissionModes(allModes);
+        saveSessionPermissionModeUpdatedAts(allUpdatedAts);
+        sessionPermissionModes = allModes;
+        sessionPermissionModeUpdatedAts = allUpdatedAts;
+    } catch (e) {
+        console.error('Failed to persist session permission data:', e);
+    }
 };

576-577: Consider validating the inferred permission mode.

The type assertion as PermissionMode | undefined assumes the stored value is valid. If malformed data exists, this could propagate an invalid mode.

♻️ Suggested validation
+import { PERMISSION_MODES } from '@/constants/PermissionModes';
+
 for (const message of messagesArray) {
     if (message.kind !== 'user-text') continue;
-    const mode = message.meta?.permissionMode as PermissionMode | undefined;
-    if (!mode) continue;
+    const rawMode = message.meta?.permissionMode;
+    if (!rawMode || !PERMISSION_MODES.includes(rawMode as any)) continue;
+    const mode = rawMode as PermissionMode;
     inferredPermissionMode = mode;
     inferredPermissionModeAt = message.createdAt;
     break;
 }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6e2e747f8ec2dcb2a9968d53eb95f70bb3f41cda and ae1e0db13dbb4bc92fa0b412462499b5025f6a00.

📒 Files selected for processing (12)
  • sources/-session/SessionView.tsx
  • sources/components/PermissionModeSelector.tsx
  • sources/constants/PermissionModes.ts
  • sources/sync/apiSocket.ts
  • sources/sync/persistence.ts
  • sources/sync/settings.ts
  • sources/sync/storage.ts
  • sources/sync/storageTypes.ts
  • sources/sync/sync.ts
  • sources/sync/time.ts
  • sources/sync/typesMessageMeta.ts
  • sources/sync/typesRaw.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • sources/sync/time.ts
  • sources/sync/sync.ts
  • sources/components/PermissionModeSelector.tsx
  • sources/-session/SessionView.tsx
  • sources/constants/PermissionModes.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Run yarn typecheck after all changes to ensure type safety
Ensure TypeScript strict mode is enabled - all code must be properly typed
Never use Alert module from React Native, always use @sources/modal/index.ts instead
Always apply layout width constraints from @/components/layout to full-screen ScrollViews and content containers for responsive design
Use StyleSheet.create from 'react-native-unistyles' for all styling
Always use function mode in StyleSheet.create when you need theme or runtime access
For Expo Image components, size properties (width, height) must be set as inline styles, not in Unistyles stylesheet, and tintColor must be set directly on the component
Always use t(...) function from @/text for ALL user-visible strings for internationalization
Always use expo-router API, not react-navigation API directly
Use ItemList for most containers for UI, only use custom components or alternatives for custom layouts like chat
Always try to use 'useHappyAction' from @sources/hooks/useHappyAction.ts for async operations instead of handling errors manually
For hotkeys, use 'useGlobalKeyboard' from @sources/hooks/useGlobalKeyboard - do not change it, it only works on Web
Use 'AsyncLock' class for exclusive async locks
Always use 'Avatar' component for displaying avatars

Files:

  • sources/sync/typesRaw.ts
  • sources/sync/typesMessageMeta.ts
  • sources/sync/settings.ts
  • sources/sync/storage.ts
  • sources/sync/storageTypes.ts
  • sources/sync/apiSocket.ts
  • sources/sync/persistence.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Use 4 spaces for indentation
Use path alias @/* which maps to ./sources/*

Files:

  • sources/sync/typesRaw.ts
  • sources/sync/typesMessageMeta.ts
  • sources/sync/settings.ts
  • sources/sync/storage.ts
  • sources/sync/storageTypes.ts
  • sources/sync/apiSocket.ts
  • sources/sync/persistence.ts
🧬 Code graph analysis (4)
sources/sync/typesRaw.ts (1)
sources/constants/PermissionModes.ts (1)
  • PERMISSION_MODES (1-9)
sources/sync/typesMessageMeta.ts (1)
sources/constants/PermissionModes.ts (1)
  • PERMISSION_MODES (1-9)
sources/sync/settings.ts (1)
sources/constants/PermissionModes.ts (1)
  • PERMISSION_MODES (1-9)
sources/sync/apiSocket.ts (1)
sources/sync/time.ts (1)
  • observeServerTimestamp (3-8)
🔇 Additional comments (20)
sources/sync/apiSocket.ts (2)

4-4: LGTM!

Import added for the server time calibration utility.


184-203: LGTM! Server time calibration via HTTP Date header.

This best-effort approach correctly:

  • Awaits the fetch before extracting headers
  • Validates the parsed timestamp with Number.isNaN
  • Silently handles failures to avoid disrupting the request flow

The HTTP Date header is a reliable source for server time alignment, avoiding the pitfall of deriving time from potentially stale resource timestamps.

sources/sync/typesRaw.ts (2)

3-3: LGTM!

Centralizes permission mode enum source for consistency with the rest of the codebase.


56-56: LGTM! Schema expansion is backward compatible.

The permission modes now align with the centralized PERMISSION_MODES constant, expanding from 3 to 7 valid values. Existing persisted data with the original modes remains valid.

sources/sync/settings.ts (2)

2-2: LGTM!

Import aligns with the centralized permission mode definition.


120-120: LGTM!

Consistent with the PR's goal of centralizing permission mode definitions. The AIBackendProfile schema now validates against the same source of truth used across the codebase.

sources/sync/persistence.ts (2)

6-6: LGTM!

Import path updated to the centralized constants module.


191-217: LGTM! Defensive persistence helpers for permission mode timestamps.

The implementation correctly:

  • Validates the parsed object structure (non-null, non-array object)
  • Filters to only finite numeric values, preventing invalid timestamps from propagating
  • Follows the established error-handling pattern used by other persistence functions in this file
sources/sync/typesMessageMeta.ts (2)

2-2: LGTM!

Import aligns with the centralized permission mode definition used throughout the PR.


7-7: LGTM!

This change is essential for the PR's objective—storing permissionMode in message metadata enables inferring the session's permission mode from the most recent user message when syncing across devices.

sources/sync/storageTypes.ts (3)

2-3: LGTM!

Clean imports using type-only import for PermissionMode and regular import for the PERMISSION_MODES constant array. This separation is correct since the constant is needed at runtime for the Zod schema while the type is only needed for TypeScript compilation.


26-29: LGTM!

The schema additions are well-structured:

  • permissionMode uses z.enum(PERMISSION_MODES) which properly validates against the allowed values.
  • permissionModeUpdatedAt uses z.number() for timestamp tracking.
  • Both fields are optional, ensuring backward compatibility with existing data.

77-78: LGTM!

The Session interface fields correctly mirror the schema additions with appropriate nullable types (PermissionMode | null and number | null). The inline comments clearly document that these are local-only fields not synced to the server, which is important for understanding the synchronization model.

sources/sync/storage.ts (7)

14-15: LGTM!

New imports are correctly organized:

  • Persistence functions for loading/saving permission mode timestamps
  • Type-only import for PermissionMode
  • nowServerMs for server-aligned timestamps

Also applies to: 24-24


328-377: LGTM!

The merge logic correctly prioritizes permission mode sources:

  1. Existing in-memory state (preserves current session state)
  2. Saved persistence data (initial load)
  3. Session data from server
  4. Default fallback

The metadata-based override (lines 362-368) correctly applies CLI-published permission modes only when the timestamp is newer than local state. This enables seamless handoff between devices while respecting local user selections.


569-581: LGTM!

The inference logic correctly scans messages in reverse chronological order (newest first) and extracts the permission mode from the most recent user message. The early break ensures efficiency by stopping at the first match.


589-624: LGTM!

The timestamp comparison logic is sound:

  • canInferPermissionMode gates on the inferred timestamp being newer than the local permissionModeUpdatedAt
  • shouldWritePermissionMode adds an additional check to avoid no-op updates
  • Persistence is correctly called only when the mode changes

The inline comments clearly document the timestamp coordination strategy between server timestamps and nowServerMs().


871-895: LGTM!

The implementation correctly:

  • Uses the typed PermissionMode parameter
  • Stamps permissionModeUpdatedAt with nowServerMs() to coordinate with server timestamps
  • Persists the updated state immediately

The timestamp marking ensures user-initiated mode changes are protected from being overridden by older message-based inference, while still allowing newer messages to take precedence.


1020-1025: LGTM!

The cleanup correctly removes permissionModeUpdatedAt data when a session is deleted, following the same pattern used for permission modes. Both persistence and in-memory cache are updated to maintain consistency.


120-120: LGTM!

Interface signature correctly updated to use the typed PermissionMode instead of a string union, maintaining type safety across the codebase.

@leeroybrun leeroybrun force-pushed the slopus/pr/permission-mode-from-last-message-2026-01-13 branch from ae1e0db to 6409cbe Compare January 13, 2026 21:25
@leeroybrun
Copy link
Copy Markdown
Collaborator Author

@codex @coderabbitai @gemini-code-assist please review

@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Jan 13, 2026

@leeroybrun, I'll review the force-pushed changes to verify that the reported issues have been resolved and check for any new issues or regressions introduced by the fixes.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request effectively implements cross-device synchronization for session permission modes by storing the mode in message metadata and using timestamps for conflict resolution. The changes are well-structured, introducing a centralized PermissionModes constant, a time synchronization utility, and updating the storage logic to handle the new state. The logic for inferring modes from messages and respecting local user changes via timestamps is sound. I have one suggestion to improve the robustness of data cleanup in the storage layer. Overall, this is a solid implementation of the feature.

Comment on lines +1025 to +1030
sessionPermissionModes = modes;

const updatedAts = loadSessionPermissionModeUpdatedAts();
delete updatedAts[sessionId];
saveSessionPermissionModeUpdatedAts(updatedAts);
sessionPermissionModeUpdatedAts = updatedAts;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The pattern used here to clear persisted data for both modes and updatedAts can be simplified and made more robust. Instead of re-loading from storage, you can modify the in-memory cache variables (sessionPermissionModes and sessionPermissionModeUpdatedAts) directly and then save them. This avoids unnecessary disk reads and is safer against potential race conditions if persistence were to become asynchronous.

Applying this improved pattern would change lines 1022-1030 to something like this:

// In deleteSession...
delete sessionPermissionModes[sessionId];
saveSessionPermissionModes(sessionPermissionModes);

delete sessionPermissionModeUpdatedAts[sessionId];
saveSessionPermissionModeUpdatedAts(sessionPermissionModeUpdatedAts);

Since this change spans existing code, I'm providing it as an example rather than a direct suggestion.

leeroybrun pushed a commit that referenced this pull request Jan 22, 2026
feat: support CLAUDE_CONFIG_DIR environment variable
@leeroybrun leeroybrun closed this Jan 31, 2026
@leeroybrun leeroybrun deleted the slopus/pr/permission-mode-from-last-message-2026-01-13 branch January 31, 2026 15:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant