Apply session _meta once per adapter update and keep it transactional - #327675
Merged
Conversation
ulugbekna
changed the base branch from
main
to
ulugbekna/automation-session-grouping
July 27, 2026 18:18
Contributor
There was a problem hiding this comment.
Pull request overview
Makes agent-host session metadata updates transactional and avoids duplicate _meta application.
Changes:
- Threads transactions through
setMeta. - Adds atomic refresh and protocol metadata tests.
- Corrects workspace-less persistence key documentation.
Show a summary per file
| File | Description |
|---|---|
baseAgentHostSessionsProvider.ts |
Updates transactional metadata handling. |
localAgentHostSessionsProvider.test.ts |
Tests atomic refresh publication. |
protocolServerHandler.test.ts |
Tests absent _meta serialization. |
AGENT_HOST_SESSIONS_PROVIDER.md |
Corrects the persistence key. |
.github/skills/sessions/SKILL.md |
Corrects session guidance. |
Review details
- Files reviewed: 5/5 changed files
- Comments generated: 2
- Review effort level: Medium
roblourens
previously approved these changes
Jul 27, 2026
…sactional Follow-up to "agents window: keep workspace-less quick chats out of bogus workspace groups". That change made `_meta` arrive on every `listSessions` refresh, which turns two pre-existing quirks in `AgentHostSessionAdapter.update` into per-session, per-turn-completion work: - `_meta` was applied twice: once inline and again through the trailing `setMeta`. With the promotion logic now living in `setMeta`, that also ran `_promoteToQuickChatIfWorkspaceless` and `_computeWorkspace` twice. Apply it once, up front, where the promotion still precedes the workspace rebuild. - `setMeta` opened its own `transaction()`. `transaction()` never joins an enclosing one - it constructs a `TransactionImpl` and `finish()`es it - so moving the single application to the top of `update` would notify observers of `_meta` / `isQuickChat` / `workspace` before `isArchived`, `isRead`, `changes` and `activity` were applied, handing them a torn snapshot. Take an optional `ITransaction` and use `subtransaction`, and thread the caller's transaction from `update` and `_handleSessionSummaryChanged`. The two standalone callers pass nothing and keep their own transaction. Also pins the absent-`_meta` case on the wire (the item is built field by field and `satisfies SessionSummary` cannot catch a dropped optional in either direction), and corrects the session-database key named in the docs: the marker is persisted as `agentHost.workspaceless` (`AH_META_WORKSPACELESS_DB_KEY`, written by `AgentService`), never `copilot.workspaceless`. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8f4788b1-3307-4b88-b3fc-23565548de06
ulugbekna
force-pushed
the
ulugbekna/quick-chat-meta-followup
branch
from
July 28, 2026 09:23
5bb20ed to
ce3e04a
Compare
ulugbekna
requested review from
TylerLeonhardt,
Yoyokrazy,
alexdima,
lszomoru,
rzhao271 and
sandy081
as code owners
July 28, 2026 09:23
ulugbekna
changed the base branch from
ulugbekna/automation-session-grouping
to
main
July 28, 2026 09:23
…ansaction threading
Addresses review feedback on this PR.
- `update()` still recomputed the workspace unconditionally after `setMeta`
had already rebuilt it from the same project / working directories and the
same `_meta`, so a `_meta`-bearing refresh paid for `_computeWorkspace()`
twice - exactly the duplicate per-turn work this PR set out to remove. The
fallback rebuild now only runs when the snapshot carries no `_meta`.
- `setChangesSummary` and `setActivity` wrote their observables with no
transaction, and `observableValue.set(v, undefined)` builds and finishes one
of its own, so they notified mid-way through the enclosing update. Both now
take an optional `ITransaction`, and `update()` /
`_handleSessionSummaryChanged` thread theirs through. Without this, threading
the transaction into `setMeta` alone left `SessionSummaryChanged` still torn:
`changes` is applied before `_meta`, so an observer of both ran once on the
new chip with the stale workspace, then again at the outer finish.
The new test covers that notification path and fails with the intermediate
`{ branch: undefined, files: 2 }` snapshot when the transaction is dropped.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8f4788b1-3307-4b88-b3fc-23565548de06
ulugbekna
enabled auto-merge (squash)
July 28, 2026 10:15
TylerLeonhardt
approved these changes
Jul 28, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #327629 ("quick chats: fix: show within Chats section instead of random UUID dirs"), now merged. Rebased onto main; this PR is a single commit against
main.Scope, stated plainly: the behavioural fix for the quick-chat grouping bug shipped in #327629. This one is an optimization, its prerequisite, and two hygiene items. Sizing it that way up front so it can be triaged accordingly — or trimmed, see "If you want to trim" below.
#327629 made
_metaarrive on everylistSessionsrefresh._refreshSessions()fires on eachChatTurnCompleteand callsupdate()for every listed session (the cache caps at 100), so anything redundant inAgentHostSessionAdapter.update()is now per-session, per-turn work.1. Apply
_metaonce per update — optimizationupdate()applied_metatwice: once inline, then again through the trailingsetMeta. With promotion now living insidesetMeta, the duplicate also re-ran_promoteToQuickChatIfWorkspacelessand_computeWorkspace()— the latter allocating a workspace object and doing aconfigurationService.getValue('git.branchProtection', { resource })lookup — plus a secondagentHostSessionWorkspaceKey()(an 11-field string join).Applied once now, up front, where the promotion still precedes the workspace rebuild. Halves that work. Real but modest — not something you'd notice in a profile.
2.
setMetajoins the caller's transaction — prerequisite + hardening, not a live bug fixBeing straight about this one: on
maintoday there is no tearing.setMetais the last write in both transactional callers (update()and_handleSessionSummaryChanged), so its nestedtransaction()flushes after everything else is applied.It's needed because of change 1.
transaction()never joins an enclosing one — it constructs aTransactionImplandfinish()es it — so collapsing to a single application at the top ofupdate()is exactly what opens the window: observers of_meta/isQuickChat/workspacewould be notified beforeisArchived,isRead,changesandactivityland, handing them a torn snapshot.setMetanow takes an optionalITransactionand usessubtransaction;update()and_handleSessionSummaryChangedthread theirs through. The two standalone callers (_applySessionMetaFromState,_handleSessionMetaChanged) pass nothing and keep their own transaction, so their behaviour is unchanged.Beyond unblocking change 1, this removes an ordering landmine: today the invariant "
setMetamust be the last write" is implicit and unenforced, so adding any field write after it silently reintroduces torn notifications.3. Doc key correction + negative wire test — the part I'd argue actually matters
copilot.workspacelessappears nowhere in the code. The marker is persisted asagentHost.workspaceless(AH_META_WORKSPACELESS_DB_KEY), written centrally byAgentService; the agents only read it. The wrong key was named inAGENT_HOST_SESSIONS_PROVIDER.mdand in.github/skills/sessions/SKILL.md— the latter is read by agents, so it keeps propagating._metacase on the wire. The item is built field by field andsatisfies SessionSummarycatches a dropped optional in neither direction — which is exactly how this bug shipped twice.If you want to trim
The honest minimum is section 3. Sections 1 and 2 travel together (2 exists to make 1 safe) and can be dropped without affecting anything user-visible.
Testing
a refresh publishes _meta and summary fields as one atomic update— asserts an autorun observingworkspace+isArchivedsees exactly two snapshots across a refresh that moves both. Re-verified after the rebase that it fails with a torn{ feature, false }middle snapshot whensubtransactionis reverted totransaction.listSessions omits _meta when the agent provides noneat the protocol layer.npm run typecheck-clientclean, eslint clean, 532 tests passing across the local/remote provider, protocol server/client, agent service and sessions-list suites.Review provenance
#327629 and an independent branch of mine were produced by two separate sessions that reached the same root-cause diagnosis and the same wire fix, then diverged on how to heal already-mis-classified sessions. Rather than merge them, I kept #327629's approach and reduced mine to the deltas above.
workspaceflipping toundefinedmid-session andsupportsMultipleChatscollapsing chat tabs — and recommended dropping the persisted cache via a storage-key bump instead. Keep workspace-less quick chats out of bogus workspace groups #327629 shipped the mutable-kind approach anyway, with a one-way promotion guard, and it is the better call: it heals in place with no cold-start cost and covers the remote provider for free. My cache-key bump and_metaByRawIdguard were dropped as redundant.update()rewrite is equivalent to the original indidChangepropagation and notification timing, and that the preserved metadata is contained to_persistCache. A third flagged the remote provider's cache key and withdrew after the offline-list tradeoff was explained.