fix(git): keep shared-environment status authoritative (#3173)
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
# ADR-2026-08-30-environment-owned-git-status: Project Current Git Status From the Task Environment
|
||||
|
||||
**Status:** proposed
|
||||
**Date:** 2026-08-30
|
||||
**Area:** backend, frontend, protocol
|
||||
|
||||
## Context
|
||||
|
||||
Multiple task sessions share one `TaskEnvironment` and one physical workspace.
|
||||
Some sessions can be inherited or shared across task boundaries while still
|
||||
binding to that environment.
|
||||
Git-status transport is still session-scoped. Each mounted consumer can
|
||||
subscribe with a different session ID, and each status event is then stored
|
||||
under the shared environment key in the frontend.
|
||||
|
||||
This creates competing writers. A delayed sibling session can publish a status
|
||||
from an obsolete workspace or an older database snapshot. The frontend then
|
||||
replaces the current environment status with that result. In the observed
|
||||
incident, 91 dirty files appeared and then disappeared when an older sibling's
|
||||
clean snapshot arrived later.
|
||||
|
||||
## Decision
|
||||
|
||||
Treat the task environment as the authority for current Git status. Treat a
|
||||
session ID only as a request and delivery handle.
|
||||
|
||||
Before the backend publishes current status, it must prove that the source
|
||||
session is bound to the task environment and has a raw recorded workspace path
|
||||
equal to the environment's canonical workspace. A live execution must match
|
||||
that workspace; a recovered execution may have an empty environment ID only
|
||||
after the session binding has been verified. A persisted snapshot is eligible
|
||||
only when its source session has the same canonical binding. Among eligible
|
||||
snapshots, the newest observation wins independently for each repository. The
|
||||
outgoing event keeps the requested session ID for protocol compatibility.
|
||||
|
||||
The frontend keeps Git status under the environment key and applies a monotonic
|
||||
timestamp rule per repository. An older observation cannot replace a newer
|
||||
one. This is a defense against response reordering after the backend has
|
||||
validated workspace authority.
|
||||
|
||||
Keep snapshots attributed to sessions. Resolve current environment authority
|
||||
at projection time. Do not add a second persisted Git-status owner.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Sibling subscriptions can no longer publish conflicting current status for
|
||||
one task environment.
|
||||
- A clean canonical status can replace dirty state. The rule does not prefer
|
||||
non-empty file lists.
|
||||
- Historical sessions with an obsolete workspace path remain in the audit
|
||||
record but cannot supply current environment status.
|
||||
- Multi-repository status remains session-attributed and is retained one row per
|
||||
repository for fallback hydration.
|
||||
- A lookup failure can temporarily produce no hydration event. It cannot clear
|
||||
correct frontend state with unverified data.
|
||||
- PR #3167 remains necessary because it repairs the workspace path used when a
|
||||
session resumes. This decision protects the projection boundary before and
|
||||
after that repair.
|
||||
- No database migration or WebSocket action change is required.
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
1. **Accept the last event received.** Rejected because network and hydration
|
||||
order do not define workspace authority.
|
||||
2. **Reject only older timestamps in the frontend.** Rejected as the complete
|
||||
fix because a newer event can still come from the wrong workspace.
|
||||
3. **Prefer non-empty status.** Rejected because a real clean workspace must be
|
||||
able to clear old dirty state.
|
||||
4. **Unsubscribe hidden session components.** Rejected because another current
|
||||
or future consumer can recreate the same competing-writer bug.
|
||||
5. **Move all snapshots to the task-environment row.** Rejected because it
|
||||
removes useful session attribution and adds a second persistence model when
|
||||
projection-time validation is sufficient.
|
||||
@@ -220,3 +220,4 @@ Read individual ADRs for full context. Create new ones via `/record decision` or
|
||||
| 2026-08-30-empty-remote-bootstrap-publication | [Keep Empty Remote Bootstrap Local Until Publication](2026-08-30-empty-remote-bootstrap-publication.md) | accepted | backend, frontend, protocol, security | 2026-08-30 |
|
||||
| 2026-08-30-bound-untracked-dependency-enumeration | [Bound Untracked Dependency Enumeration](2026-08-30-bound-untracked-dependency-enumeration.md) | accepted | backend | 2026-08-30 |
|
||||
| 2026-08-30-e2e-browser-cache | [Cache host-runner E2E browser provisioning](2026-08-30-e2e-browser-cache.md) | accepted | infra, workflow | 2026-08-30 |
|
||||
| 2026-08-30-environment-owned-git-status | [Project Current Git Status From the Task Environment](2026-08-30-environment-owned-git-status.md) | proposed | backend, frontend, protocol | 2026-08-30 |
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
---
|
||||
spec: docs/specs/tasks/requirements/additional-session-workspace-reuse.md
|
||||
design: docs/specs/tasks/system-design/environment-owned-git-status.md
|
||||
created: 2026-08-30
|
||||
status: complete
|
||||
---
|
||||
|
||||
# Fix Plan: Keep Shared-Environment Git Status Authoritative
|
||||
|
||||
## Overview
|
||||
|
||||
The Changes panel caches Git status by task environment, but backend hydration
|
||||
currently reads status by requested session. Two sibling subscriptions can
|
||||
therefore write different workspace results into one frontend cache entry.
|
||||
|
||||
The fix makes the backend select status only from a session bound to the task
|
||||
environment's canonical workspace. It also makes the frontend reject older
|
||||
environment observations. A focused end-to-end test proves that dirty files
|
||||
remain visible through reload and sibling hydration.
|
||||
|
||||
## Confirmed root cause
|
||||
|
||||
For task `c086d141-1966-4502-923a-c5ede67876d3`, two sessions shared task
|
||||
environment `9d364cad-dde8-4e02-b29b-a07c83f66165` but recorded different
|
||||
workspace paths. The canonical session had 91 dirty files. The older sibling
|
||||
pointed at a clean checkout.
|
||||
|
||||
Both live Git requests timed out. `appendDBSnapshotGitStatus` then loaded a
|
||||
snapshot by each requested session ID. The frontend mapped both events to the
|
||||
same environment key. It stored the 91-file snapshot first and replaced it
|
||||
147 milliseconds later with the sibling's clean snapshot.
|
||||
|
||||
PR #3167 fixes the recovery path that can preserve an incorrect session
|
||||
workspace. It does not make current Git-status projection environment-owned or
|
||||
order frontend status observations.
|
||||
|
||||
## Approach
|
||||
|
||||
1. Resolve eligible Git-status source sessions from the canonical task
|
||||
environment before live or snapshot hydration.
|
||||
2. Route the canonical result through the requested subscription session ID.
|
||||
3. Keep a monotonic timestamp watermark per environment and repository in the
|
||||
frontend store.
|
||||
4. Extend the two-session E2E coverage to prove that Changes remains populated
|
||||
after reload and sibling hydration.
|
||||
|
||||
Do not add a database table, change the WebSocket action, prefer dirty state,
|
||||
or hide the problem by removing sibling subscriptions.
|
||||
|
||||
## Task waves
|
||||
|
||||
### Wave 1: backend authority
|
||||
|
||||
- [x] [Task 01: Select canonical Git-status sources](task-01-canonical-status-source.md)
|
||||
|
||||
### Wave 2: ordering and user regression
|
||||
|
||||
- [x] [Task 02: Reject older environment status](task-02-status-ordering-and-e2e.md)
|
||||
|
||||
Task 02 depends on Task 01. The end-to-end result requires the backend authority
|
||||
rule and the frontend ordering safeguard together.
|
||||
|
||||
## Verification strategy
|
||||
|
||||
- Backend unit tests reproduce same-environment sessions with different
|
||||
workspace paths and prove live and snapshot source selection.
|
||||
- Frontend unit tests reproduce a newer dirty event followed by an older clean
|
||||
sibling event.
|
||||
- A Chromium E2E test creates a dirty file in a shared environment and proves
|
||||
that it remains visible after reload and sibling-session hydration.
|
||||
- Specification lint and `git diff --check` validate the delivery records.
|
||||
|
||||
## Risks and mitigations
|
||||
|
||||
- **Remote workspace paths:** compare the same effective workspace field that
|
||||
the task environment and session persist. Test host execution now and avoid
|
||||
host-only path resolution such as symlink evaluation.
|
||||
- **Missing historical paths:** fail closed for environment-owned hydration.
|
||||
A later valid live update can recover the view.
|
||||
- **Equal timestamps:** reject only strictly older valid timestamps. Preserve
|
||||
meaningful equal-time updates for compatibility.
|
||||
- **Duplicate updates:** advance the timestamp watermark without invalidating
|
||||
the cumulative diff cache when Git content is unchanged.
|
||||
- **PR #3167 overlap:** rebase before delivery and keep its recovery-path tests.
|
||||
This package owns projection authority and ordering, not workspace recovery.
|
||||
|
||||
## Mobile parity
|
||||
|
||||
Desktop and mobile Changes surfaces share the same environment-keyed store.
|
||||
This fix changes no layout, navigation, control, gesture, or copy. The store
|
||||
unit test and shared-state E2E evidence cover the mobile data contract. A new
|
||||
mobile E2E scenario duplicates the same state assertion. It does not test a
|
||||
mobile-specific interaction.
|
||||
|
||||
## Implementation results
|
||||
|
||||
- Task 01 selects live and persisted Git-status sources from the canonical task
|
||||
environment and keeps the requested session ID on the outgoing event.
|
||||
- Task 02 applies per-environment and per-repository timestamp ordering, keeps
|
||||
duplicate-content watermarks without invalidating cumulative diffs, and adds
|
||||
the shared-environment Chromium regression.
|
||||
- PR fixup coverage now verifies raw workspace provenance, inherited
|
||||
cross-task environment bindings, recovered executions, one deadline across
|
||||
live source probes, and one persisted fallback status per repository.
|
||||
- The shared desktop/mobile store contract is covered without a duplicate
|
||||
mobile E2E case because presentation and interaction are unchanged.
|
||||
- The exact RED/GREEN evidence and verification results are recorded in the
|
||||
two work orders.
|
||||
@@ -0,0 +1,109 @@
|
||||
---
|
||||
id: "01-canonical-status-source"
|
||||
title: "Select canonical Git-status sources"
|
||||
status: done
|
||||
wave: 1
|
||||
depends_on: []
|
||||
plan: "plan.md"
|
||||
spec: "../../specs/tasks/requirements/additional-session-workspace-reuse.md"
|
||||
design: "../../specs/tasks/system-design/environment-owned-git-status.md"
|
||||
---
|
||||
|
||||
# Task 01: Select Canonical Git-Status Sources
|
||||
|
||||
## Goal
|
||||
|
||||
Make session subscription and explicit Git refresh project current status only
|
||||
from a session and execution bound to the task environment's canonical
|
||||
workspace.
|
||||
|
||||
## Scope
|
||||
|
||||
- Add focused source-selection helpers in
|
||||
`apps/backend/internal/backendapp/helpers.go` and
|
||||
`apps/backend/internal/backendapp/git_status_sources.go`.
|
||||
- Resolve the requested session's task environment and every session bound to
|
||||
that environment, including inherited sessions from another task.
|
||||
- Validate session and live-execution workspace identity before a live query.
|
||||
- Batch-load eligible snapshots and select the newest observation per
|
||||
repository after live status is unavailable.
|
||||
- Route the selected result with the requested subscription session ID.
|
||||
- Add bounded debug logs for source choice and rejection reason.
|
||||
|
||||
## Exclusions
|
||||
|
||||
- Do not change workspace recovery or resume path selection from PR #3167.
|
||||
- Do not move snapshots to a task-environment table.
|
||||
- Do not expose workspace paths in logs or WebSocket payloads.
|
||||
- Do not prefer dirty or non-empty status.
|
||||
|
||||
## Requirements
|
||||
|
||||
- `REQ-TASKS-ADDITIONAL-SESSION-WORKSPACE-REUSE-001`
|
||||
- `AC-TASKS-ADDITIONAL-SESSION-WORKSPACE-REUSE-001.5`
|
||||
|
||||
## RED regression
|
||||
|
||||
Add backend tests that seed two sessions with one task-environment ID and two
|
||||
different workspace paths. The requested sibling points at the non-canonical
|
||||
path. The canonical sibling has a dirty snapshot.
|
||||
|
||||
Before production changes, prove that hydration returns the requested sibling's
|
||||
clean snapshot. Also cover a mismatched live execution so it cannot bypass the
|
||||
snapshot authority rule.
|
||||
|
||||
Suggested test names:
|
||||
|
||||
- `TestAppendLiveGitStatusMessageSelectsCanonicalEnvironmentSnapshot`
|
||||
- `TestAppendLiveGitStatusMessageRejectsMismatchedLiveExecution`
|
||||
|
||||
## Acceptance
|
||||
|
||||
- A mismatched sibling cannot supply live or persisted current status for the
|
||||
shared environment.
|
||||
- A canonical sibling can supply status while the outgoing event remains routed
|
||||
to the requested subscription session.
|
||||
- A lookup error or an environment with no eligible source emits no suspect
|
||||
status and does not fall back to the mismatched session.
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
cd apps/backend && go test -tags fts5 ./internal/backendapp -run 'TestAppendLiveGitStatusMessage.*(Canonical|Mismatched)' -count=1
|
||||
```
|
||||
|
||||
## Files likely touched
|
||||
|
||||
- `apps/backend/internal/backendapp/helpers.go`
|
||||
- `apps/backend/internal/backendapp/git_status_sources.go`
|
||||
- `apps/backend/internal/backendapp/helpers_git_status_test.go`
|
||||
- `apps/backend/internal/task/repository/sqlite/session.go`
|
||||
|
||||
## Dependencies
|
||||
|
||||
None. Rebase on PR #3167 before delivery if it merges first.
|
||||
|
||||
## Output contract
|
||||
|
||||
Report RED and GREEN evidence, the source-selection order, changed files, test
|
||||
results, and residual compatibility behavior for sessions without an
|
||||
environment. Update this work order and `plan.md` in the implementation turn.
|
||||
|
||||
## Results
|
||||
|
||||
- RED evidence: the new canonical snapshot test stored no status, and the
|
||||
mismatched-live test selected the wrong live status before the production
|
||||
changes.
|
||||
- GREEN evidence: the targeted canonical and mismatched-source test command
|
||||
passed 3 tests.
|
||||
- Source order: a matching live execution is preferred; when live status is
|
||||
unavailable, the newest timestamped snapshot from canonical-bound sessions
|
||||
is selected. The requested session ID remains on the published event.
|
||||
- Environment-owned hydration fails closed when the task environment is
|
||||
missing, mismatched, or has no eligible source. Sessions without an
|
||||
environment retain the existing session-scoped behavior.
|
||||
- The fixup adds raw workspace provenance checks, environment-wide source
|
||||
enumeration across task boundaries, recovered-execution handling, a single
|
||||
live-probe deadline, and per-repository snapshot fallback.
|
||||
- `go test -tags fts5 ./internal/backendapp -count=1` passed 754 tests, and
|
||||
`make -C apps/backend build` passed.
|
||||
@@ -0,0 +1,121 @@
|
||||
---
|
||||
id: "02-status-ordering-and-e2e"
|
||||
title: "Reject older environment status"
|
||||
status: done
|
||||
wave: 2
|
||||
depends_on:
|
||||
- "01-canonical-status-source"
|
||||
plan: "plan.md"
|
||||
spec: "../../specs/tasks/requirements/additional-session-workspace-reuse.md"
|
||||
design: "../../specs/tasks/system-design/environment-owned-git-status.md"
|
||||
---
|
||||
|
||||
# Task 02: Reject Older Environment Status
|
||||
|
||||
## Goal
|
||||
|
||||
Prevent a late, older Git-status event from replacing newer state in the shared
|
||||
environment cache, and prove the user-visible Changes result across sibling
|
||||
session hydration.
|
||||
|
||||
## Scope
|
||||
|
||||
- Add timestamp ordering to `applyGitStatus` for each environment and repository
|
||||
key.
|
||||
- Keep the newest valid timestamp as a watermark, including when Git content is
|
||||
unchanged.
|
||||
- Reject older dated entries. Also reject undated entries when the current
|
||||
state has a valid timestamp.
|
||||
- Keep equal-time meaningful updates compatible.
|
||||
- Extend focused WebSocket/store tests.
|
||||
- Add or extend a two-session Chromium E2E test. Create an uncommitted file,
|
||||
reload the task, and allow both sessions to hydrate. Switch between the
|
||||
sibling tabs, and prove that the file remains in Changes.
|
||||
|
||||
## Exclusions
|
||||
|
||||
- Do not add layout, touch, navigation, or user-copy changes.
|
||||
- Do not add a special rule that preserves non-empty file lists.
|
||||
- Do not remove or conditionally mount existing session subscriptions.
|
||||
- Do not add a separate mobile implementation or duplicate mobile E2E case.
|
||||
|
||||
## Requirements
|
||||
|
||||
- `REQ-TASKS-ADDITIONAL-SESSION-WORKSPACE-REUSE-001`
|
||||
- `AC-TASKS-ADDITIONAL-SESSION-WORKSPACE-REUSE-001.5`
|
||||
|
||||
## RED regressions
|
||||
|
||||
First add a unit test that maps two sessions to one environment. Deliver a
|
||||
newer dirty status through one session and an older clean status through the
|
||||
other. Prove that the current store incorrectly becomes clean before the fix.
|
||||
|
||||
Then add the user regression in
|
||||
`apps/web/e2e/tests/session/session-tab-management.spec.ts`. Confirm that the
|
||||
test reaches both session hydrations and observes the dirty file through the
|
||||
Changes UI.
|
||||
|
||||
Suggested unit test name:
|
||||
|
||||
- `does not overwrite newer environment status with an older sibling event`
|
||||
|
||||
## Acceptance
|
||||
|
||||
- Older or unknown-age events cannot replace a newer dated environment status.
|
||||
- Duplicate content can advance the timestamp watermark without invalidating
|
||||
the cumulative diff cache.
|
||||
- The Changes panel keeps the uncommitted file visible through reload and a
|
||||
sibling-session switch.
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
cd apps/web && pnpm test -- lib/ws/handlers/git-status.test.ts
|
||||
make -C apps/backend build
|
||||
cd apps/web && pnpm run build:e2e
|
||||
cd apps/web && pnpm e2e:run tests/session/session-tab-management.spec.ts --project=chromium -g "keeps dirty Changes status after sibling hydration"
|
||||
```
|
||||
|
||||
## Files likely touched
|
||||
|
||||
- `apps/web/lib/state/slices/session-runtime/session-runtime-slice.ts`
|
||||
- `apps/web/lib/state/slices/session-runtime/git-status-state.ts`
|
||||
- `apps/web/lib/ws/handlers/git-status.test.ts`
|
||||
- `apps/web/e2e/tests/session/session-tab-management.spec.ts`
|
||||
|
||||
## Dependencies
|
||||
|
||||
Task 01. Rebase on PR #3167 before delivery if it merges first.
|
||||
|
||||
## Mobile parity
|
||||
|
||||
Desktop and mobile Changes surfaces read the same status store. This task does
|
||||
not change presentation or interaction. The unit regression proves the shared
|
||||
data rule, and the Chromium E2E test proves the user-visible flow. No
|
||||
mobile-specific behavior needs a second browser case.
|
||||
|
||||
## Output contract
|
||||
|
||||
Report RED and GREEN evidence, timestamp handling for invalid and equal values,
|
||||
changed files, unit and E2E results, and any observed flake risk. Update this
|
||||
work order and `plan.md` in the implementation turn.
|
||||
|
||||
## Results
|
||||
|
||||
- RED evidence: the shared-environment handler test allowed an older clean
|
||||
sibling event to replace the newer dirty status before timestamp ordering
|
||||
was implemented.
|
||||
- GREEN evidence: the focused handler, session-runtime return-value, and
|
||||
multi-repository tests passed 25 tests. The exact work-order command
|
||||
`pnpm test -- lib/ws/handlers/git-status.test.ts` also passes.
|
||||
- Valid timestamps are monotonic per environment and repository. Older valid
|
||||
timestamps and undated entries after dated state are rejected. Equal-time
|
||||
content changes remain accepted, and newer duplicate content advances the
|
||||
watermark without invalidating the cumulative diff cache.
|
||||
- The managed Chromium regression passed: it reloads the shared task, waits
|
||||
for both sibling `session.git.event` hydrations, and keeps the dirty file
|
||||
visible in Changes after switching sessions. No flake or retry was needed.
|
||||
- `pnpm run typecheck`, `pnpm run lint`, `pnpm run build:e2e`, and the focused
|
||||
Chromium E2E command passed. Mobile uses the same store and has no
|
||||
presentation or interaction change, so no duplicate mobile scenario was
|
||||
added.
|
||||
@@ -110,6 +110,7 @@ signals, and task-scoped scheduling contracts.
|
||||
### System design
|
||||
|
||||
- [Additional Session Workspace Reuse](system-design/additional-session-workspace-reuse.md)
|
||||
- [Environment-Owned Git Status](system-design/environment-owned-git-status.md)
|
||||
- [Attach Workspace Sources](system-design/attach-workspace-sources.md)
|
||||
- [Quick Chat Agent Titles](system-design/quick-chat-agent-titles.md)
|
||||
- [Quick Chat Session Resumption](system-design/quick-chat-session-resumption.md)
|
||||
|
||||
@@ -34,6 +34,10 @@ preserving independent session runtime state.
|
||||
- **AC-TASKS-ADDITIONAL-SESSION-WORKSPACE-REUSE-001.4:** Unsafe or unsupported
|
||||
reuse shall fail with a typed, recoverable API error without creating a
|
||||
session or replacement workspace.
|
||||
- **AC-TASKS-ADDITIONAL-SESSION-WORKSPACE-REUSE-001.5:** Desktop and mobile
|
||||
workspace views shall show one current Git status for sessions that share a
|
||||
task environment. A response from a non-canonical workspace or an older
|
||||
observation shall not replace that status.
|
||||
|
||||
### REQ-TASKS-ADDITIONAL-SESSION-WORKSPACE-REUSE-002: Canonical Workspace Identity Continuity
|
||||
|
||||
@@ -115,6 +119,8 @@ an optional session name remains best effort after a successful launch.
|
||||
|
||||
- A named or unnamed additional session sees the same uncommitted tracked and
|
||||
untracked files as the task's first session.
|
||||
- A late status response from a sibling session does not clear or replace the
|
||||
current task-environment status in desktop or mobile workspace views.
|
||||
- Git worktree inventory, HEAD, index, branch and status remain unchanged by
|
||||
an additional launch.
|
||||
- A terminal primary or zero-session task can reuse its retained ready
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
---
|
||||
status: draft
|
||||
system: tasks
|
||||
requirements:
|
||||
- REQ-TASKS-ADDITIONAL-SESSION-WORKSPACE-REUSE-001
|
||||
---
|
||||
|
||||
# Environment-Owned Git Status System Design
|
||||
|
||||
## Purpose and boundaries
|
||||
|
||||
The task environment owns the current workspace. It also owns the current Git
|
||||
status that desktop and mobile workspace views show. A task session is a
|
||||
transport handle for status requests and events. It is not a separate owner of
|
||||
workspace state.
|
||||
|
||||
This design covers status hydration, WebSocket delivery, frontend cache
|
||||
ordering, and the Changes surfaces. It does not change Git polling inside
|
||||
agentctl, Git operations, commit history, or workspace materialization.
|
||||
|
||||
The workspace-path repair in PR #3167 prevents a resumed session from keeping
|
||||
an incorrect workspace path. This design adds a separate boundary. It prevents
|
||||
historical or delayed sibling-session data from becoming authoritative after a
|
||||
task environment already has a canonical workspace.
|
||||
|
||||
## Requirement mapping
|
||||
|
||||
| Requirement | Design section |
|
||||
| --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `AC-TASKS-ADDITIONAL-SESSION-WORKSPACE-REUSE-001.5` | [Backend status authority](#backend-status-authority), [Frontend ordering](#frontend-ordering), [Workspace views](#workspace-views) |
|
||||
|
||||
## Backend status authority
|
||||
|
||||
`buildSessionDataProvider` and `buildSessionGitDataProvider` in
|
||||
`apps/backend/internal/backendapp/helpers.go` accept a session ID because the
|
||||
WebSocket protocol subscribes to sessions. Before either provider reads live
|
||||
or persisted Git status, it resolves the session's `TaskEnvironment` and the
|
||||
all sessions bound to that environment. This includes inherited or shared
|
||||
sessions whose task differs from the environment owner's task.
|
||||
|
||||
The provider selects a source session with these rules:
|
||||
|
||||
1. If the requested session has no task environment, keep the legacy
|
||||
session-scoped behavior.
|
||||
2. Read the canonical workspace path from
|
||||
`task_environments.workspace_path`.
|
||||
3. Keep only sessions that reference the same task environment and whose raw
|
||||
recorded workspace path is present and exactly matches the canonical path.
|
||||
The projected environment path cannot prove that a historical session was
|
||||
recorded against the canonical workspace.
|
||||
4. Prefer a matching live execution. Its workspace path must match the
|
||||
canonical environment. A recovered execution with an empty task-environment
|
||||
ID is accepted only after the session binding and workspace checks pass. A
|
||||
non-empty mismatched ID is rejected before the provider asks agentctl for
|
||||
status.
|
||||
5. If no live query succeeds, load the latest ranked snapshot for each
|
||||
matching source session and repository, then select the newest observation
|
||||
for each repository.
|
||||
6. Publish the selected status with the requested subscription session ID so
|
||||
existing WebSocket routing and environment mapping remain compatible. A
|
||||
multi-repository workspace publishes one event per repository.
|
||||
|
||||
The provider does not use a clean status, a non-empty file list, or a primary
|
||||
session flag as authority. A clean canonical workspace is valid. Session
|
||||
selection uses workspace identity, and snapshot selection uses observation
|
||||
time only after the workspace identity matches.
|
||||
|
||||
The implementation uses environment-bound session queries,
|
||||
`GetTaskEnvironment`, and a per-session/per-repository snapshot query. It does
|
||||
not add a database table or copy Git status onto the task-environment row.
|
||||
|
||||
## Frontend ordering
|
||||
|
||||
`session.git.event` continues to carry a session ID. The session-runtime store
|
||||
continues to map that ID through `environmentIdBySessionId` and to cache status
|
||||
under `gitStatus.byEnvironmentId` and `gitStatus.byEnvironmentRepo`.
|
||||
|
||||
For each environment and repository key, `setGitStatus` keeps the newest valid
|
||||
status timestamp it has observed. It rejects an incoming entry with an older
|
||||
valid timestamp. If an entry has the same Git content and a newer timestamp,
|
||||
the store advances the timestamp watermark without invalidating the cumulative
|
||||
diff cache. This prevents an older hydration response from winning only because
|
||||
it arrived later.
|
||||
|
||||
An entry with no valid timestamp cannot replace an entry that already has a
|
||||
valid timestamp. This rule keeps a legacy snapshot with unknown age from
|
||||
replacing a dated live observation. Existing undated state can still accept a
|
||||
new entry, which preserves compatibility during initial hydration.
|
||||
|
||||
Timestamp ordering is a safeguard, not the source-authority rule. The backend
|
||||
must first exclude data from a different workspace.
|
||||
|
||||
## Workspace views
|
||||
|
||||
Desktop and mobile Changes surfaces use the same environment-keyed store and
|
||||
Git-status hooks. This change does not add controls, change layout, or alter
|
||||
touch behavior. Both surfaces receive the same corrected state without separate
|
||||
presentation logic.
|
||||
|
||||
The desktop end-to-end regression uses two sessions in one environment. It
|
||||
creates an uncommitted file, reloads the task, and proves that the file stays
|
||||
visible after both session hydrations and a sibling-session switch. A focused
|
||||
store test proves that an older sibling event cannot replace newer environment
|
||||
state. No separate mobile end-to-end test is required because this design does
|
||||
not change mobile composition or interaction, and the mobile surface reads the
|
||||
same store contract.
|
||||
|
||||
## Failure and recovery
|
||||
|
||||
If environment or sibling-session lookup fails, the provider logs the failure
|
||||
and does not publish an unverified session snapshot. The next Git poll, focus
|
||||
refresh, reconnect, or explicit refresh can retry. The provider keeps legacy
|
||||
session behavior only when the session has no task-environment identity.
|
||||
|
||||
If a live status request times out, the provider can use only a snapshot whose
|
||||
source session matches the canonical environment workspace. If none exists, it
|
||||
returns no status rather than clearing the current frontend state with suspect
|
||||
data. The live probe has one deadline for all eligible sources, so a stuck
|
||||
sibling cannot extend hydration once per source.
|
||||
|
||||
A corrected session path from PR #3167 makes that session eligible on its next
|
||||
hydration. No database migration or historical snapshot rewrite is required.
|
||||
|
||||
## Persistence
|
||||
|
||||
Git snapshots remain session-attributed history in
|
||||
`task_session_git_snapshots`. The source session preserves the audit trail.
|
||||
Environment authority is resolved when current status is projected. This keeps
|
||||
historical records intact and avoids a second status ownership model. Live
|
||||
monitor snapshots are retained one per session and repository so fallback can
|
||||
rebuild a multi-repository status.
|
||||
|
||||
## Observability
|
||||
|
||||
Backend logs for live-query and snapshot fallback include the requested session
|
||||
ID, selected source session ID, task-environment ID, and a bounded reason when a
|
||||
candidate is rejected. Logs must not include workspace paths.
|
||||
|
||||
Frontend debug logs include the environment key, repository name, incoming and
|
||||
stored timestamps, and whether ordering rejected the event. Existing file-count
|
||||
and cache-invalidation fields remain.
|
||||
|
||||
## Verification
|
||||
|
||||
- Backend tests seed same-environment sessions, including an inherited session
|
||||
from another task, with different workspace paths. They prove that live and
|
||||
snapshot hydration select only canonical sources, preserve one event per
|
||||
repository, and route results to the requested subscription.
|
||||
- Frontend tests deliver a newer dirty status and then an older clean status
|
||||
from a sibling session. They prove that the environment cache stays dirty and
|
||||
the cumulative diff cache is not invalidated by the rejected event.
|
||||
- The desktop end-to-end test proves that uncommitted files remain visible
|
||||
through reload and sibling-session hydration.
|
||||
|
||||
## Related decisions
|
||||
|
||||
- [Keep Worktree Ownership at the Task Lifecycle](../../../decisions/2026-08-08-task-owned-worktree-lifetime.md)
|
||||
- [Project Current Git Status From the Task Environment](../../../decisions/2026-08-30-environment-owned-git-status.md)
|
||||
Reference in New Issue
Block a user