Repository navigation
feat(profiles): support session-bound active profiles without global routing mutation #1064
Description
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:
pwrites 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.Pwrites 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
player takes precedence overP, 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
JeanN00B commentedon Sep 16, 2026
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.jsonand Pi orchestrator settings use per-file temp-and-rename writes, whilemodels.jsonis written directly. Applying a profile spans the profile store,models.json, materializedsubagents.jsonor 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.ppidoes not solve this by itself because it only setsPI_CODING_AGENT_DIR; Gentle'sGENTLE_PI_CONFIG_HOMEremains 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_HOMEvalues 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.daandradec commentedon Sep 16, 2026
+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
pisessions 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:
What happens today
The
activemarker 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:modelsin project A had moved to DeepSeek as well.The per-repository pin shipped in #1074 (
pon 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
@earendil-works/pi-coding-agent)v2.5.0-179-g8ec0f98b(package.json 2.5.0)No new mechanism to propose — the precedence model in the description already handles this case. Happy to test a branch once session binding lands.
LCubero commentedon Sep 17, 2026
Confirmed a Windows reproduction that adds one specific failure mode to this request:
/gentle:profilespersists the profile'sorchestratorentry to Pi's globaldefaultProvider,defaultModel, anddefaultThinkingLevel, 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 bothsettings.jsonand the active profile. Also,ssnapshots the orchestrator throughreadOrchestratorSettings(...), which captures the global defaults rather thanctx.modelandctx.thinkingLevelfrom 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()andpi.setThinkingLevel()). Persistingdefault*should remain the separate global-default action.AitorDuran commentedon Sep 27, 2026
A concrete parallel-work scenario for this request:
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.
Denver2828 commentedon Sep 27, 2026
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 recordedopenai-codex/gpt-6-astrawithlowthinking before dispatch, but an unpinnedgentle-ai-verifylaunched withopenai-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:
resolveAgentProfileresolves profile, then agent definition, then configured defaults. It does not take the parent's live model as an input.childArgumentsomits--modelwhen unresolved, leaving the child to use Pi startup defaults.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
/modelselection 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.
matraket commentedon Sep 29, 2026
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, thenP, 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
pandPkeys 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 newGkey sets the global default, next top(clone) andP(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 ofprofiles.json), a lock, atomic writes formodels.json, and rename retries on Windows (EPERM/EBUSY).I agree with @daandradec that
pis 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'sdefault*settings is a separate concern. Since #1220 the live switch exists (switchLiveOrchestrator), and a2e1f22 / #1522 fixed the live capture foru. What remains is that applying a profile also writessettings.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,PorGonly 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.jsondirectly (gentle-ai.ts:6759,:7587), so today they ignore even the repository pin.Consolidated proposal
1. Panel actions
p<git-common-dir>/gentle-ai/profile-pin.json(unchanged)P<worktree-root>/.pi/gentle-ai/profile.json(unchanged)G(new)activeinprofiles.jsonEnter never writes pins,
models.json, agent frontmatter,subagents.jsonorsettings.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]Three details that matter for the implementation:
reasoncannot tell a new session from a resumed one. Pi creates the initial runtime without asessionStartEvent, sopi --resume,--continueand--sessionall arrive asreason: "startup", the same as a brand new session (Pi 0.87.1,dist/main.js:681andcore/agent-session.js:166). The decision has to come from the session's content.gentle-ai'ssession_starthas no guard for them today (gentle-ai.ts:9221). Without that guard, every subagent would replace its--modelwith the profile's orchestrator.--modelwins. Pi applies the CLI model to every runtime it creates,/newincluded, so the extension should respect it too./forkneeds no special handling: Pi copies every entry on the branch (labels excluded) into the forked session, so the profile entry travels with it./newresolves again fromp/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]This generalizes what the pin already does at launch (
withPinnedModelProfiles,gentle-agents.ts:1011): from "the pin" to "the session's profile". Materialized frontmatter andsubagents.jsonstop 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/defaultThinkingLevelis no longer needed, andGdoes not writesettings.jsoneither. 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.jsonwrite 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
activekeeps its shape and now means "global default for new sessions".docs/readme-reference.md.6. Suggested slicing
The first three are independent fixes and can land before the design discussion is settled.
Suggested regression coverage
--resume,--continue,--session) restores the bound profile and does not touch the orchestrator.p,PorG; not applied with an explicit--model./forkkeeps the profile;/newresolves again from the defaults.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.
matraket commentedon Sep 29, 2026
@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.
barbatdev commentedon Sep 29, 2026
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:
Enterin/gentle:profilesbinds the selected profile to the current parent session.name (session).p → P → global.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.
barbatdev commentedon Sep 29, 2026
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) → globaland use that effective source for subagent launch routing, header/status display (
name (session)), and Usage provider scope.danielgap commentedon Sep 29, 2026
Taking the first slice as scoped above:
Enterin/gentle:profilesbinds the selected profile to the current parent session, shown asname (session).p → P → global; no mutation of pin, declaration, global routing, materialized stores, frontmatter, or Pi defaults.session → p → P → globalfor 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.
danielgap commentedon Sep 29, 2026
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.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, nomodels.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 explicitakey ("set as global default"), unchanged in semantics.Launch routing (
gentle-agents): the launch resolver becomessession → p (clone pin) → P (repo declaration) → global. A present session binding replacesmodelProfileswholesale 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.Display: the shell bar and the profiles panel show the effective profile as
name (session)when a binding is present, ahead ofpin (source)and the global active name.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.
noxsystems commentedon Sep 29, 2026
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.
danielgap commentedon Sep 29, 2026
Thanks @noxsystems, the chain is ready for you:
name (session), the legacy global apply moves toa. All checks green, CodeRabbit clean.session → p → P → global, the shell status label, and the usage provider scope.On your four scenarios, so you know what is already pinned by tests versus what would benefit most from your live testing:
models.json, materializedsubagents.json, and both pin layers stay byte-identical, including with a winning pin present).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.danielgap commentedon Sep 29, 2026
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:
Plan: cover both in the next slice that touches this area, or a small dedicated
test:PR under this umbrella if it lands first.AlejandroTatum commentedon Sep 30, 2026
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:
.pi/settings.jsonfor the orchestrator, since the pin never moves it (feat(models): explicit orchestrator model/effort selection in /gentle-models so profiles stop silently overwriting the live session #1478),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.
65 remaining items
feat(profiles): add session profile v1 record decoder
zhalker commentedon Oct 13, 2026
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.jsonmodel_profiles, the agent definition frontmatter in~/.pi/agent/agents/<role>.md, plus the agents panel — while the session had a/gentle:profilesbinding whose snapshot still contained the old model. Result: every freshsubagent_runrelaunch 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:
.pi/subagents.json; no pin (pin: nonein the panel).lib/agents-config.ts(loadAgentsConfig→resolveAgentProfile) cold, outside the session process, against the real files returned the new model withsource: profile— so the on-disk resolution path was already correct while launches used the old model.GENTLE_PI_AGENT_HOME/PI_CODING_AGENT_DIR— same agent home, not an isolation artifact.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_changeheader). Meanwhile/gentle:profilesshowed 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 tomodelin the TaskRecord —resolveAgentProfilealready computes it. Also consider a launch-time notice when the winning layer disagrees withsubagents.json/frontmatter for the launched role.(Also noting in passing: the bulk rewrite of all agent
.mdfiles observed during this incident matches the session-start materialization described in #600 — worth keeping in mind for how hand-edits interact with the binding.)Merge pull request #1827 from matraket/feat/1064-3b-ii-3-consumers
Merge pull request #2007 from matraket/feat/1064-3b-ii-4-followups
matraket commentedon Oct 13, 2026
Thanks @zhalker, a very useful report, and the cold-path check against
loadAgentsConfigis exactly the evidence that separates the binding from the disk layers.Observation 1 is the agreed contract: a session's profile outranks
subagents.jsonand frontmatter. It will also cover more cases soon. Once #1827 lands, a session that never pressed Enter freezesp → P → Gat startup, so it will ignore those edits too. Its drift notice only fires whenp,Por the global profile changes, not on edits tosubagents.jsonor 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 launchessubagent_runorsubagent_continue?), that would make it reproducible.matraket commentedon Oct 13, 2026
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.
zhalker commentedon Oct 13, 2026
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)
node_modules/gentle-pi/package.jsonas 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.lib/session-profile-binding.tsdoes not exist there, and thesubagent_runrequest builder resolvespin → subagents.json → agent frontmatter → defaultwith 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):
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 materializedsubagents.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-correctsubagents.json/frontmatter was the pin.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_runresolution 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. Recordingbinding | pin | profile | definition | default | inheritedin the TaskRecord (the data already exists inresolveAgentProfile'ssource), 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.docs(profiles): scope `a` apply global writes to unpinned repositories (
matraket commentedon Oct 13, 2026
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(gitHead79323b76), NaN provider,gentle-ai-exploreas the role, models checked from each child session's firstmodel_change.repro(old model)subagents.json+ frontmatter edited to newreproRe-Enter takes effect immediately; it re-copies the profile definition from
profiles.json, which still held the old model. Edits tosubagents.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.
danielgap commentedon Oct 13, 2026
@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,
are-pins the clone and writes no global routing, no materialized stores, and no orchestrator settings (extensions/gentle-ai.ts,apply-globalcase). That is intentional, butdocs/readme-reference.mdstill saysawritesmodels.jsonand reconciles frontmatter/subagents.jsonwith no pin caveat. #1926 fixes that paragraph, and your incident is a good real-world case of the confusion it prevents.barbatdev commentedon Oct 13, 2026
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 tosubagents.jsonand frontmatter live below the binding and never reach a bound session, exactly the documented invariant. Your diagnosis path (coldloadAgentsConfigagainst 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
bindingshows on the card), and the remaining surprise is documentation, not code: nothing states that re-binding re-snapshots fromprofiles.jsonrather than the materialized layers. That line is going into the reference docs.feat(profiles): tell a bound session its profile definition changed (G…
docs(profiles): state that re-selection re-copies the profile definit…
barbatdev commentedon Oct 16, 2026
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 tosubagents.jsonor 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 indocs/readme-reference.mdstates 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.