Skip to content

feat(profiles): support session-bound active profiles without global routing mutation #1064

Open

Description

@noxsystems

Before submitting

  • I searched open and closed issues and did not find a request for this feature.
  • I reviewed this request and removed credentials, tokens, private paths, hostnames, and other sensitive data.

Problem or opportunity

Concurrent Pi processes may need different active profiles while sharing the same machine or project. Today, /gentle:profiles updates shared configuration and materialized routing by default. Running work remains unchanged, but later subagent launches can observe another process's profile changes.

This affects ordinary terminals, terminal multiplexers, IDE panes, CI, and other environments. It is a Pi process/session configuration boundary rather than a host-specific integration concern.

Proposed outcome

Preserve the existing global profile mechanism and add an explicit, opt-in active-profile binding at the parent Pi session boundary.

The profile UI should distinguish actions conceptually equivalent to:

  • Use in this session — resolve and store a session-local profile binding and effective routing snapshot without mutating shared routing files.
  • Reload in this session — explicitly refresh that session's snapshot from the saved profile definition.
  • Set as global default — retain the existing global persistence and materialization behavior for future or unbound sessions.

Session-bound behavior should satisfy these invariants:

  • Concurrent parent sessions can select different profiles without rewriting each other's shared defaults or routing.
  • Switching profiles within one parent session updates only that session's binding.
  • New child launches inherit the parent's current effective snapshot.
  • Running and already-queued children retain the routing resolved when they were launched or queued.
  • Resuming a parent session restores its last session-bound profile and snapshot.
  • Editing a saved profile definition does not silently alter an already-bound session; refreshing it requires an explicit reload or re-selection.
  • Display the effective profile, scope, and configuration source.
  • Existing behavior remains unchanged when no session binding is selected.
  • Existing profile files require no migration.

A possible precedence model is:

explicit launch-local route
→ parent session-bound profile snapshot
→ project routing
→ global active profile
→ existing defaults

The exact command names and persistence mechanism can follow the maintainers' preferred configuration authority. The central invariant is that session-scoped selection must not mutate routing observed by unrelated concurrent parent sessions.

Suggested regression coverage:

  • two concurrent parent sessions using different profiles;
  • concurrent sessions in the same project;
  • switching one session while the other remains unchanged;
  • queued and running child stability;
  • future child inheritance after switching;
  • session resume;
  • explicit session reload after editing a saved profile;
  • unchanged behavior for sessions without a binding;
  • existing profile files loading without migration.

Relationship to the existing per-repository pins

The per-repository pin shipped in #1074 solves a narrower configuration boundary:

  • p writes a clone-local selection to <git-common-dir>/gentle-ai/profile-pin.json. It is isolated from other clones, but every worktree and every Pi session in that clone shares it.
  • P writes a repository declaration to <worktree-root>/.pi/gentle-ai/profile.json. It is intended as a shared repository or team default, so it is also shared by concurrent sessions using that worktree.
  • Both layers affect subagent routing at launch without rewriting the globally active routing. The clone-local p layer takes precedence over P, then routing falls back to the globally active profile.

Those pins solve parallel work across different repositories or clones. They do not allow two parent sessions in the same clone to select different profiles. If session A changes p, future child launches from session B observe the same clone-local selection. P is likewise a repository default, not a session override.

This request adds an independent layer above those defaults. A parent session should hold its own effective profile snapshot, children should inherit the snapshot of the parent that launched or queued them, and another session should remain unchanged. Because it is a snapshot rather than a shared profile-name lookup on every launch, later edits to the saved profile definition should require explicit reload or re-selection before affecting that bound session.

The resulting conceptual precedence is:

explicit launch-local route
→ parent session-bound profile snapshot
→ clone-local pin (`p`)
→ repository declaration (`P`)
→ global active profile
→ existing defaults

The existing p and P behavior should remain unchanged. They become defaults for sessions that have no explicit session binding.

Alternatives considered

Separate GENTLE_PI_CONFIG_HOME and GENTLE_PI_AGENT_HOME / PI_CODING_AGENT_DIR roots provide supported path isolation, but require external environment setup and duplicated configuration rather than a native session binding.

Project-local routing helps separate projects but does not solve concurrent sessions in the same project. Switching the shared active profile is not session isolation.

Additional context

Related: #1025 proposes authorized routing classes selected for individual child launches. This request concerns a human-selected active profile bound to the parent Pi session and inherited as its default routing context. The two features can coexist if launch-local routes have explicit higher precedence.

Coordinate precedence with #600 and preserve the path-isolation support established by #575 and #633.

Activity

  1. JeanN00B commented on Sep 16, 2026

    @JeanN00B

    Additional concurrency finding from a source and runtime audit:

    The global scope is documented and should remain backward-compatible, but concurrent profile operations are not serialized. profiles.json and Pi orchestrator settings use per-file temp-and-rename writes, while models.json is written directly. Applying a profile spans the profile store, models.json, materialized subagents.json or agent frontmatter, and Pi settings without a cross-file lock or transaction. Two processes can therefore read stale panel state, overwrite each other's active marker or routing, or leave the stores representing different profiles. ppi does not solve this by itself because it only sets PI_CODING_AGENT_DIR; Gentle's GENTLE_PI_CONFIG_HOME remains shared, and same-cwd project routing remains shared too.

    A narrow disposable two-process repro confirmed the shared-store behavior: process A wrote profile A, process B read A; after B wrote profile B, A read B. Using distinct GENTLE_PI_CONFIG_HOME values kept the stores isolated. Suggested coverage includes concurrent apply, create, update, and rollback operations, stale panel state, and shared versus isolated config homes. This is distinct from the session-bound behavior requested here, but likely worth considering in the same design.

  2. daandradec commented on Sep 16, 2026

    @daandradec

    +1 on this — here is a concrete workflow that needs the session-bound invariant, in case it helps with prioritization.

    Use case

    I run two pi sessions side by side in two completely separate project directories, with two profiles created via /gentle:profiles: one routed to OpenAI, one to DeepSeek. Project A is the OpenAI work, project B is the DeepSeek work.

    What I need from this feature is exactly one of the invariants already listed: selecting the DeepSeek profile in session B must leave session A on OpenAI. The two that matter most for this workflow are:

    • "Concurrent parent sessions can select different profiles without rewriting each other's shared defaults or routing."
    • "New child launches inherit the parent's current effective snapshot."

    What happens today

    The active marker is machine-global, so choosing a profile in one session changes the routing that every other concurrent session resolves for its next subagent launch. In the two-project setup above, after activating DeepSeek in project B, /gentle:models in project A had moved to DeepSeek as well.

    The per-repository pin shipped in #1074 (p on a profile) covers the two-separate-repositories case and is the right workaround today. It does not cover the general case this request is about: concurrent sessions that share one repository, or a single session where I want a different profile than the repository default without touching shared state.

    Environment

    • Pi: 0.85.1 (@earendil-works/pi-coding-agent)
    • gentle-ai CLI: 2.8.2
    • gentle-pi: source checkout v2.5.0-179-g8ec0f98b (package.json 2.5.0)
    • OS: macOS

    No new mechanism to propose — the precedence model in the description already handles this case. Happy to test a branch once session binding lands.

  3. LCubero commented on Sep 17, 2026

    @LCubero

    Confirmed a Windows reproduction that adds one specific failure mode to this request: /gentle:profiles persists the profile's orchestrator entry to Pi's global defaultProvider, defaultModel, and defaultThinkingLevel, but it does not update the model or thinking level of the current parent session.

    The paths resolve correctly on Windows, with no agent/config-home overrides and no project-local .pi/settings.json, so this does not appear to be a Windows path issue. The live session state can diverge from both settings.json and the active profile. Also, s snapshots the orchestrator through readOrchestratorSettings(...), which captures the global defaults rather than ctx.model and ctx.thinkingLevel from the running session.

    For the proposed Use in this session behavior, please include both sides explicitly: capture the live parent model/effort from the session, and apply the selected profile through Pi's current-session APIs (pi.setModel() and pi.setThinkingLevel()). Persisting default* should remain the separate global-default action.

  4. AitorDuran commented on Sep 27, 2026

    @AitorDuran

    A concrete parallel-work scenario for this request:

    • Session A in one worktree uses an OpenSource profile.
    • Session B in another worktree (or even a different project) uses an OpenAI profile.
    • Session C uses a Claude-Hybrid profile, with Claude, OpenAI, and NaN models assigned to different roles.

    The reported behavior is that selecting a profile in one session changes the effective profile in other already-running sessions, even across projects. That makes it impractical to run these independent model/provider mixes in parallel, although each session can already use a different orchestrator.

    Desired behavior: selecting a profile changes only the current session's effective model routing. Other live sessions should retain their chosen profiles regardless of whether they share a repository, belong to separate worktrees, or run in unrelated projects. Any repository-level default for new sessions should not silently rewrite existing sessions.

  5. Denver2828 commented on Sep 27, 2026

    @Denver2828

    Adding a concrete 3.7.0 use case for this session-bound routing proposal: please distinguish saved defaults from the dispatching parent's live model and thinking level. This is a UX/contract clarification and opt-in enhancement request, not a confirmed regression.

    Observed with Gentle Agents (subagent_run): the parent session recorded openai-codex/gpt-6-astra with low thinking before dispatch, but an unpinned gentle-ai-verify launched with openai-codex/gpt-5.5 / medium, matching the saved Pi default. No verifier model assignment was present in the agent definition or saved routing. Child runtime metadata confirmed the difference.

    The installed 3.7.0 implementation explains the result:

    • resolveAgentProfile resolves profile, then agent definition, then configured defaults. It does not take the parent's live model as an input.
    • childArguments omits --model when unresolved, leaving the child to use Pi startup defaults.
    • The versioned documentation says “Inherit active/default model”, while the repository-pin section describes inheritance as “their own definition, then the default model”. The former is easy to interpret as the live parent selection, but I did not find an explicit promise of that behavior.

    For the design here, could we make these separate, user-owned choices: use saved routing/defaults (unchanged existing behavior) and inherit the parent session's live model + thinking (explicit opt-in)? A session-bound profile snapshot and a live /model selection are not necessarily the same thing, so their interaction and precedence should be stated rather than inferred. Explicit agent assignments should remain predictable, with the resolved provider/model/thinking and source visible before execution. The intended benefit is predictable cost and capability, not forcing every role onto one model.

    Suggested coverage: parent selection differs from saved defaults; explicit role assignments; parent changes after queueing (queued tasks retain their resolved snapshot); and unchanged behavior without opt-in. This can fit the design discussion in this issue alongside #1025 rather than creating another overlapping tracker. No claim is made that current main has been reproduced or that the existing behavior violates a documented live-inheritance contract.

  6. matraket commented on Sep 29, 2026

    @matraket
    Contributor

    I ran into this problem while working with two sessions in the same repo: one on a frontier-model profile and one on a local/open-source profile to translate a documentation corpus. I spent quite some time analyzing the best way to solve it, and along the way I came across this issue and its comments, as well as #1240 and #1478, which tackle the same problem from different angles.

    This comment is an attempt to consolidate my approach with the issue and with everything contributed in the thread. To avoid arguing from assumptions, I traced the code on current main (5cc591f) and on Pi 0.87.1. Everything below is verified against that code unless I say otherwise.

    Where I agree and where I don't

    I agree with @noxsystems on the central invariant: selecting a profile in one parent session must not change what any other session launches. I also agree with the precedence (launch-local route, then session, then p, then P, then global), with children inheriting the parent's selection, with running and queued children keeping what they resolved (this already holds today: the queue stores the fully resolved request, agents-runner.ts:349), with resume restoring the selection, and with no migration of existing files.

    I disagree with @noxsystems on making the session binding opt-in. If Enter keeps mutating global state and "use in this session" is a separate action, the default path is still the one that silently moves every other session. The p and P keys already establish the right mental model: Enter picks a profile for this session, and the persistent layers are explicit keys. I propose that Enter becomes session-only and that a new G key sets the global default, next to p (clone) and P (repository).

    On snapshot versus following the profile by name, I don't have a settled position. I see arguments for both. Simplicity favors storing only the name: in my opinion, the most common reason to edit a profile is tuning it (an agent is under- or over-powered), and in that case it makes sense for every session using it to get the adjustment without a reload in each one. Avoiding silent changes favors the snapshot + reload that @noxsystems proposes. I think both options are reasonable and only real usage will tell which one was right, so either works for me. What does seem important to me is not letting this decision block the rest: it is a minor detail compared with the current situation, where it is practically impossible to parallelize work that needs different capability levels. Whichever option is chosen, children that are already running or queued are unaffected (this already holds today), and if the bound profile is deleted, the session falls back to the next layer.

    I agree with @JeanN00B that the profile stores are not serialized. With this proposal the only shared writes left are explicit (G, p, P, create/edit/rename/delete), so there are few of them, but they still need to re-read before writing (today the panel writes back an in-memory copy of profiles.json), a lock, atomic writes for models.json, and rename retries on Windows (EPERM/EBUSY).

    I agree with @daandradec that p is the right tool for separate repositories and does not cover sessions in the same clone. One detail to add: today, when a pin exists, Enter rewrites the clone-local pin (gentle-ai.ts:4122-4128). In other words, in a pinned repo, one session's Enter changes the profile of every session and worktree of that clone. I believe this is exactly the case I ran into.

    I agree with @LCubero that "use in this session" should go through pi.setModel() / pi.setThinkingLevel() and that persisting Pi's default* settings is a separate concern. Since #1220 the live switch exists (switchLiveOrchestrator), and a2e1f22 / #1522 fixed the live capture for u. What remains is that applying a profile also writes settings.json (gentle-ai.ts:4238), which changes the starting model of every future session. I propose removing that write entirely (see "Orchestrator" below).

    I agree with @AitorDuran, including the last point: a repository default must not rewrite sessions that are already running. In this proposal each session resolves its profile once, at start, so changing p, P or G only affects sessions started afterwards.

    I agree with @Denver2828, with one nuance. A role the profile does not assign should not fall back to whatever global default Pi has at that moment. Inside a profile, "not assigned" should mean "inherit the parent session's live model and thinking". I would make this the default behavior for sessions with a profile, not an opt-in, because a profile is meant to be a complete snapshot (#916): an omitted role inheriting an unrelated global default is exactly the kind of leak this issue is about. Sessions with no profile at all keep today's behavior.

    I agree with #1240 that the review lens path must be covered. Lens captures read models.json directly (gentle-ai.ts:6759, :7587), so today they ignore even the repository pin.

    Consolidated proposal

    1. Panel actions

    Key Writes Affects
    Enter Session only (a session entry, plus the live orchestrator if the profile carries one) This session
    p <git-common-dir>/gentle-ai/profile-pin.json (unchanged) New sessions in this clone
    P <worktree-root>/.pi/gentle-ai/profile.json (unchanged) New sessions in this repo (team)
    G (new) active in profiles.json New sessions everywhere

    Enter never writes pins, models.json, agent frontmatter, subagents.json or settings.json.

    2. Resolving the profile when a session starts

    flowchart TD
        S[session_start] --> C{Is it a<br/>subagent?}
        C -- yes --> X[Do nothing<br/>the parent passed --model]
        C -- no --> E{Session has a<br/>profile entry?}
        E -- yes --> R[Restore the profile<br/>orchestrator: Pi restores it]
        E -- no --> P[Resolve profile<br/>p → P → G]
        P --> N{Session has<br/>messages?}
        N -- yes --> W[Bind profile<br/>keep orchestrator]
        N -- no --> M{Explicit<br/>--model?}
        M -- yes --> W2[Bind profile<br/>keep CLI model]
        M -- no --> O[Bind profile<br/>apply its orchestrator]
    
    Loading

    Three details that matter for the implementation:

    • reason cannot tell a new session from a resumed one. Pi creates the initial runtime without a sessionStartEvent, so pi --resume, --continue and --session all arrive as reason: "startup", the same as a brand new session (Pi 0.87.1, dist/main.js:681 and core/agent-session.js:166). The decision has to come from the session's content.
    • The subagent guard is required. In a regular gentle-pi install, subagent processes load the package, and gentle-ai's session_start has no guard for them today (gentle-ai.ts:9221). Without that guard, every subagent would replace its --model with the profile's orchestrator.
    • An explicit --model wins. Pi applies the CLI model to every runtime it creates, /new included, so the extension should respect it too.

    /fork needs no special handling: Pi copies every entry on the branch (labels excluded) into the forked session, so the profile entry travels with it. /new resolves again from p / P / G, which is the same thing Pi does with the model itself.

    3. Launching a subagent

    flowchart TD
        L[subagent_run<br/>subagent_continue] --> A{Launch-local<br/>route #1025?}
        A -- yes --> RT[Use the route]
        A -- no --> B{Profile assigns<br/>this role?}
        B -- yes --> PR[Use the<br/>profile entry]
        B -- no --> PL[Inherit the parent's<br/>live model and thinking]
        RT & PR & PL --> Q[Resolve and queue<br/>queued and running keep it]
    
    Loading

    This generalizes what the pin already does at launch (withPinnedModelProfiles, gentle-agents.ts:1011): from "the pin" to "the session's profile". Materialized frontmatter and subagents.json stop being read for sessions with a profile, which removes the one-launch lag and any cross-session effect through the materialized stores. Lens captures resolve through the same session profile. When a session delegates into another repository (workspace_root / repository_root), the session's profile still wins over the target repository's defaults.

    4. Orchestrator (and #1478)

    Enter only switches the live orchestrator. New sessions get the profile's orchestrator in step 2, so writing Pi's defaultModel / defaultThinkingLevel is no longer needed, and G does not write settings.json either. Anyone who wants a Pi-wide default still has the "set as default" option in Pi's own /model.

    @danielgap, this overlaps with point 4 of your #1478 design ("apply side unchanged"). The stored contract you describe (explicit value or leave-untouched) fits this proposal as is: leave-untouched simply means step 2 never touches the orchestrator. The only conflict is the settings.json write on apply. Would you be open to leaving the apply side out of #1478 so it can change here, or to aligning both designs before either PR lands?

    5. Compatibility

    • No file migration: active keeps its shape and now means "global default for new sessions".
    • Sessions with no profile at any layer behave exactly as today.
    • The behavior change is Enter, which becomes session-scoped. It should be called out in the release notes and in docs/readme-reference.md.

    6. Suggested slicing

    1. Fix: Enter must not rewrite the clone pin.
    2. Fix: lens captures honor the pin (and later the session profile).
    3. Fix: serialized, atomic profile writes (the @JeanN00B findings), cross-platform.
    4. Feature: session-bound profile (steps 1 to 3 above).
    5. Feature: orchestrator handling (step 4), coordinated with feat(models): explicit orchestrator model/effort selection in /gentle-models so profiles stop silently overwriting the live session #1478.

    The first three are independent fixes and can land before the design discussion is settled.

    Suggested regression coverage
    • Two parent sessions in the same clone selecting different profiles; each launch uses its own.
    • Enter in a pinned repository leaves the pin untouched.
    • Resume (--resume, --continue, --session) restores the bound profile and does not touch the orchestrator.
    • New session: orchestrator applied from p, P or G; not applied with an explicit --model.
    • Subagents never apply an orchestrator.
    • /fork keeps the profile; /new resolves again from the defaults.
    • A role the profile omits inherits the parent's live model and thinking.
    • Editing the bound profile: the session behaves according to the chosen option (name or snapshot + reload); queued and running tasks keep the previous definition.
    • Deleting the bound profile: the session falls back to the next layer.
    • Lens captures use the session profile.
    • Concurrent G / p / create operations do not lose each other's writes, including rename contention on Windows.

    Happy to help with any of these slices once the direction is agreed.

  7. matraket commented on Sep 29, 2026

    @matraket
    Contributor

    @noxsystems @JeanN00B @daandradec @LCubero @AitorDuran @Denver2828

    I've opened a post in the gentle-shell channel on Discord to discuss this topic more directly. I'd appreciate it if you could stop by when you have a moment.

  8. barbatdev commented on Sep 29, 2026

    @barbatdev
    SponsorContributor

    Approved as an umbrella for session-bound profile selection.

    Please keep this as reviewable slices instead of one large PR. I think the first slice should be the smallest end-to-end path:

    • Enter in /gentle:profiles binds the selected profile to the current parent session.
    • The binding is visible as name (session).
    • Future child launches inherit that parent-session binding.
    • Already queued or running children keep the routing resolved when they were queued/launched.
    • Existing unbound sessions continue resolving p → P → global.
    • No mutation of clone pin (p), repository declaration (P), global routing, materialized stores, agent frontmatter, or Pi default settings.

    Follow-up slices can cover explicit reload/edit/delete semantics, orchestrator/default handling, lens/reviewer routing if needed, and concurrent store-write hardening.

    Out of scope for the first PR: global locking/transactions, Windows rename retry hardening, and broader profile-store serialization.

  9. barbatdev commented on Sep 29, 2026

    @barbatdev
    SponsorContributor

    Approved note above still stands. One extra clarification after checking current main: Usage now follows the effective profile to decide which providers to refresh/show.

    That means the session-bound layer should plug into that same effective-profile resolution, not only into subagent launch routing. Otherwise children could launch with a session profile while Usage still reports providers from the local/repo/global profile.

    So for the first slice, please treat the shared precedence as:

    session → local pin (p) → repository declaration (P) → global

    and use that effective source for subagent launch routing, header/status display (name (session)), and Usage provider scope.

  10. danielgap commented on Sep 29, 2026

    @danielgap
    Contributor

    Taking the first slice as scoped above:

    • Enter in /gentle:profiles binds the selected profile to the current parent session, shown as name (session).
    • Child launches inherit the parent-session binding; queued/running children keep their resolved routing.
    • Unbound sessions keep p → P → global; no mutation of pin, declaration, global routing, materialized stores, frontmatter, or Pi defaults.
    • Effective resolution follows session → p → P → global for launch routing, status display, and Usage provider scope per the clarification above.

    I will keep it test-first and post the slice plan here before opening the PR.

  11. danielgap commented on Sep 29, 2026

    @danielgap
    Contributor

    Slice plan for the first PR, mapped to the approved scope:

    Shared state — new lib/session-profile-binding.ts: an in-process map keyed by parent session id holding { name, modelProfiles } (the normalized routing snapshot taken from the saved profile at bind time, defensively copied on read/write). Concurrent Pi processes are isolated by construction; resume/restore and reload semantics stay for follow-up slices.

    1. Enter binds the session (/gentle:profiles): Enter stores the session binding for the selected profile under the current parent session id. It writes nothing: no store active marker, no models.json, no materialized stores, no agent frontmatter, no Pi settings, and no pin or declaration changes, pin present or not. The current global-apply behavior (including its pin-aware repo-scoped re-pin) moves to a new explicit a key ("set as global default"), unchanged in semantics.

    2. Launch routing (gentle-agents): the launch resolver becomes session → p (clone pin) → P (repo declaration) → global. A present session binding replaces modelProfiles wholesale for launches from that parent session, exactly like a winning pin does today. Queued and running children keep the routing resolved when their task request was created.

    3. Display: the shell bar and the profiles panel show the effective profile as name (session) when a binding is present, ahead of pin (source) and the global active name.

    4. Usage provider scope: usage refresh covers the providers named by the session binding's routing (distinct provider/ prefixes of its model ids) alongside the live provider, so the panel meters what the session actually launches with.

    Tests, written first: binding store (isolation per session, snapshot immutability); launch precedence session > pin > global and queued-request retention; reader precedence and the (session) label; panel Enter = session bind with zero file writes (spied), a = legacy global apply; existing unbound behavior untouched.

    Out of scope per the approval note: reload/edit/delete binding, orchestrator/default handling, lens/reviewer routing, persistence across resume, store-write hardening.

  12. noxsystems commented on Sep 29, 2026

    @noxsystems
    Author

    Thanks for taking the first slice, @danielgap. I won't open an overlapping PR. I can review and test it when it's ready, especially two parent sessions in the same clone, zero shared or pin writes on Enter, queued-task stability, and agreement between child routing, status, and Usage on the effective profile.

    I have a local prototype with persisted session snapshots and resume/fork tests. Its UI keys and per-field fallback differ from the approved first slice, so I would adapt the relevant pieces rather than cherry-pick it. After the first slice lands, would a separate, narrowly scoped persistence/resume follow-up be useful? Happy to coordinate its contract before starting.

  13. danielgap commented on Sep 29, 2026

    @danielgap
    Contributor

    Thanks @noxsystems, the chain is ready for you:

    On your four scenarios, so you know what is already pinned by tests versus what would benefit most from your live testing:

    • Zero shared or pin writes on Enter: asserted in feat(profiles): bind the active profile to the parent session (#1064 1/2) #1557's tests (store, Pi settings, models.json, materialized subagents.json, and both pin layers stay byte-identical, including with a winning pin present).
    • Queued-task stability: the binding is resolved at task-request creation, so a queued child keeps the routing frozen into its request; the precedence itself is unit-tested, but a live queue check is welcome.
    • Two parent sessions in the same clone: this one is manual by nature (two live processes), and it is the scenario unit tests cannot cover. Two windows, bind different profiles in each, launch children from both, and watch that neither session's status, launches, or usage drift.
    • Agreement between child routing, status, and Usage: each consumer is tested through the same store, but cross-surface agreement in a live session is exactly the end-to-end check worth doing.

    On the persistence/resume follow-up: yes, that would be useful and it is the natural next slice. The store is deliberately in-process for slice 1 (lib/session-profile-binding.ts), and everything reads it through one seam, so persisted snapshots could plug into the same read path without touching the three consumers again. Coordinating the contract first is the right call, and it should probably line up with barbatdev's follow-up list (reload/edit/delete semantics in particular, since persistence and reload rules shape each other). Happy to sync on the contract whenever you start.

  14. danielgap commented on Sep 29, 2026

    @danielgap
    Contributor

    Recording two test-debt items from the native review of slice 2 (#1558, lineage review-0cb9827ae718d863, both informational advisories) so they stay visible for the follow-up slices:

    1. Routing-freeze for queued children — no dedicated test proves that a child queued under one session binding keeps its frozen routing after the session rebinds (the launch seam resolves routing once at task-request creation; only the pure precedence helper is tested today).
    2. Session-over-pin precedence at launch — no test with both a winning pin and a session binding present, proving the session wins at the actual launch and reader seams (each layer is tested alone).

    Plan: cover both in the next slice that touches this area, or a small dedicated test: PR under this umbrella if it lands first.

  15. AlejandroTatum commented on Sep 30, 2026

    @AlejandroTatum

    Adding a real-world data point for this request.

    I work on several repositories in parallel, each in its own Pi session, and each wants a different routing profile: one on a Claude-only profile, another on a GPT/GLM profile, everything else on the global default. Today that takes three separate mechanisms, each covering only part of it:

    When any of the three drifts, the session quietly runs on the global profile. The only way I found to notice was reading the subagent session JSONL files. It happened often enough that I stopped trusting what I thought each session was running.

    What would remove the friction: the profile chosen when a session starts stays bound to that session — for the orchestrator and every subagent it launches — regardless of what another session activates later. Happy to test the session-binding work (#1557/#1558) against this setup.

  16. 65 remaining items

  17. zhalker commented on Oct 13, 2026

    @zhalker

    Field report from 4.1.0 that matches this invariant and, I think, adds two observability gaps worth tracking while the persistence slices (#1991–#1993) land. Happy to move this elsewhere if a narrower issue fits better.

    What happened. A role's model was changed on disk in every editable layer — ~/.pi/agent/subagents.json model_profiles, the agent definition frontmatter in ~/.pi/agent/agents/<role>.md, plus the agents panel — while the session had a /gentle:profiles binding whose snapshot still contained the old model. Result: every fresh subagent_run relaunch of that role resolved the old model from the binding. The old model's provider quota was exhausted, so four consecutive relaunches failed identically with "You've hit your session limit · resets 12:50pm" before the cause was found.

    What rules out the disk layers. Verified during diagnosis:

    • Both on-disk files had the new model (checked by mtime and content); no project-level .pi/subagents.json; no pin (pin: none in the panel).
    • Running lib/agents-config.ts (loadAgentsConfig → resolveAgentProfile) cold, outside the session process, against the real files returned the new model with source: profile — so the on-disk resolution path was already correct while launches used the old model.
    • The spawned children's environment had no GENTLE_PI_AGENT_HOME / PI_CODING_AGENT_DIR — same agent home, not an isolation artifact.
    • Task records reflect the child's actual model (get_state), so the four failures are solid evidence, not launch-args guesswork.

    Observation 1 — the binding outranks layers that the operator can still edit, silently. This is the documented invariant ("editing a saved profile definition does not silently alter an already-bound session"), but it also covers edits made below the binding (subagents.json, frontmatter). The operator's edits genuinely no-op, and nothing at launch time says so.

    Observation 2 — the panel and the launches can disagree. After the operator re-bound the corrected profile via Enter, two further fresh relaunches still resolved the stale model; only after closing and re-entering pi did launches resolve the new binding (next relaunch: correct model, verified from the child session's model_change header). Meanwhile /gentle:profiles showed the corrected routing under "Current routing (effective)". If Enter is supposed to replace the in-process binding immediately (per #1558, the binding is read at task-request creation), either the re-bind didn't propagate to the launch path before the restart, or the effective-routing display and the launch resolution read different snapshots. I can't fully reconstruct the ordering from session files, but the combination (re-bind → 2 stale launches → restart → correct launches) looks reproducible.

    Cheap fix that would have saved an hour: record the model source (binding | pin | profile | definition | default | inherited) next to model in the TaskRecord — resolveAgentProfile already computes it. Also consider a launch-time notice when the winning layer disagrees with subagents.json/frontmatter for the launched role.

    (Also noting in passing: the bulk rewrite of all agent .md files observed during this incident matches the session-start materialization described in #600 — worth keeping in mind for how hand-edits interact with the binding.)

  18. added 2 commits that reference this issue on Oct 13, 2026
  19. matraket commented on Oct 13, 2026

    @matraket
    Contributor

    Thanks @zhalker, a very useful report, and the cold-path check against loadAgentsConfig is exactly the evidence that separates the binding from the disk layers.

    Observation 1 is the agreed contract: a session's profile outranks subagents.json and frontmatter. It will also cover more cases soon. Once #1827 lands, a session that never pressed Enter freezes p → P → G at startup, so it will ignore those edits too. Its drift notice only fires when p, P or the global profile changes, not on edits to subagents.json or frontmatter. So I agree the silence is the real gap.

    Recording the model source in the TaskRecord, plus a launch notice when the winning layer differs from the role's on-disk routing, would close it for every launch, not only bound ones. It touches launch observability beyond this umbrella, so I'd suggest its own issue. Happy to help with it.

    Observation 2 looks like a real bug if it reproduces. For context, 4.1.0 includes #1825/#1826 but not #1827, so the startup freeze was not active in your session: the launches resolved the explicit Enter binding from #1558. @danielgap @noxsystems you know the Enter path best, and #1992 is wiring persistence into it right now. One thing that may help narrow it: the panel's "Current routing (effective)" table and the launch resolve the binding through different code paths today (extensions/gentle-ai.ts), so the display matching the new profile does not prove the launch saw it. @zhalker if you can share the exact sequence (was the session resumed or reloaded before the re-bind? were the two stale launches subagent_run or subagent_continue?), that would make it reproducible.

  20. matraket commented on Oct 13, 2026

    @matraket
    Contributor

    Update on ownership after syncing with @barbatdev: the maintainers are taking Observation 1 (recording the model source in the TaskRecord), so no separate issue is needed from your side, @zhalker. I'm taking the reproduction of Observation 2 and will post findings here. If it confirms, it becomes a bug with its own issue and fix. @danielgap @noxsystems no action needed from you for now; I'll ping you if the repro points at the Enter path.

  21. zhalker commented on Oct 13, 2026

    @zhalker

    23. Thanks @zhalker, a very useful report, and the cold-path check against loadAgentsConfig is exactly the evidence that separates the binding from the disk layers.
    Observation 1 is the agreed contract: a session's profile outranks subagents.json and frontmatter. It will also cover more cases soon. Once #1827 lands, a session that never pressed Enter freezes p → P → G at startup, so it will ignore those edits too. Its drift notice only fires when p, P or the global profile changes, not on edits to subagents.json or frontmatter. So I agree the silence is the real gap.
    Recording the model source in the TaskRecord, plus a launch notice when the winning layer differs from the role's on-disk routing, would close it for every launch, not only bound ones. It touches launch observability beyond this umbrella, so I'd suggest its own issue. Happy to help with it.
    Observation 2 looks like a real bug if it reproduces. For context, 4.1.0 includes #1825/#1826 but not #1827, so the startup freeze was not active in your session: the launches resolved the explicit Enter binding from #1558. @danielgap @noxsystems you know the Enter path best, and #1992 is wiring persistence into it right now. One thing that may help narrow it: the panel's "Current routing (effective)" table and the launch resolve the binding through different code paths today (extensions/gentle-ai.ts), so the display matching the new profile does not prove the launch saw it. @zhalker if you can share the exact sequence (was the session resumed or reloaded before the re-bind? were the two stale launches subagent_run or subagent_continue?), that would make it reproducible.


    Thanks for the pointers — digging into them answered the reproduction question, and the answer turns out to involve a version mix I did not have on my radar. Summary up front: the incident ran on 4.0.0; the controlled repro in my previous message ran on 4.1.0 — and 4.0.0 has no session-binding layer at all, so the incident went through the pin/apply mechanics instead.

    Version fact (verified)

    • The failing launches ran in a process started Oct 9. A subagent running in that process on Oct 13 morning read node_modules/gentle-pi/package.json as 4.0.0; the on-disk upgrade to 4.1.0 landed that day (~11:29 — the managed agent assets were bulk-rewritten then), but the old process kept running its 4.0.0 code until it was closed. The first launch after the restart (12:11) ran 4.1.0 and resolved correctly.
    • Checked 4.0.0's shipped code: lib/session-profile-binding.ts does not exist there, and the subagent_run request builder resolves pin → subagents.json → agent frontmatter → default with fresh disk reads — no session-binding layer. So feat(profiles): bind the active profile to the parent session (#1064 1/2) #1557/feat(profiles): resolve the session binding in launches, status, and usage (#1064 2/2) #1558 semantics are 4.1.0-only on my install; my repro conclusions only cover 4.1.0.

    What the incident actually was (4.0.0 mechanics + operator recollection)

    Reconstructing with the operator (their recollection, cross-checked against file mtimes and the 4.0.0 shipped code):

    1. The session ran for days with the judge role resolving to an exhausted provider. That day started with switching profiles gpt → claude (gpt quota hit 100%). Under 4.0.0's apply semantics, when a pin layer resolves for the repository, apply does not write routing at all — it re-pins the clone to the selected profile ("Subagent launches here keep resolving the pin; no global routing or orchestrator was written"). Subsequent launches resolved through that pin.
    2. Switching to the NaN profile re-pinned the clone to it — but its stored assignments still carried the judge role → the exhausted provider ("the NaN profile also seemed to have claude for judge A and some other subagents" — leftover from when the role was originally configured). Launches kept resolving the exhausted provider: correct per the pin's store, invisible to the operator.
    3. The operator then edited the assignments, ran a (global default) and Enter, and retried — still the exhausted provider. Whether those edits landed in the store the pin resolves, in the saved routing the panel edits, or in the materialized subagents.json/frontmatter (which were already correct on disk by 11:25–11:29 per mtimes) is exactly the ambiguity that could not be resolved from artifacts: three plausibly-writable stores, one launch-time winner, and no artifact saying which won. Under 4.0.0 semantics, with fresh disk reads at launch, the only layer that could override the already-correct subagents.json/frontmatter was the pin.
    4. After the restart on 4.1.0, the next launch resolved the intended model — consistent with the pin pointing at a corrected store (or having been cleared) by then, and with no session binding involved (the 4.0.0 process's Enter could not have created one).

    Controlled repro (from my previous message, now correctly scoped to 4.1.0)

    First bind and re-bind over an existing binding both propagate to subagent_run resolution instantly on 4.1.0, and the binding outranks the global layer. Observation 2 as a 4.1.0 propagation bug: not reproduced, and now moot — the stale launches predate the upgrade, so they never exercised the binding path at all.

    What I'd still ask for (unchanged, strengthened)

    The incident spanned two package versions and at least four routing layers (pin, session binding, subagents.json/frontmatter, saved routing), and at no point did any artifact say which layer won a given launch — the TaskRecord carries the resolved model but not its source, and no panel names the winner either. Recording binding | pin | profile | definition | default | inherited in the TaskRecord (the data already exists in resolveAgentProfile's source), plus an optional launch notice when the winning layer disagrees with the role's on-disk routing, would have made this diagnosable in minutes across both versions. Happy to open that as its own issue as suggested — and if the 4.0.0-era apply-repins-clone behavior above is itself surprising to you, that might deserve its own look.

  22. matraket commented on Oct 13, 2026

    @matraket
    Contributor

    Independent confirmation of Observation 2, matching @zhalker's conclusion: no propagation bug in 4.1.0.

    I first reproduced it against the 4.1.0 source with the test fixtures (real panel, Enter and launch code, simulated UI and session). That showed the mechanism below, but a fixture cannot exercise extension loading in a live process or the panel as a user sees it, so I repeated it on a real, clean install.

    Setup: clean Ubuntu 24.04, Node 22.23, Pi 1.1.0, pi install npm:gentle-pi@4.1.0 (gitHead 79323b76), NaN provider, gentle-ai-explore as the role, models checked from each child session's first model_change.

    Launch Session state Child model
    1 bound to repro (old model) old
    2 still bound; subagents.json + frontmatter edited to new old
    3 after re-Enter on repro old
    4 after restart, unbound new

    Re-Enter takes effect immediately; it re-copies the profile definition from profiles.json, which still held the old model. Edits to subagents.json/frontmatter never reach a bound session, so a re-bind after editing those layers keeps the old model, and only a restart (which drops the in-memory binding) picks them up. The panel was consistent with launches throughout: bound, "Current routing (effective)" showed the old model; unbound, the new one.

    So nothing to file as a bug. That trap is exactly what the source token from #2184 now makes visible on the task card.

  23. danielgap commented on Oct 13, 2026

    @danielgap
    Contributor

    @zhalker thanks for scoping the version mix, and @matraket for the clean-install confirmation.

    One piece from your step 1 is still current on main: when a pin resolves for the repository, a re-pins the clone and writes no global routing, no materialized stores, and no orchestrator settings (extensions/gentle-ai.ts, apply-global case). That is intentional, but docs/readme-reference.md still says a writes models.json and reconciles frontmatter/subagents.json with no pin caveat. #1926 fixes that paragraph, and your incident is a good real-world case of the confusion it prevents.

  24. barbatdev commented on Oct 13, 2026

    @barbatdev
    SponsorContributor

    Thanks @zhalker, closing the loop on your report. @matraket reproduced it on a clean install: no propagation bug — re-Enter immediately re-copies the profile definition from profiles.json, which still held the old model; the edits to subagents.json and frontmatter live below the binding and never reach a bound session, exactly the documented invariant. Your diagnosis path (cold loadAgentsConfig against disk) is what made the separation provable.

    Two things came out of it: the observability gap you flagged is shipped (#2184, task records now carry the winning layer token, so binding shows on the card), and the remaining surprise is documentation, not code: nothing states that re-binding re-snapshots from profiles.json rather than the materialized layers. That line is going into the reference docs.

  25. self-assigned this
    on Oct 16, 2026
  26. barbatdev commented on Oct 16, 2026

    @barbatdev
    SponsorContributor

    Thanks @zhalker and @matraket — your source audit and clean-install reproduction made the separation provable: no propagation bug in 4.1.0, just an undocumented invariant. Re-selecting re-copies the profile definition from profiles.json, so later edits to subagents.json or agent frontmatter never reach a bound session, in that session or through a re-selection.

    That documentation gap is now closed in 4709797 (#2187): the paragraph above the a/Enter semantics in docs/readme-reference.md states it explicitly.

    The session-bound profile request itself stays open here — thanks @daandradec, @LCubero, @Denver2828, and @AitorDuran for the concrete workflows; they're exactly the invariants slice 1 covers and the use cases we're tracking for the remaining slices.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or requeststatus:approvedIssue approved by maintainer; PR may be openedtype:featureNew feature

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions