How Pi‑Web Persists Startup Preferences Without Replaying Agent Commands

TLDR: Pi‑Web writes explicit UI selections directly to the user's settings.json via SettingsManager immediately after session creation, comparing them against the effective values already persisted by the AgentSession constructor. If they match, it updates the defaults and flushes to disk without ever invoking setModel() or setThinkingLevel(), preventing duplicate session entries.

When working with the pi-web coding agent, preserving a user's selected model and thinking level across restarts presents a unique architectural challenge. The system must remember these startup preferences without appending redundant commands to the session history or firing duplicate extension events. According to the pi-web source code, this is achieved by persisting preferences to a standalone settings file rather than replaying agent commands, ensuring clean .jsonl session files and idempotent configuration.

The Problem: Constructor Persistence Creates Side Effects

When a new AgentSession is instantiated, its constructor immediately writes the effective model and thinking level into the session file. If the UI were to subsequently call setModel() or setThinkingLevel() to "save" these preferences, the agent would append extra entries to the session history and fire redundant extension events. This duplication corrupts the session log and wastes computational resources. Pi‑Web therefore avoids re-invoking these setter methods entirely, opting instead to persist the explicit browser selections directly into the user's settings.json before any setter commands are processed.

The Pre‑Session Persistence Workflow

Pi‑Web solves this through a strict seven-step validation and persistence pipeline implemented in lib/startup-preferences.ts and orchestrated by lib/rpc-manager.ts:

  1. Collect explicit preferences supplied by the UI (initialModel and thinkingLevel).
  2. Create the session with those values (or with defaults derived from current settings).
  3. Determine effective values that the session actually started with (inner.model, inner.thinkingLevel, inner.supportsThinking()).
  4. Compare the explicit UI values with the effective ones.
  5. If they match, write them to the SettingsManager as defaults via setDefaultModelAndProvider and/or setDefaultThinkingLevel.
  6. Flush the SettingsManager to persist the changes to disk.
  7. Never call inner.setModel() or inner.setThinkingLevel() again—the preferences are now stored, and future sessions will start with them automatically.

Validation Through Comparison

The core logic resides in lib/startup-preferences.ts (lines 33‑44). The function receives both the explicit UI choices and the effective session values, performing an equality check before writing. This ensures that only valid, actually-applied configurations become defaults, preventing the persistence of unsupported model selections or invalid states.

Atomic Disk Persistence

Once validated, the helper invokes settingsManager.setDefaultModelAndProvider and settingsManager.setDefaultThinkingLevel (lines 39‑50), followed by settingsManager.flush() (line 53). This sequence atomically writes to ~/.pi/agent/settings.json, guaranteeing that preferences survive application restarts without requiring any command replay from the session history.

Implementation: lib/startup-preferences.ts

The persistExplicitStartupPreferences function encapsulates the comparison and persistence logic. Located at lines 15‑55, it accepts the user's explicit choices and the session's effective state, returning a flag indicating whether the model default changed (to trigger cache invalidation).

// Example: manual persistence (rarely needed – used internally by Pi‑Web)
import { SettingsManager } from '@earendil-works/pi-coding-agent';
import {
  persistExplicitStartupPreferences,
  ExplicitStartupPreferences,
} from '@/lib/startup-preferences';

async function saveUserPreferences(
  settings: SettingsManager,
  uiPrefs: ExplicitStartupPreferences,
  sessionEffective: {
    model?: { provider: string; modelId: string };
    thinkingLevel: ThinkingLevel;
    supportsThinking: boolean;
  },
) {
  const { modelDefaultChanged } = await persistExplicitStartupPreferences(
    settings,
    uiPrefs,
    sessionEffective,
  );

  // Optional: refresh UI if the default model changed
  if (modelDefaultChanged) {
    // e.g. invalidate UI‑side model cache
  }
}

Integration: lib/rpc-manager.ts

The RPC manager orchestrates the workflow immediately after AgentSession construction (lines 1633‑1648). It extracts the effective values from the session inner object, invokes the persistence helper, and conditionally clears the model cache if the default changed.

// Inside lib/rpc-manager.ts – how Pi‑Web invokes the helper
const persistedPreferences = await persistExplicitStartupPreferences(
  services.settingsManager,
  {
    ...(initialModel ? { model: initialModel } : {}),
    ...(thinkingLevel ? { thinkingLevel } : {}),
  },
  {
    ...(inner.model
      ? { model: { provider: inner.model.provider, modelId: inner.model.id } }
      : {}),
    thinkingLevel: inner.thinkingLevel,
    supportsThinking: inner.supportsThinking(),
  },
);

if (persistedPreferences.modelDefaultChanged) invalidateModelsCache();

This integration ensures that the settings.json file is updated synchronously with session startup, eliminating the need to replay any configuration commands when the application restarts.

Summary

  • Pi‑Web avoids command replay by writing startup preferences directly to settings.json via the SettingsManager API rather than invoking agent setters.
  • The persistExplicitStartupPreferences helper in lib/startup-preferences.ts (lines 15‑55) validates that explicit UI choices match the effective session values before persisting.
  • Defaults are only updated when the comparison succeeds, ensuring that unsupported or invalid configurations are never written to disk.
  • SettingsManager.flush() atomically commits changes to ~/.pi/agent/settings.json, making preferences durable across restarts.
  • lib/rpc-manager.ts (lines 1633‑1648) coordinates the workflow and triggers invalidateModelsCache() when the default model changes, keeping the UI state synchronized.

Frequently Asked Questions

Why doesn't pi-web just call setModel() after creating the session?

The AgentSession constructor immediately persists effective values to the session .jsonl file upon instantiation. Re-invoking setModel() or setThinkingLevel() would append duplicate entries to this log and fire redundant extension events, corrupting the session history. By writing directly to settings.json instead, pi-web stores preferences without any side effects on the active session.

Where exactly are these startup preferences stored on disk?

The preferences are persisted to the user's local settings file at ~/.pi/agent/settings.json. The SettingsManager class provides the abstraction for this file, exposing methods like setDefaultModelAndProvider, setDefaultThinkingLevel, and flush to perform atomic disk writes without manual file handling.

What happens if the explicit UI values don't match the effective session values?

The comparison logic in lib/startup-preferences.ts (lines 33‑44) verifies that the explicit user selections exactly match the values actually adopted by the session. If they differ—perhaps due to model availability changes or validation failures—the function skips writing to SettingsManager, preventing the persistence of invalid or unsupported defaults.

Does this persistence method survive application restarts?

Yes. Because preferences are written to the standalone settings.json file on disk via SettingsManager.flush(), they are completely independent of the transient session state or .jsonl log files. When pi-web restarts, it reads these defaults during AgentSession construction, applying the previous configuration without replaying any historical commands.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →